mirror of https://github.com/halo-dev/halo
feat: add multi-role assignment support for users (#7037)
#### What type of PR is this? /area ui /kind feature /milestone 2.20.x #### What this PR does / why we need it: 支持为用户分配多个角色。 <img width="634" alt="image" src="https://github.com/user-attachments/assets/caa40327-518a-4bef-afc3-75a020018d3d"> <img width="764" alt="image" src="https://github.com/user-attachments/assets/8b4b807e-6c72-45d9-9368-75e70bb12dcc"> TODO: - [x] Console / UC 侧边栏显示多个角色 - [x] 支持在管理端查看用户聚合的角色模板列表,或者在分配时显示所选角色其下的角色模板列表,能够让管理员清楚的知道用户具体权限。 #### Which issue(s) this PR fixes: Fixes # #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 支持为用户分配多个角色。 ```pull/7075/head
parent
964bc28052
commit
391aac62d3
|
@ -9,11 +9,13 @@ import { coreMenuGroups } from "@console/router/constant";
|
|||
import {
|
||||
Dialog,
|
||||
IconAccountCircleLine,
|
||||
IconArrowDownLine,
|
||||
IconLogoutCircleRLine,
|
||||
IconMore,
|
||||
IconSearch,
|
||||
IconUserSettings,
|
||||
IconShieldUser,
|
||||
VAvatar,
|
||||
VDropdown,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
|
@ -152,10 +154,10 @@ onMounted(() => {
|
|||
>
|
||||
{{ currentUser?.spec.displayName }}
|
||||
</div>
|
||||
<div v-if="currentRoles?.[0]" class="flex">
|
||||
<VTag>
|
||||
<div v-if="currentRoles?.length" class="flex mt-1">
|
||||
<VTag v-if="currentRoles.length === 1">
|
||||
<template #leftIcon>
|
||||
<IconUserSettings />
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{
|
||||
currentRoles[0].metadata.annotations?.[
|
||||
|
@ -163,6 +165,41 @@ onMounted(() => {
|
|||
] || currentRoles[0].metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
<VDropdown v-else>
|
||||
<div class="flex gap-1">
|
||||
<VTag>
|
||||
<template #leftIcon>
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{ $t("core.sidebar.profile.aggregate_role") }}
|
||||
</VTag>
|
||||
<IconArrowDownLine />
|
||||
</div>
|
||||
<template #popper>
|
||||
<div class="p-1">
|
||||
<h2
|
||||
class="text-gray-600 text-sm font-semibold border-b border-gray-100 pb-1.5"
|
||||
>
|
||||
{{ $t("core.sidebar.profile.aggregate_role") }}
|
||||
</h2>
|
||||
<div class="flex gap-2 flex-wrap mt-2">
|
||||
<VTag
|
||||
v-for="role in currentRoles"
|
||||
:key="role.metadata.name"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DISPLAY_NAME
|
||||
] || role.metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -299,6 +336,7 @@ onMounted(() => {
|
|||
|
||||
.profile-placeholder {
|
||||
height: 70px;
|
||||
flex: none;
|
||||
|
||||
.current-profile {
|
||||
height: 70px;
|
||||
|
|
|
@ -183,8 +183,8 @@ const handleUpdateRole = async () => {
|
|||
<div>
|
||||
<dl class="divide-y divide-gray-100">
|
||||
<div
|
||||
v-for="(group, groupIndex) in roleTemplateGroups"
|
||||
:key="groupIndex"
|
||||
v-for="(group, index) in roleTemplateGroups"
|
||||
:key="index"
|
||||
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">
|
||||
|
@ -224,7 +224,7 @@ const handleUpdateRole = async () => {
|
|||
</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">
|
||||
<li v-for="role in group.roles" :key="role.metadata.name">
|
||||
<label
|
||||
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary"
|
||||
>
|
||||
|
|
|
@ -101,7 +101,7 @@ const tabbarItems = computed(() => {
|
|||
}));
|
||||
});
|
||||
|
||||
const handleDelete = async (userToDelete: User) => {
|
||||
const handleDelete = async (user: User) => {
|
||||
Dialog.warning({
|
||||
title: t("core.user.operations.delete.title"),
|
||||
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
|
||||
|
@ -111,7 +111,7 @@ const handleDelete = async (userToDelete: User) => {
|
|||
onConfirm: async () => {
|
||||
try {
|
||||
await coreApiClient.user.deleteUser({
|
||||
name: userToDelete.metadata.name,
|
||||
name: user.metadata.name,
|
||||
});
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
router.push({ name: "Users" });
|
||||
|
@ -189,11 +189,17 @@ function onGrantPermissionModalClose() {
|
|||
<VDropdownItem @click="passwordChangeModal = true">
|
||||
{{ $t("core.user.detail.actions.change_password.title") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem @click="grantPermissionModal = true">
|
||||
<VDropdownItem
|
||||
v-if="currentUser?.metadata.name !== user?.user.metadata.name"
|
||||
@click="grantPermissionModal = true"
|
||||
>
|
||||
{{ $t("core.user.detail.actions.grant_permission.title") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem
|
||||
v-if="user?.user"
|
||||
v-if="
|
||||
user &&
|
||||
currentUser?.metadata.name !== user?.user.metadata.name
|
||||
"
|
||||
type="danger"
|
||||
@click="handleDelete(user.user)"
|
||||
>
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
IconAddCircle,
|
||||
IconLockPasswordLine,
|
||||
IconRefreshLine,
|
||||
IconUserFollow,
|
||||
IconShieldUser,
|
||||
IconUserSettings,
|
||||
Toast,
|
||||
VAvatar,
|
||||
|
@ -288,7 +288,7 @@ function onGrantPermissionModalClose() {
|
|||
type="default"
|
||||
>
|
||||
<template #icon>
|
||||
<IconUserFollow class="h-full w-full" />
|
||||
<IconShieldUser class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.user.actions.roles") }}
|
||||
</VButton>
|
||||
|
@ -469,19 +469,21 @@ function onGrantPermissionModalClose() {
|
|||
<template #end>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
<div
|
||||
v-for="(role, roleIndex) in user.roles"
|
||||
:key="roleIndex"
|
||||
class="flex items-center"
|
||||
>
|
||||
<VTag>
|
||||
<VSpace>
|
||||
<VTag
|
||||
v-for="role in user.roles"
|
||||
:key="role.metadata.name"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DISPLAY_NAME
|
||||
] || role.metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
</div>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="user.user.metadata.deletionTimestamp">
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
<script lang="ts" setup>
|
||||
import SubmitButton from "@/components/button/SubmitButton.vue";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { SUPER_ROLE_NAME } from "@/constants/constants";
|
||||
import { roleLabels } from "@/constants/labels";
|
||||
import type { User } from "@halo-dev/api-client";
|
||||
import { consoleApiClient } from "@halo-dev/api-client";
|
||||
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import { ref } from "vue";
|
||||
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
|
||||
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import { useMutation, useQuery } from "@tanstack/vue-query";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import RolesView from "./RolesView.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -19,51 +27,137 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const modal = ref<InstanceType<typeof VModal> | null>(null);
|
||||
const selectedRole = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const handleGrantPermission = async () => {
|
||||
try {
|
||||
isSubmitting.value = true;
|
||||
await consoleApiClient.user.grantPermission({
|
||||
const selectedRoleNames = ref<string[]>([]);
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.user) {
|
||||
return;
|
||||
}
|
||||
selectedRoleNames.value = JSON.parse(
|
||||
props.user.metadata.annotations?.[rbacAnnotations.ROLE_NAMES] || "[]"
|
||||
);
|
||||
});
|
||||
|
||||
const { mutate, isLoading } = useMutation({
|
||||
mutationKey: ["core:user:grant-permissions"],
|
||||
mutationFn: async ({ roles }: { roles: string[] }) => {
|
||||
return await consoleApiClient.user.grantPermission({
|
||||
name: props.user?.metadata.name as string,
|
||||
grantRequest: {
|
||||
roles: [selectedRole.value],
|
||||
roles: roles,
|
||||
},
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
Toast.success(t("core.common.toast.operation_success"));
|
||||
modal.value?.close();
|
||||
} catch (error) {
|
||||
console.error("Failed to grant permission to user", error);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: { roles: string[] }) {
|
||||
mutate({ roles: data.roles });
|
||||
}
|
||||
|
||||
const { data: allRoles } = useQuery({
|
||||
queryKey: ["core:roles"],
|
||||
queryFn: async () => {
|
||||
const { data } = await coreApiClient.role.listRole({
|
||||
page: 0,
|
||||
size: 0,
|
||||
labelSelector: [`!${roleLabels.TEMPLATE}`],
|
||||
});
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: allRoleTemplates } = useQuery({
|
||||
queryKey: ["core:role-templates"],
|
||||
queryFn: async () => {
|
||||
const { data } = await coreApiClient.role.listRole({
|
||||
page: 0,
|
||||
size: 0,
|
||||
labelSelector: [`${roleLabels.TEMPLATE}=true`, "!halo.run/hidden"],
|
||||
});
|
||||
return data.items;
|
||||
},
|
||||
});
|
||||
|
||||
const currentRoleTemplates = computed(() => {
|
||||
if (!selectedRoleNames.value.length) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const selectedRoles = allRoles.value?.items.filter((role) =>
|
||||
selectedRoleNames.value.includes(role.metadata.name)
|
||||
);
|
||||
|
||||
let allDependsRoleTemplates: string[] = [];
|
||||
|
||||
selectedRoles?.forEach((role) => {
|
||||
allDependsRoleTemplates = allDependsRoleTemplates.concat(
|
||||
JSON.parse(
|
||||
role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES] || "[]"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return allRoleTemplates.value?.filter((item) => {
|
||||
return allDependsRoleTemplates.includes(item.metadata.name);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VModal
|
||||
ref="modal"
|
||||
:title="$t('core.user.grant_permission_modal.title')"
|
||||
:width="500"
|
||||
:width="600"
|
||||
:centered="false"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<FormKit
|
||||
id="grant-permission-form"
|
||||
name="grant-permission-form"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
type="form"
|
||||
@submit="handleGrantPermission"
|
||||
>
|
||||
<div>
|
||||
<FormKit
|
||||
v-model="selectedRole"
|
||||
:label="$t('core.user.grant_permission_modal.fields.role.label')"
|
||||
type="roleSelect"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
id="grant-permission-form"
|
||||
name="grant-permission-form"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
type="form"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<!-- @vue-ignore -->
|
||||
<FormKit
|
||||
v-model="selectedRoleNames"
|
||||
multiple
|
||||
name="roles"
|
||||
:label="$t('core.user.grant_permission_modal.fields.role.label')"
|
||||
type="roleSelect"
|
||||
:placeholder="
|
||||
$t('core.user.grant_permission_modal.fields.role.placeholder')
|
||||
"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
|
||||
<div v-if="selectedRoleNames.length">
|
||||
<div
|
||||
v-if="selectedRoleNames.includes(SUPER_ROLE_NAME)"
|
||||
class="text-sm text-gray-600 mt-4"
|
||||
>
|
||||
{{ $t("core.user.grant_permission_modal.roles_preview.all") }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="currentRoleTemplates?.length" class="space-y-3 mt-4">
|
||||
<span class="text-sm text-gray-600">
|
||||
{{ $t("core.user.grant_permission_modal.roles_preview.includes") }}
|
||||
</span>
|
||||
<RolesView :role-templates="currentRoleTemplates" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<VSpace>
|
||||
<SubmitButton
|
||||
:loading="isSubmitting"
|
||||
:loading="isLoading"
|
||||
type="secondary"
|
||||
:text="$t('core.common.buttons.submit')"
|
||||
@submit="$formkit.submit('grant-permission-form')"
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
<script setup lang="ts">
|
||||
import { useRoleTemplateSelection } from "@/composables/use-role";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { pluginLabels } from "@/constants/labels";
|
||||
import type { Role } from "@halo-dev/api-client";
|
||||
import { toRefs } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
roleTemplates?: Role[];
|
||||
}>(),
|
||||
{
|
||||
roleTemplates: () => [],
|
||||
}
|
||||
);
|
||||
|
||||
const { roleTemplates } = toRefs(props);
|
||||
|
||||
const { roleTemplateGroups } = useRoleTemplateSelection(roleTemplates);
|
||||
</script>
|
||||
<template>
|
||||
<dl
|
||||
class="divide-y divide-gray-100 border border-gray-100 rounded-base overflow-hidden"
|
||||
>
|
||||
<div
|
||||
v-for="(group, index) in roleTemplateGroups"
|
||||
:key="index"
|
||||
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">
|
||||
<div>
|
||||
{{ $t(`core.rbac.${group.module}`, group.module as string) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
group.roles.length &&
|
||||
group.roles[0].metadata.labels?.[pluginLabels.NAME]
|
||||
"
|
||||
class="mt-3 text-xs text-gray-500"
|
||||
>
|
||||
<i18n-t keypath="core.role.common.text.provided_by_plugin" tag="div">
|
||||
<template #plugin>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'PluginDetail',
|
||||
params: {
|
||||
name: group.roles[0].metadata.labels?.[pluginLabels.NAME],
|
||||
},
|
||||
}"
|
||||
class="hover:text-blue-600"
|
||||
>
|
||||
{{ group.roles[0].metadata.labels?.[pluginLabels.NAME] }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</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 in group.roles" :key="role.metadata.name">
|
||||
<label
|
||||
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary"
|
||||
>
|
||||
<input type="checkbox" disabled checked />
|
||||
<div class="flex flex-1 flex-col gap-y-3">
|
||||
<span class="font-medium text-gray-900">
|
||||
{{
|
||||
$t(
|
||||
`core.rbac.${
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DISPLAY_NAME
|
||||
]
|
||||
}`,
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DISPLAY_NAME
|
||||
] as string
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES]
|
||||
"
|
||||
class="text-xs text-gray-400"
|
||||
>
|
||||
{{
|
||||
$t("core.role.common.text.dependent_on", {
|
||||
roles: JSON.parse(
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DEPENDENCIES
|
||||
]
|
||||
)
|
||||
.map((item: string) =>
|
||||
$t(`core.rbac.${item}`, item as string)
|
||||
)
|
||||
.join(","),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</template>
|
|
@ -4,9 +4,9 @@ import { formatDatetime } from "@/utils/date";
|
|||
import type { DetailedUser } from "@halo-dev/api-client";
|
||||
import {
|
||||
IconInformation,
|
||||
IconUserSettings,
|
||||
VDescription,
|
||||
VDescriptionItem,
|
||||
VSpace,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import RiVerifiedBadgeLine from "~icons/ri/verified-badge-line";
|
||||
|
@ -55,24 +55,23 @@ withDefaults(defineProps<{ user?: DetailedUser }>(), {
|
|||
:label="$t('core.user.detail.fields.roles')"
|
||||
class="!px-2"
|
||||
>
|
||||
<VTag
|
||||
v-for="(role, index) in user?.roles"
|
||||
:key="index"
|
||||
@click="
|
||||
$router.push({
|
||||
name: 'RoleDetail',
|
||||
params: { name: role.metadata.name },
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<IconUserSettings />
|
||||
</template>
|
||||
{{
|
||||
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
|
||||
role.metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
<VSpace>
|
||||
<VTag
|
||||
v-for="role in user?.roles"
|
||||
:key="role.metadata.name"
|
||||
@click="
|
||||
$router.push({
|
||||
name: 'RoleDetail',
|
||||
params: { name: role.metadata.name },
|
||||
})
|
||||
"
|
||||
>
|
||||
{{
|
||||
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
|
||||
role.metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
</VSpace>
|
||||
</VDescriptionItem>
|
||||
<VDescriptionItem
|
||||
:label="$t('core.user.detail.fields.bio')"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { SUPER_ROLE_NAME } from "@/constants/constants";
|
||||
import { useRoleStore } from "@/stores/role";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
|
@ -24,6 +25,10 @@ export function setupPermissionGuard(router: Router) {
|
|||
}
|
||||
|
||||
function isConsoleAccessDisallowed(currentRoles?: Role[]): boolean {
|
||||
if (currentRoles?.some((role) => role.metadata.name === SUPER_ROLE_NAME)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
currentRoles?.some(
|
||||
(role) =>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { roleLabels } from "@/constants/labels";
|
||||
import { i18n } from "@/locales";
|
||||
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
|
||||
import { coreApiClient } from "@halo-dev/api-client";
|
||||
import { select } from "./select";
|
||||
|
@ -14,12 +13,6 @@ function optionsHandler(node: FormKitNode) {
|
|||
});
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: i18n.global.t(
|
||||
"core.user.grant_permission_modal.fields.role.placeholder"
|
||||
),
|
||||
value: "",
|
||||
},
|
||||
...data.items.map((role) => {
|
||||
return {
|
||||
label:
|
||||
|
|
|
@ -33,6 +33,8 @@ core:
|
|||
tooltip: Profile
|
||||
visit_homepage:
|
||||
title: Visit homepage
|
||||
profile:
|
||||
aggregate_role: Aggregate Role
|
||||
uc_sidebar:
|
||||
menu:
|
||||
items:
|
||||
|
@ -42,6 +44,8 @@ core:
|
|||
operations:
|
||||
console:
|
||||
tooltip: Console
|
||||
profile:
|
||||
aggregate_role: Aggregate Role
|
||||
dashboard:
|
||||
title: Dashboard
|
||||
actions:
|
||||
|
@ -1059,6 +1063,9 @@ core:
|
|||
role:
|
||||
label: Role
|
||||
placeholder: Please select a role
|
||||
roles_preview:
|
||||
all: The currently selected role contains all permissions
|
||||
includes: "The currently selected role contains the following permissions:"
|
||||
detail:
|
||||
title: User detail
|
||||
tabs:
|
||||
|
|
|
@ -31,6 +31,8 @@ core:
|
|||
tooltip: 个人中心
|
||||
visit_homepage:
|
||||
title: 访问首页
|
||||
profile:
|
||||
aggregate_role: 聚合角色
|
||||
uc_sidebar:
|
||||
menu:
|
||||
items:
|
||||
|
@ -40,6 +42,8 @@ core:
|
|||
operations:
|
||||
console:
|
||||
tooltip: 管理控制台
|
||||
profile:
|
||||
aggregate_role: 聚合角色
|
||||
dashboard:
|
||||
title: 仪表板
|
||||
actions:
|
||||
|
@ -989,6 +993,9 @@ core:
|
|||
role:
|
||||
label: 角色
|
||||
placeholder: 请选择角色
|
||||
roles_preview:
|
||||
all: 当前所选角色包含所有权限
|
||||
includes: 当前所选角色包含的权限:
|
||||
detail:
|
||||
title: 用户详情
|
||||
tabs:
|
||||
|
|
|
@ -31,6 +31,8 @@ core:
|
|||
tooltip: 個人中心
|
||||
visit_homepage:
|
||||
title: 訪問首頁
|
||||
profile:
|
||||
aggregate_role: 聚合角色
|
||||
uc_sidebar:
|
||||
menu:
|
||||
items:
|
||||
|
@ -40,6 +42,8 @@ core:
|
|||
operations:
|
||||
console:
|
||||
tooltip: 管理控制台
|
||||
profile:
|
||||
aggregate_role: 聚合角色
|
||||
dashboard:
|
||||
title: 儀表板
|
||||
actions:
|
||||
|
@ -966,6 +970,9 @@ core:
|
|||
role:
|
||||
label: 角色
|
||||
placeholder: 請選擇角色
|
||||
roles_preview:
|
||||
all: 目前所選角色包含所有權限
|
||||
includes: 目前選定角色所包含的權限:
|
||||
detail:
|
||||
title: 用戶詳情
|
||||
tabs:
|
||||
|
|
|
@ -2,15 +2,18 @@
|
|||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { SUPER_ROLE_NAME } from "@/constants/constants";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { coreMenuGroups } from "@console/router/constant";
|
||||
import {
|
||||
Dialog,
|
||||
IconArrowDownLine,
|
||||
IconLogoutCircleRLine,
|
||||
IconMore,
|
||||
IconSettings3Line,
|
||||
IconUserSettings,
|
||||
IconShieldUser,
|
||||
VAvatar,
|
||||
VDropdown,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import {
|
||||
|
@ -86,6 +89,12 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
const disallowAccessConsole = computed(() => {
|
||||
if (
|
||||
currentRoles?.value?.some((role) => role.metadata.name === SUPER_ROLE_NAME)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasDisallowAccessConsoleRole = currentRoles?.value?.some((role) => {
|
||||
return (
|
||||
role.metadata.annotations?.[rbacAnnotations.DISALLOW_ACCESS_CONSOLE] ===
|
||||
|
@ -121,7 +130,7 @@ const disallowAccessConsole = computed(() => {
|
|||
<VAvatar
|
||||
:src="currentUser?.spec.avatar"
|
||||
:alt="currentUser?.spec.displayName"
|
||||
size="md"
|
||||
size="sm"
|
||||
circle
|
||||
></VAvatar>
|
||||
</div>
|
||||
|
@ -129,10 +138,10 @@ const disallowAccessConsole = computed(() => {
|
|||
<div class="flex text-sm font-medium">
|
||||
{{ currentUser?.spec.displayName }}
|
||||
</div>
|
||||
<div v-if="currentRoles?.[0]" class="flex">
|
||||
<VTag>
|
||||
<div v-if="currentRoles?.length" class="flex mt-1">
|
||||
<VTag v-if="currentRoles.length === 1">
|
||||
<template #leftIcon>
|
||||
<IconUserSettings />
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{
|
||||
currentRoles[0].metadata.annotations?.[
|
||||
|
@ -140,6 +149,41 @@ const disallowAccessConsole = computed(() => {
|
|||
] || currentRoles[0].metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
<VDropdown v-else>
|
||||
<div class="flex gap-1">
|
||||
<VTag>
|
||||
<template #leftIcon>
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{ $t("core.uc_sidebar.profile.aggregate_role") }}
|
||||
</VTag>
|
||||
<IconArrowDownLine />
|
||||
</div>
|
||||
<template #popper>
|
||||
<div class="p-1">
|
||||
<h2
|
||||
class="text-gray-600 text-sm font-semibold border-b border-gray-100 pb-1.5"
|
||||
>
|
||||
{{ $t("core.uc_sidebar.profile.aggregate_role") }}
|
||||
</h2>
|
||||
<div class="flex gap-2 flex-wrap mt-2">
|
||||
<VTag
|
||||
v-for="role in currentRoles"
|
||||
:key="role.metadata.name"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DISPLAY_NAME
|
||||
] || role.metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
|
@ -276,6 +320,7 @@ const disallowAccessConsole = computed(() => {
|
|||
|
||||
.profile-placeholder {
|
||||
height: 70px;
|
||||
flex: none;
|
||||
|
||||
.current-profile {
|
||||
height: 70px;
|
||||
|
|
|
@ -5,11 +5,12 @@ import type { DetailedUser, ListedAuthProvider } from "@halo-dev/api-client";
|
|||
import { consoleApiClient } from "@halo-dev/api-client";
|
||||
import {
|
||||
Dialog,
|
||||
IconUserSettings,
|
||||
IconShieldUser,
|
||||
VAlert,
|
||||
VButton,
|
||||
VDescription,
|
||||
VDescriptionItem,
|
||||
VSpace,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
|
@ -133,15 +134,17 @@ const emailVerifyModal = ref(false);
|
|||
:label="$t('core.uc_profile.detail.fields.roles')"
|
||||
class="!px-2"
|
||||
>
|
||||
<VTag v-for="role in user?.roles" :key="role.metadata.name">
|
||||
<template #leftIcon>
|
||||
<IconUserSettings />
|
||||
</template>
|
||||
{{
|
||||
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
|
||||
role.metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
<VSpace>
|
||||
<VTag v-for="role in user?.roles" :key="role.metadata.name">
|
||||
<template #leftIcon>
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{
|
||||
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
|
||||
role.metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
</VSpace>
|
||||
</VDescriptionItem>
|
||||
<VDescriptionItem
|
||||
:label="$t('core.uc_profile.detail.fields.bio')"
|
||||
|
|
Loading…
Reference in New Issue