refactor: logic for role template selection

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/3445/head
Ryan Wang 2022-07-26 18:04:51 +08:00
parent 4fb5c6f588
commit a70fd5eec4
5 changed files with 201 additions and 135 deletions

View File

@ -26,8 +26,8 @@ const buttonClassification = {
const theme: Record<string, Record<string, string>> = { const theme: Record<string, Record<string, string>> = {
global: { global: {
outer: "formkit-disabled:opacity-50", outer: "formkit-disabled:opacity-50",
help: "text-xs text-gray-500", help: "text-xs mt-1.5 text-gray-500",
messages: "list-none p-0 mt-1 mb-0", messages: "list-none p-0 mt-1.5 mb-0",
message: "text-red-500 mb-1 text-xs", message: "text-red-500 mb-1 text-xs",
form: "flex flex-col space-y-4", form: "flex flex-col space-y-4",
}, },

View File

@ -10,27 +10,21 @@ import {
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { computed, onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { apiClient } from "@halo-dev/admin-shared"; import { apiClient } from "@halo-dev/admin-shared";
import type { Role, User } from "@halo-dev/api-client"; import type { Role, User } from "@halo-dev/api-client";
import { pluginLabels, roleLabels } from "@/constants/labels"; import { pluginLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import { useRoleTemplateSelection } from "@/modules/system/roles/composables/use-role";
interface RoleTemplateGroup {
module: string | null | undefined;
roles: Role[];
}
interface FormState { interface FormState {
role: Role; role: Role;
selectedRoleTemplates: string[];
saving: boolean; saving: boolean;
} }
const route = useRoute(); const route = useRoute();
const users = ref<User[]>([]); const users = ref<User[]>([]);
const roles = ref<Role[]>([]);
const roleActiveId = ref("detail"); const roleActiveId = ref("detail");
const formState = ref<FormState>({ const formState = ref<FormState>({
role: { role: {
@ -46,36 +40,11 @@ const formState = ref<FormState>({
}, },
rules: [], rules: [],
}, },
selectedRoleTemplates: [],
saving: false, saving: false,
}); });
const roleTemplates = computed<Role[]>(() => { const { roleTemplateGroups, handleRoleTemplateSelect, selectedRoleTemplates } =
return roles.value.filter( useRoleTemplateSelection();
(role) =>
role.metadata.labels?.[roleLabels.TEMPLATE] === "true" &&
role.metadata.labels?.["halo.run/hidden"] !== "true"
);
});
const roleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
const groups: RoleTemplateGroup[] = [];
roleTemplates.value.forEach((role) => {
const group = groups.find(
(group) =>
group.module === role.metadata.annotations?.[rbacAnnotations.MODULE]
);
if (group) {
group.roles.push(role);
} else {
groups.push({
module: role.metadata.annotations?.[rbacAnnotations.MODULE],
roles: [role],
});
}
});
return groups;
});
const handleFetchRole = async () => { const handleFetchRole = async () => {
try { try {
@ -83,23 +52,17 @@ const handleFetchRole = async () => {
route.params.name as string route.params.name as string
); );
formState.value.role = response.data; formState.value.role = response.data;
formState.value.selectedRoleTemplates = JSON.parse( selectedRoleTemplates.value = new Set(
response.data.metadata.annotations?.[rbacAnnotations.DEPENDENCIES] || "[]" JSON.parse(
response.data.metadata.annotations?.[rbacAnnotations.DEPENDENCIES] ||
"[]"
)
); );
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
}; };
const handleFetchRoles = async () => {
try {
const { data } = await apiClient.extension.role.listv1alpha1Role();
roles.value = data.items;
} catch (e) {
console.error(e);
}
};
const handleFetchUsers = async () => { const handleFetchUsers = async () => {
try { try {
const { data } = await apiClient.extension.user.listv1alpha1User(); const { data } = await apiClient.extension.user.listv1alpha1User();
@ -114,7 +77,7 @@ const handleUpdateRole = async () => {
formState.value.saving = true; formState.value.saving = true;
if (formState.value.role.metadata.annotations) { if (formState.value.role.metadata.annotations) {
formState.value.role.metadata.annotations[rbacAnnotations.DEPENDENCIES] = formState.value.role.metadata.annotations[rbacAnnotations.DEPENDENCIES] =
JSON.stringify(formState.value.selectedRoleTemplates); JSON.stringify(Array.from(selectedRoleTemplates.value));
} }
await apiClient.extension.role.updatev1alpha1Role( await apiClient.extension.role.updatev1alpha1Role(
route.params.name as string, route.params.name as string,
@ -136,7 +99,6 @@ const handleRouteToUser = (name: string) => {
onMounted(() => { onMounted(() => {
handleFetchRole(); handleFetchRole();
handleFetchRoles();
handleFetchUsers(); handleFetchUsers();
}); });
</script> </script>
@ -337,13 +299,14 @@ onMounted(() => {
<ul class="space-y-2"> <ul class="space-y-2">
<li v-for="(role, index) in group.roles" :key="index"> <li v-for="(role, index) in group.roles" :key="index">
<label <label
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-primary" class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary"
> >
<input <input
v-model="formState.selectedRoleTemplates" v-model="selectedRoleTemplates"
:value="role.metadata.name" :value="role.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox" type="checkbox"
@change="handleRoleTemplateSelect"
/> />
<div class="flex flex-1 flex-col gap-y-3"> <div class="flex flex-1 flex-col gap-y-3">
<span class="font-medium text-gray-900"> <span class="font-medium text-gray-900">

View File

@ -11,7 +11,7 @@ import {
VSpace, VSpace,
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import RoleCreationModal from "./components/RoleCreationModal.vue"; import RoleEditingModal from "./components/RoleEditingModal.vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import type { Role } from "@halo-dev/api-client"; import type { Role } from "@halo-dev/api-client";
@ -48,10 +48,7 @@ onMounted(() => {
}); });
</script> </script>
<template> <template>
<RoleCreationModal <RoleEditingModal v-model:visible="createVisible" @close="handleFetchRoles" />
v-model:visible="createVisible"
@close="handleFetchRoles"
/>
<VPageHeader title="角色"> <VPageHeader title="角色">
<template #icon> <template #icon>

View File

@ -1,23 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import { VButton, VModal, VTabItem, VTabs } from "@halo-dev/components"; import { VButton, VModal, VTabItem, VTabs } from "@halo-dev/components";
import { computed, ref, watch } from "vue"; import { ref } from "vue";
import { apiClient } from "@halo-dev/admin-shared"; import { apiClient } from "@halo-dev/admin-shared";
import type { Role } from "@halo-dev/api-client"; import type { Role } from "@halo-dev/api-client";
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import { roleLabels } from "@/constants/labels"; import { useRoleTemplateSelection } from "@/modules/system/roles/composables/use-role";
interface RoleTemplateGroup { interface FormState {
module: string | null | undefined;
roles: Role[];
}
interface CreationFormState {
role: Role; role: Role;
selectedRoleTemplates: string[];
saving: boolean; saving: boolean;
} }
const props = defineProps({ defineProps({
visible: { visible: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -26,9 +20,11 @@ const props = defineProps({
const emit = defineEmits(["update:visible", "close"]); const emit = defineEmits(["update:visible", "close"]);
const creationActiveId = ref("general"); const { roleTemplateGroups, handleRoleTemplateSelect, selectedRoleTemplates } =
const roles = ref<Role[]>([]); useRoleTemplateSelection();
const creationFormState = ref<CreationFormState>({
const activeId = ref("general");
const formState = ref<FormState>({
role: { role: {
apiVersion: "v1alpha1", apiVersion: "v1alpha1",
kind: "Role", kind: "Role",
@ -42,62 +38,22 @@ const creationFormState = ref<CreationFormState>({
}, },
rules: [], rules: [],
}, },
selectedRoleTemplates: [],
saving: false, saving: false,
}); });
const roleTemplates = computed<Role[]>(() => {
return roles.value.filter(
(role) =>
role.metadata.labels?.[roleLabels.TEMPLATE] === "true" &&
role.metadata.labels?.["halo.run/hidden"] !== "true"
);
});
const roleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
const groups: RoleTemplateGroup[] = [];
roleTemplates.value.forEach((role) => {
const group = groups.find(
(group) =>
group.module === role.metadata.annotations?.[rbacAnnotations.MODULE]
);
if (group) {
group.roles.push(role);
} else {
groups.push({
module: role.metadata.annotations?.[rbacAnnotations.MODULE],
roles: [role],
});
}
});
return groups;
});
const handleFetchRoles = async () => {
try {
const { data } = await apiClient.extension.role.listv1alpha1Role();
roles.value = data.items;
} catch (e) {
console.error(e);
}
};
const handleCreateRole = async () => { const handleCreateRole = async () => {
try { try {
creationFormState.value.saving = true; formState.value.saving = true;
if (creationFormState.value.role.metadata.annotations) { if (formState.value.role.metadata.annotations) {
creationFormState.value.role.metadata.annotations[ formState.value.role.metadata.annotations[rbacAnnotations.DEPENDENCIES] =
rbacAnnotations.DEPENDENCIES JSON.stringify(Array.from(selectedRoleTemplates.value));
] = JSON.stringify(creationFormState.value.selectedRoleTemplates);
} }
await apiClient.extension.role.createv1alpha1Role( await apiClient.extension.role.createv1alpha1Role(formState.value.role);
creationFormState.value.role
);
handleVisibleChange(false); handleVisibleChange(false);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally { } finally {
creationFormState.value.saving = false; formState.value.saving = false;
} }
}; };
@ -107,15 +63,6 @@ const handleVisibleChange = (visible: boolean) => {
emit("close"); emit("close");
} }
}; };
watch(
() => props.visible,
(visible) => {
if (visible) {
handleFetchRoles();
}
}
);
</script> </script>
<template> <template>
<VModal <VModal
@ -124,10 +71,10 @@ watch(
title="创建角色" title="创建角色"
@update:visible="handleVisibleChange" @update:visible="handleVisibleChange"
> >
<VTabs v-model:active-id="creationActiveId" type="outline"> <VTabs v-model:active-id="activeId" type="outline">
<VTabItem id="general" label="基础信息"> <VTabItem id="general" label="基础信息">
<FormKit <FormKit
v-if="creationFormState.role.metadata.annotations" v-if="formState.role.metadata.annotations"
id="role-form" id="role-form"
:actions="false" :actions="false"
type="form" type="form"
@ -135,16 +82,14 @@ watch(
> >
<FormKit <FormKit
v-model=" v-model="
creationFormState.role.metadata.annotations[ formState.role.metadata.annotations[rbacAnnotations.DISPLAY_NAME]
rbacAnnotations.DISPLAY_NAME
]
" "
label="名称" label="名称"
type="text" type="text"
validation="required" validation="required"
></FormKit> ></FormKit>
<FormKit <FormKit
v-model="creationFormState.role.metadata.name" v-model="formState.role.metadata.name"
help="角色别名,用于区分角色,不能重复,创建之后不能修改" help="角色别名,用于区分角色,不能重复,创建之后不能修改"
label="别名" label="别名"
type="text" type="text"
@ -167,13 +112,14 @@ watch(
<ul class="space-y-2"> <ul class="space-y-2">
<li v-for="(role, index) in group.roles" :key="index"> <li v-for="(role, index) in group.roles" :key="index">
<label <label
class="inline-flex w-full cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-primary" class="inline-flex w-full cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary"
> >
<input <input
v-model="creationFormState.selectedRoleTemplates" v-model="selectedRoleTemplates"
:value="role.metadata.name" :value="role.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600" class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox" type="checkbox"
@change="handleRoleTemplateSelect"
/> />
<div class="flex flex-1 flex-col gap-y-3"> <div class="flex flex-1 flex-col gap-y-3">
<span class="font-medium text-gray-900"> <span class="font-medium text-gray-900">
@ -212,7 +158,7 @@ watch(
</VTabs> </VTabs>
<template #footer> <template #footer>
<VButton <VButton
:loading="creationFormState.saving" :loading="formState.saving"
type="secondary" type="secondary"
@click="$formkit.submit('role-form')" @click="$formkit.submit('role-form')"
>创建 >创建

View File

@ -0,0 +1,160 @@
import type { Role } from "@halo-dev/api-client";
import { computed, onMounted, ref } from "vue";
import { roleLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations";
import { apiClient } from "@halo-dev/admin-shared";
interface RoleTemplateGroup {
module: string | null | undefined;
roles: Role[];
}
export function useRoleTemplateSelection() {
const rawRoles = ref<Role[]>([] as Role[]);
const selectedRoleTemplates = ref<Set<string>>(new Set());
// Get all role templates based on the condition that `metadata.labels.[halo.run/role-template] === 'true'`
const roleTemplates = computed<Role[]>(() => {
return rawRoles.value.filter(
(role) =>
role.metadata.labels?.[roleLabels.TEMPLATE] === "true" &&
role.metadata.labels?.["halo.run/hidden"] !== "true"
);
});
/**
* Grouping role templates by module
* Example:
* {
* "module": "Users Management",
* "roles": [
* {
* "rules": [
* {
* "apiGroups": [
* ""
* ],
* "resources": [
* "users"
* ],
* "resourceNames": [],
* "nonResourceURLs": [],
* "verbs": [
* "create",
* "patch",
* "update",
* "delete",
* "deletecollection"
* ]
* }
* ],
* "apiVersion": "v1alpha1",
* "kind": "Role",
* "metadata": {
* "name": "role-template-manage-users",
* "labels": {
* "halo.run/role-template": "true"
* },
* "annotations": {
* "rbac.authorization.halo.run/dependencies": "[ \"role-template-view-users\", \"role-template-change-password\" ]\n",
* "rbac.authorization.halo.run/module": "Users Management",
* "rbac.authorization.halo.run/display-name": "User manage",
* "rbac.authorization.halo.run/ui-permissions": "[\"system:users:manage\"]\n",
* "rbac.authorization.halo.run/dependency-rules": "[{\"apiGroups\":[\"\"],\"resources\":[\"users\"],\"resourceNames\":[],\"nonResourceURLs\":[],\"verbs\":[\"get\",\"list\"]},{\"apiGroups\":[\"api.halo.run\"],\"resources\":[\"users/password\"],\"resourceNames\":[],\"nonResourceURLs\":[],\"verbs\":[\"update\"]}]",
* "rbac.authorization.halo.run/ui-permissions-aggregated": "[\"system:users:view\"]"
* },
* "version": 9
* }
* },
* {
* "rules": [
* {
* "apiGroups": [
* ""
* ],
* "resources": [
* "users"
* ],
* "resourceNames": [],
* "nonResourceURLs": [],
* "verbs": [
* "get",
* "list"
* ]
* }
* ],
* "apiVersion": "v1alpha1",
* "kind": "Role",
* "metadata": {
* "name": "role-template-view-users",
* "labels": {
* "halo.run/role-template": "true"
* },
* "annotations": {
* "rbac.authorization.halo.run/module": "Users Management",
* "rbac.authorization.halo.run/display-name": "User View",
* "rbac.authorization.halo.run/ui-permissions": "[\"system:users:view\"]\n",
* "rbac.authorization.halo.run/dependency-rules": "[]",
* "rbac.authorization.halo.run/ui-permissions-aggregated": "[]"
* },
* "version": 9
* }
* }
* ]
* }
*/
const roleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
const groups: RoleTemplateGroup[] = [];
roleTemplates.value.forEach((role) => {
const group = groups.find(
(group) =>
group.module === role.metadata.annotations?.[rbacAnnotations.MODULE]
);
if (group) {
group.roles.push(role);
} else {
groups.push({
module: role.metadata.annotations?.[rbacAnnotations.MODULE],
roles: [role],
});
}
});
return groups;
});
const handleFetchRoles = async () => {
try {
const { data } = await apiClient.extension.role.listv1alpha1Role();
rawRoles.value = data.items;
} catch (e) {
console.error(e);
}
};
const handleRoleTemplateSelect = async (e: Event) => {
const { checked, value } = e.target as HTMLInputElement;
if (!checked) {
return;
}
const role = rawRoles.value.find((role) => role.metadata.name === value);
const dependencies =
role?.metadata.annotations?.[rbacAnnotations.DEPENDENCIES];
if (!dependencies) {
return;
}
const dependenciesArray = JSON.parse(dependencies);
dependenciesArray.forEach((role) => {
selectedRoleTemplates.value.add(role);
});
};
onMounted(handleFetchRoles);
return {
rawRoles,
selectedRoleTemplates,
roleTemplates,
roleTemplateGroups,
handleRoleTemplateSelect,
};
}