feat: refine roles management

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/588/head
Ryan Wang 2022-07-04 19:28:53 +08:00
parent ace341efef
commit f18951abd2
3 changed files with 384 additions and 216 deletions

View File

@ -10,27 +10,92 @@ import {
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { axiosInstance } from "@halo-dev/admin-shared"; import { axiosInstance } from "@halo-dev/admin-shared";
import type { Role, User } from "@/types/extension"; import type { Role, User } from "@/types/extension";
interface RoleTemplateGroup {
name: string | null | undefined;
roles: Role[];
}
interface FormState {
role: Role;
selectedRoleTemplates: string[];
saving: boolean;
}
const route = useRoute(); const route = useRoute();
const users = ref<User[]>([]); const users = ref<User[]>([]);
const role = ref<Role>(); const roles = ref<Role[]>([]);
const roleActiveId = ref("detail"); const roleActiveId = ref("detail");
const formState = ref<FormState>({
role: {
apiVersion: "v1alpha1",
kind: "Role",
metadata: {
name: "",
labels: {},
annotations: {
"plugin.halo.run/dependencies": "",
"plugin.halo.run/display-name": "",
},
},
},
selectedRoleTemplates: [],
saving: false,
});
const roleTemplates = computed<Role[]>(() => {
return roles.value.filter(
(role) => role.metadata.labels?.["plugin.halo.run/role-template"] === "true"
);
});
const roleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
const groups: RoleTemplateGroup[] = [];
roleTemplates.value.forEach((role) => {
const group = groups.find(
(group) =>
group.name === role.metadata.annotations?.["plugin.halo.run/module"]
);
if (group) {
group.roles.push(role);
} else {
groups.push({
name: role.metadata.annotations?.["plugin.halo.run/module"],
roles: [role],
});
}
});
return groups;
});
const handleFetchRole = async () => { const handleFetchRole = async () => {
try { try {
const response = await axiosInstance.get( const response = await axiosInstance.get<Role>(
`/api/v1alpha1/roles/${route.params.name}` `/api/v1alpha1/roles/${route.params.name}`
); );
role.value = response.data; formState.value.role = response.data;
formState.value.selectedRoleTemplates = JSON.parse(
response.data.metadata.annotations?.["plugin.halo.run/dependencies"] ||
"[]"
);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
}; };
const handleFetchRoles = async () => {
try {
const { data } = await axiosInstance.get("/api/v1alpha1/roles");
roles.value = data;
} catch (e) {
console.error(e);
}
};
const handleFetchUsers = async () => { const handleFetchUsers = async () => {
try { try {
const { data } = await axiosInstance.get("/api/v1alpha1/users"); const { data } = await axiosInstance.get("/api/v1alpha1/users");
@ -40,6 +105,26 @@ const handleFetchUsers = async () => {
} }
}; };
const handleUpdateRole = async () => {
try {
formState.value.saving = true;
if (formState.value.role.metadata.annotations) {
formState.value.role.metadata.annotations[
"plugin.halo.run/dependencies"
] = JSON.stringify(formState.value.selectedRoleTemplates);
}
await axiosInstance.put<Role>(
`/api/v1alpha1/roles/${route.params.name}`,
formState.value.role
);
} catch (e) {
console.error(e);
} finally {
formState.value.saving = false;
await handleFetchRole();
}
};
const router = useRouter(); const router = useRouter();
const handleRouteToUser = (name: string) => { const handleRouteToUser = (name: string) => {
@ -48,11 +133,17 @@ const handleRouteToUser = (name: string) => {
onMounted(() => { onMounted(() => {
handleFetchRole(); handleFetchRole();
handleFetchRoles();
handleFetchUsers(); handleFetchUsers();
}); });
</script> </script>
<template> <template>
<VPageHeader :title="`角色:${role?.metadata?.name}`"> <VPageHeader
:title="`角色:${
formState.role?.metadata?.annotations?.['plugin.halo.run/display-name'] ||
formState.role?.metadata?.name
}`"
>
<template #icon> <template #icon>
<IconShieldUser class="mr-2 self-center" /> <IconShieldUser class="mr-2 self-center" />
</template> </template>
@ -84,7 +175,7 @@ onMounted(() => {
<p <p
class="mt-1 flex max-w-2xl items-center gap-2 text-sm text-gray-500" class="mt-1 flex max-w-2xl items-center gap-2 text-sm text-gray-500"
> >
<span>包含 {{ role?.rules?.length }} 个权限</span> <span>包含 {{ formState.role?.rules?.length }} 个权限</span>
</p> </p>
</div> </div>
<div class="border-t border-gray-200"> <div class="border-t border-gray-200">
@ -94,7 +185,11 @@ onMounted(() => {
> >
<dt class="text-sm font-medium text-gray-900">名称</dt> <dt class="text-sm font-medium text-gray-900">名称</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0"> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ role?.metadata?.name }} {{
formState.role?.metadata?.annotations?.[
"plugin.halo.run/display-name"
] || formState.role?.metadata?.name
}}
</dd> </dd>
</div> </div>
<div <div
@ -102,7 +197,7 @@ onMounted(() => {
> >
<dt class="text-sm font-medium text-gray-900">别名</dt> <dt class="text-sm font-medium text-gray-900">别名</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0"> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ role?.metadata?.name }} {{ formState.role?.metadata?.name }}
</dd> </dd>
</div> </div>
<div <div
@ -126,7 +221,7 @@ onMounted(() => {
> >
<dt class="text-sm font-medium text-gray-900">创建时间</dt> <dt class="text-sm font-medium text-gray-900">创建时间</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0"> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ role?.metadata?.creationTimestamp }} {{ formState.role?.metadata?.creationTimestamp }}
</dd> </dd>
</div> </div>
<div <div
@ -193,220 +288,49 @@ onMounted(() => {
<div> <div>
<dl class="divide-y divide-gray-100"> <dl class="divide-y divide-gray-100">
<div <div
v-for="(group, groupIndex) in roleTemplateGroups"
:key="groupIndex"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6" class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
> >
<dt class="text-sm font-medium text-gray-900"> <dt class="text-sm font-medium text-gray-900">
Posts Management {{ group.name }}
</dt> </dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0"> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2"> <ul class="space-y-2">
<li> <li v-for="(role, index) in group.roles" :key="index">
<div <div
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-themeable-primary" class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-themeable-primary"
> >
<input <input
v-model="formState.selectedRoleTemplates"
: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"
/> />
<div class="inline-flex flex-col gap-y-3"> <div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900"> <span class="font-medium text-gray-900">
Posts Management {{
role.metadata.annotations?.[
"plugin.halo.run/display-name"
]
}}
</span> </span>
<span class="text-xs text-gray-400"> <span
依赖于 Posts View v-if="
</span> role.metadata.annotations?.[
</div> 'plugin.halo.run/dependencies'
</div> ]
</li> "
<li> class="text-xs text-gray-400"
<div
class="inline-flex w-72 cursor-pointer items-center gap-4 rounded border p-5 hover:border-themeable-primary"
> >
<input 依赖于
class="h-4 w-4 rounded border-gray-300 text-indigo-600" {{
type="checkbox" JSON.parse(
/> role.metadata.annotations?.[
<div class="inline-flex flex-col gap-y-3"> "plugin.halo.run/dependencies"
<span class="font-medium text-gray-900"> ]
Posts View ).join(", ")
</span> }}
</div>
</div>
</li>
</ul>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">
Categories Management
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2">
<li>
<div
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-themeable-primary"
>
<input
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
/>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
Categories Management
</span>
<span class="text-xs text-gray-400">
依赖于 Categories View
</span>
</div>
</div>
</li>
<li>
<div
class="inline-flex w-72 cursor-pointer items-center gap-4 rounded border p-5 hover:border-themeable-primary"
>
<input
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
/>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
Categories View
</span>
</div>
</div>
</li>
</ul>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">Tags Management</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2">
<li>
<div
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-themeable-primary"
>
<input
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
/>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
Tags Management
</span>
<span class="text-xs text-gray-400">
依赖于 Tags View
</span>
</div>
</div>
</li>
<li>
<div
class="inline-flex w-72 cursor-pointer items-center gap-4 rounded border p-5 hover:border-themeable-primary"
>
<input
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
/>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
Tags View
</span>
</div>
</div>
</li>
</ul>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">
Plugins Management
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2">
<li>
<div
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-themeable-primary"
>
<input
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
/>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
Plugins Management
</span>
<span class="text-xs text-gray-400">
依赖于 Plugins View
</span>
</div>
</div>
</li>
<li>
<div
class="inline-flex w-72 cursor-pointer items-center gap-4 rounded border p-5 hover:border-themeable-primary"
>
<input
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
/>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
Plugins View
</span>
</div>
</div>
</li>
</ul>
</dd>
</div>
<div
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt
class="flex flex-col gap-y-3 text-sm font-medium text-gray-900"
>
<span> Discussions Management </span>
<span class="text-xs text-gray-400"> 由社区插件提供 </span>
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2">
<li>
<div
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-themeable-primary"
>
<input
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
/>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
Discussions Management
</span>
<span class="text-xs text-gray-400">
依赖于 Discussions View
</span>
</div>
</div>
</li>
<li>
<div
class="inline-flex w-72 cursor-pointer items-center gap-4 rounded border p-5 hover:border-themeable-primary"
>
<input
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
/>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
Discussions View
</span> </span>
</div> </div>
</div> </div>
@ -416,7 +340,12 @@ onMounted(() => {
</div> </div>
</dl> </dl>
<div class="p-4"> <div class="p-4">
<VButton type="secondary">保存</VButton> <VButton
:loading="formState.saving"
type="secondary"
@click="handleUpdateRole"
>保存
</VButton>
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,19 +7,26 @@ import {
VButton, VButton,
VCard, VCard,
VInput, VInput,
VModal,
VPageHeader, VPageHeader,
VSpace, VSpace,
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import RoleCreationModal from "./components/RoleCreationModal.vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import type { Role } from "@/types/extension"; import type { Role } from "@/types/extension";
import { axiosInstance } from "@halo-dev/admin-shared"; import { axiosInstance } from "@halo-dev/admin-shared";
const createVisible = ref(false); const createVisible = ref(false);
const roles = ref<Role[]>([]); const roles = ref<Role[]>([]);
const basicRoles = computed(() => {
return roles.value.filter(
(role) =>
role.metadata?.labels?.["plugin.halo.run/role-template"] !== "true"
);
});
const router = useRouter(); const router = useRouter();
const handleFetchRoles = async () => { const handleFetchRoles = async () => {
@ -30,6 +37,7 @@ const handleFetchRoles = async () => {
console.error(e); console.error(e);
} }
}; };
const handleRouteToDetail = (name: string) => { const handleRouteToDetail = (name: string) => {
router.push({ name: "RoleDetail", params: { name } }); router.push({ name: "RoleDetail", params: { name } });
}; };
@ -39,7 +47,10 @@ onMounted(() => {
}); });
</script> </script>
<template> <template>
<VModal v-model:visible="createVisible" title="新建角色"></VModal> <RoleCreationModal
v-model:visible="createVisible"
@close="handleFetchRoles"
/>
<VPageHeader title="角色"> <VPageHeader title="角色">
<template #icon> <template #icon>
@ -152,7 +163,7 @@ onMounted(() => {
</template> </template>
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list"> <ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
<li <li
v-for="(role, index) in roles" v-for="(role, index) in basicRoles"
:key="index" :key="index"
@click="handleRouteToDetail(role.metadata.name)" @click="handleRouteToDetail(role.metadata.name)"
> >
@ -163,13 +174,15 @@ onMounted(() => {
<div class="flex-1"> <div class="flex-1">
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<span class="mr-2 truncate text-sm font-medium text-gray-900"> <span class="mr-2 truncate text-sm font-medium text-gray-900">
{{ role.metadata.name }} {{
role.metadata.annotations?.[
"plugin.halo.run/display-name"
] || role.metadata.name
}}
</span> </span>
</div> </div>
<div class="mt-2 flex"> <div class="mt-2 flex">
<span class="text-xs text-gray-500"> <span class="text-xs text-gray-500"> 包含 0 个权限 </span>
包含 {{ role.rules?.length }} 个权限
</span>
</div> </div>
</div> </div>
<div class="flex"> <div class="flex">

View File

@ -0,0 +1,226 @@
<script lang="ts" setup>
import { VButton, VInput, VModal, VTabItem, VTabs } from "@halo-dev/components";
import { computed, ref, watch } from "vue";
import type { Role } from "@/types/extension";
import { axiosInstance } from "@halo-dev/admin-shared";
interface RoleTemplateGroup {
name: string | null | undefined;
roles: Role[];
}
interface CreationFormState {
role: Role;
selectedRoleTemplates: string[];
saving: boolean;
}
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:visible", "close"]);
const creationActiveId = ref("general");
const roles = ref<Role[]>([]);
const creationFormState = ref<CreationFormState>({
role: {
apiVersion: "v1alpha1",
kind: "Role",
metadata: {
name: "",
labels: {},
annotations: {
"plugin.halo.run/dependencies": "",
"plugin.halo.run/display-name": "",
}!,
},
},
selectedRoleTemplates: [],
saving: false,
});
const roleTemplates = computed<Role[]>(() => {
return roles.value.filter(
(role) => role.metadata.labels?.["plugin.halo.run/role-template"] === "true"
);
});
const roleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
const groups: RoleTemplateGroup[] = [];
roleTemplates.value.forEach((role) => {
const group = groups.find(
(group) =>
group.name === role.metadata.annotations?.["plugin.halo.run/module"]
);
if (group) {
group.roles.push(role);
} else {
groups.push({
name: role.metadata.annotations?.["plugin.halo.run/module"],
roles: [role],
});
}
});
return groups;
});
const handleFetchRoles = async () => {
try {
const { data } = await axiosInstance.get("/api/v1alpha1/roles");
roles.value = data;
} catch (e) {
console.error(e);
}
};
const handleCreateRole = async () => {
try {
creationFormState.value.saving = true;
if (creationFormState.value.role.metadata.annotations) {
creationFormState.value.role.metadata.annotations[
"plugin.halo.run/dependencies"
] = JSON.stringify(creationFormState.value.selectedRoleTemplates);
}
await axiosInstance.post<Role>(
"/api/v1alpha1/roles",
creationFormState.value.role
);
handleVisibleChange(false);
} catch (e) {
console.error(e);
} finally {
creationFormState.value.saving = false;
}
};
const handleVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
if (!visible) {
emit("close");
}
};
watch(
() => props.visible,
(visible) => {
if (visible) {
handleFetchRoles();
}
}
);
</script>
<template>
<VModal
:visible="visible"
:width="650"
title="创建角色"
@update:visible="handleVisibleChange"
>
<VTabs v-model:active-id="creationActiveId" type="outline">
<VTabItem id="general" label="基础信息">
<form>
<div class="space-y-6 divide-y-0 sm:divide-y sm:divide-gray-200">
<div
v-if="creationFormState.role.metadata.annotations"
class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-4 sm:pt-5"
>
<label
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
名称
</label>
<div class="mt-1 sm:col-span-2 sm:mt-0">
<VInput
v-model="
creationFormState.role.metadata.annotations[
'plugin.halo.run/display-name'
]
"
></VInput>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-4 sm:pt-5">
<label
class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
>
别名
</label>
<div class="mt-1 sm:col-span-2 sm:mt-0">
<VInput v-model="creationFormState.role.metadata.name"></VInput>
</div>
</div>
</div>
</form>
</VTabItem>
<VTabItem id="permissions" label="权限">
<div>
<dl class="divide-y divide-gray-100">
<div
v-for="(group, groupIndex) in roleTemplateGroups"
:key="groupIndex"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">
{{ group.name }}
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2">
<li v-for="(role, index) in group.roles" :key="index">
<div
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded border p-5 hover:border-themeable-primary"
>
<input
v-model="creationFormState.selectedRoleTemplates"
:value="role.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
type="checkbox"
/>
<div class="inline-flex flex-col gap-y-3">
<span class="font-medium text-gray-900">
{{
role.metadata.annotations?.[
"plugin.halo.run/display-name"
]
}}
</span>
<span
v-if="
role.metadata.annotations?.[
'plugin.halo.run/dependencies'
]
"
class="text-xs text-gray-400"
>
依赖于
{{
JSON.parse(
role.metadata.annotations?.[
"plugin.halo.run/dependencies"
]
).join(", ")
}}
</span>
</div>
</div>
</li>
</ul>
</dd>
</div>
</dl>
</div>
</VTabItem>
</VTabs>
<template #footer>
<VButton
:loading="creationFormState.saving"
type="secondary"
@click="handleCreateRole"
>创建
</VButton>
</template>
</VModal>
</template>