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
Ryan Wang 2024-11-24 23:48:21 +08:00 committed by GitHub
parent 964bc28052
commit 391aac62d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 403 additions and 90 deletions

View File

@ -9,11 +9,13 @@ import { coreMenuGroups } from "@console/router/constant";
import { import {
Dialog, Dialog,
IconAccountCircleLine, IconAccountCircleLine,
IconArrowDownLine,
IconLogoutCircleRLine, IconLogoutCircleRLine,
IconMore, IconMore,
IconSearch, IconSearch,
IconUserSettings, IconShieldUser,
VAvatar, VAvatar,
VDropdown,
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { useEventListener } from "@vueuse/core"; import { useEventListener } from "@vueuse/core";
@ -152,10 +154,10 @@ onMounted(() => {
> >
{{ currentUser?.spec.displayName }} {{ currentUser?.spec.displayName }}
</div> </div>
<div v-if="currentRoles?.[0]" class="flex"> <div v-if="currentRoles?.length" class="flex mt-1">
<VTag> <VTag v-if="currentRoles.length === 1">
<template #leftIcon> <template #leftIcon>
<IconUserSettings /> <IconShieldUser />
</template> </template>
{{ {{
currentRoles[0].metadata.annotations?.[ currentRoles[0].metadata.annotations?.[
@ -163,6 +165,41 @@ onMounted(() => {
] || currentRoles[0].metadata.name ] || currentRoles[0].metadata.name
}} }}
</VTag> </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>
</div> </div>
@ -299,6 +336,7 @@ onMounted(() => {
.profile-placeholder { .profile-placeholder {
height: 70px; height: 70px;
flex: none;
.current-profile { .current-profile {
height: 70px; height: 70px;

View File

@ -183,8 +183,8 @@ const handleUpdateRole = async () => {
<div> <div>
<dl class="divide-y divide-gray-100"> <dl class="divide-y divide-gray-100">
<div <div
v-for="(group, groupIndex) in roleTemplateGroups" v-for="(group, index) in roleTemplateGroups"
:key="groupIndex" :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" 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">
@ -224,7 +224,7 @@ const handleUpdateRole = async () => {
</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 v-for="(role, index) in group.roles" :key="index"> <li v-for="role in group.roles" :key="role.metadata.name">
<label <label
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base 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"
> >

View File

@ -101,7 +101,7 @@ const tabbarItems = computed(() => {
})); }));
}); });
const handleDelete = async (userToDelete: User) => { const handleDelete = async (user: User) => {
Dialog.warning({ Dialog.warning({
title: t("core.user.operations.delete.title"), title: t("core.user.operations.delete.title"),
description: t("core.common.dialog.descriptions.cannot_be_recovered"), description: t("core.common.dialog.descriptions.cannot_be_recovered"),
@ -111,7 +111,7 @@ const handleDelete = async (userToDelete: User) => {
onConfirm: async () => { onConfirm: async () => {
try { try {
await coreApiClient.user.deleteUser({ await coreApiClient.user.deleteUser({
name: userToDelete.metadata.name, name: user.metadata.name,
}); });
Toast.success(t("core.common.toast.delete_success")); Toast.success(t("core.common.toast.delete_success"));
router.push({ name: "Users" }); router.push({ name: "Users" });
@ -189,11 +189,17 @@ function onGrantPermissionModalClose() {
<VDropdownItem @click="passwordChangeModal = true"> <VDropdownItem @click="passwordChangeModal = true">
{{ $t("core.user.detail.actions.change_password.title") }} {{ $t("core.user.detail.actions.change_password.title") }}
</VDropdownItem> </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") }} {{ $t("core.user.detail.actions.grant_permission.title") }}
</VDropdownItem> </VDropdownItem>
<VDropdownItem <VDropdownItem
v-if="user?.user" v-if="
user &&
currentUser?.metadata.name !== user?.user.metadata.name
"
type="danger" type="danger"
@click="handleDelete(user.user)" @click="handleDelete(user.user)"
> >

View File

@ -11,7 +11,7 @@ import {
IconAddCircle, IconAddCircle,
IconLockPasswordLine, IconLockPasswordLine,
IconRefreshLine, IconRefreshLine,
IconUserFollow, IconShieldUser,
IconUserSettings, IconUserSettings,
Toast, Toast,
VAvatar, VAvatar,
@ -288,7 +288,7 @@ function onGrantPermissionModalClose() {
type="default" type="default"
> >
<template #icon> <template #icon>
<IconUserFollow class="h-full w-full" /> <IconShieldUser class="h-full w-full" />
</template> </template>
{{ $t("core.user.actions.roles") }} {{ $t("core.user.actions.roles") }}
</VButton> </VButton>
@ -469,19 +469,21 @@ function onGrantPermissionModalClose() {
<template #end> <template #end>
<VEntityField> <VEntityField>
<template #description> <template #description>
<div <VSpace>
v-for="(role, roleIndex) in user.roles" <VTag
:key="roleIndex" v-for="role in user.roles"
class="flex items-center" :key="role.metadata.name"
> >
<VTag> <template #leftIcon>
<IconShieldUser />
</template>
{{ {{
role.metadata.annotations?.[ role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME rbacAnnotations.DISPLAY_NAME
] || role.metadata.name ] || role.metadata.name
}} }}
</VTag> </VTag>
</div> </VSpace>
</template> </template>
</VEntityField> </VEntityField>
<VEntityField v-if="user.user.metadata.deletionTimestamp"> <VEntityField v-if="user.user.metadata.deletionTimestamp">

View File

@ -1,9 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import SubmitButton from "@/components/button/SubmitButton.vue"; 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 type { User } from "@halo-dev/api-client";
import { consoleApiClient } from "@halo-dev/api-client"; import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
import { VButton, VModal, VSpace } from "@halo-dev/components"; import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
import { ref } from "vue"; 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( const props = withDefaults(
defineProps<{ defineProps<{
@ -19,51 +27,137 @@ const emit = defineEmits<{
}>(); }>();
const modal = ref<InstanceType<typeof VModal> | null>(null); const modal = ref<InstanceType<typeof VModal> | null>(null);
const selectedRole = ref("");
const isSubmitting = ref(false);
const handleGrantPermission = async () => { const selectedRoleNames = ref<string[]>([]);
try {
isSubmitting.value = true; onMounted(() => {
await consoleApiClient.user.grantPermission({ 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, name: props.user?.metadata.name as string,
grantRequest: { grantRequest: {
roles: [selectedRole.value], roles: roles,
}, },
}); });
},
onSuccess() {
Toast.success(t("core.common.toast.operation_success"));
modal.value?.close(); 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> </script>
<template> <template>
<VModal <VModal
ref="modal" ref="modal"
:title="$t('core.user.grant_permission_modal.title')" :title="$t('core.user.grant_permission_modal.title')"
:width="500" :width="600"
:centered="false"
@close="emit('close')" @close="emit('close')"
> >
<div>
<FormKit <FormKit
id="grant-permission-form" id="grant-permission-form"
name="grant-permission-form" name="grant-permission-form"
:config="{ validationVisibility: 'submit' }" :config="{ validationVisibility: 'submit' }"
type="form" type="form"
@submit="handleGrantPermission" @submit="onSubmit"
> >
<!-- @vue-ignore -->
<FormKit <FormKit
v-model="selectedRole" v-model="selectedRoleNames"
multiple
name="roles"
:label="$t('core.user.grant_permission_modal.fields.role.label')" :label="$t('core.user.grant_permission_modal.fields.role.label')"
type="roleSelect" type="roleSelect"
:placeholder="
$t('core.user.grant_permission_modal.fields.role.placeholder')
"
></FormKit> ></FormKit>
</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> <template #footer>
<VSpace> <VSpace>
<SubmitButton <SubmitButton
:loading="isSubmitting" :loading="isLoading"
type="secondary" type="secondary"
:text="$t('core.common.buttons.submit')" :text="$t('core.common.buttons.submit')"
@submit="$formkit.submit('grant-permission-form')" @submit="$formkit.submit('grant-permission-form')"

View File

@ -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>

View File

@ -4,9 +4,9 @@ import { formatDatetime } from "@/utils/date";
import type { DetailedUser } from "@halo-dev/api-client"; import type { DetailedUser } from "@halo-dev/api-client";
import { import {
IconInformation, IconInformation,
IconUserSettings,
VDescription, VDescription,
VDescriptionItem, VDescriptionItem,
VSpace,
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import RiVerifiedBadgeLine from "~icons/ri/verified-badge-line"; import RiVerifiedBadgeLine from "~icons/ri/verified-badge-line";
@ -55,9 +55,10 @@ withDefaults(defineProps<{ user?: DetailedUser }>(), {
:label="$t('core.user.detail.fields.roles')" :label="$t('core.user.detail.fields.roles')"
class="!px-2" class="!px-2"
> >
<VSpace>
<VTag <VTag
v-for="(role, index) in user?.roles" v-for="role in user?.roles"
:key="index" :key="role.metadata.name"
@click=" @click="
$router.push({ $router.push({
name: 'RoleDetail', name: 'RoleDetail',
@ -65,14 +66,12 @@ withDefaults(defineProps<{ user?: DetailedUser }>(), {
}) })
" "
> >
<template #leftIcon>
<IconUserSettings />
</template>
{{ {{
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] || role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
role.metadata.name role.metadata.name
}} }}
</VTag> </VTag>
</VSpace>
</VDescriptionItem> </VDescriptionItem>
<VDescriptionItem <VDescriptionItem
:label="$t('core.user.detail.fields.bio')" :label="$t('core.user.detail.fields.bio')"

View File

@ -1,4 +1,5 @@
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import { SUPER_ROLE_NAME } from "@/constants/constants";
import { useRoleStore } from "@/stores/role"; import { useRoleStore } from "@/stores/role";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { hasPermission } from "@/utils/permission"; import { hasPermission } from "@/utils/permission";
@ -24,6 +25,10 @@ export function setupPermissionGuard(router: Router) {
} }
function isConsoleAccessDisallowed(currentRoles?: Role[]): boolean { function isConsoleAccessDisallowed(currentRoles?: Role[]): boolean {
if (currentRoles?.some((role) => role.metadata.name === SUPER_ROLE_NAME)) {
return false;
}
return ( return (
currentRoles?.some( currentRoles?.some(
(role) => (role) =>

View File

@ -1,6 +1,5 @@
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import { roleLabels } from "@/constants/labels"; import { roleLabels } from "@/constants/labels";
import { i18n } from "@/locales";
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
import { coreApiClient } from "@halo-dev/api-client"; import { coreApiClient } from "@halo-dev/api-client";
import { select } from "./select"; import { select } from "./select";
@ -14,12 +13,6 @@ function optionsHandler(node: FormKitNode) {
}); });
const options = [ const options = [
{
label: i18n.global.t(
"core.user.grant_permission_modal.fields.role.placeholder"
),
value: "",
},
...data.items.map((role) => { ...data.items.map((role) => {
return { return {
label: label:

View File

@ -33,6 +33,8 @@ core:
tooltip: Profile tooltip: Profile
visit_homepage: visit_homepage:
title: Visit homepage title: Visit homepage
profile:
aggregate_role: Aggregate Role
uc_sidebar: uc_sidebar:
menu: menu:
items: items:
@ -42,6 +44,8 @@ core:
operations: operations:
console: console:
tooltip: Console tooltip: Console
profile:
aggregate_role: Aggregate Role
dashboard: dashboard:
title: Dashboard title: Dashboard
actions: actions:
@ -1059,6 +1063,9 @@ core:
role: role:
label: Role label: Role
placeholder: Please select a 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: detail:
title: User detail title: User detail
tabs: tabs:

View File

@ -31,6 +31,8 @@ core:
tooltip: 个人中心 tooltip: 个人中心
visit_homepage: visit_homepage:
title: 访问首页 title: 访问首页
profile:
aggregate_role: 聚合角色
uc_sidebar: uc_sidebar:
menu: menu:
items: items:
@ -40,6 +42,8 @@ core:
operations: operations:
console: console:
tooltip: 管理控制台 tooltip: 管理控制台
profile:
aggregate_role: 聚合角色
dashboard: dashboard:
title: 仪表板 title: 仪表板
actions: actions:
@ -989,6 +993,9 @@ core:
role: role:
label: 角色 label: 角色
placeholder: 请选择角色 placeholder: 请选择角色
roles_preview:
all: 当前所选角色包含所有权限
includes: 当前所选角色包含的权限:
detail: detail:
title: 用户详情 title: 用户详情
tabs: tabs:

View File

@ -31,6 +31,8 @@ core:
tooltip: 個人中心 tooltip: 個人中心
visit_homepage: visit_homepage:
title: 訪問首頁 title: 訪問首頁
profile:
aggregate_role: 聚合角色
uc_sidebar: uc_sidebar:
menu: menu:
items: items:
@ -40,6 +42,8 @@ core:
operations: operations:
console: console:
tooltip: 管理控制台 tooltip: 管理控制台
profile:
aggregate_role: 聚合角色
dashboard: dashboard:
title: 儀表板 title: 儀表板
actions: actions:
@ -966,6 +970,9 @@ core:
role: role:
label: 角色 label: 角色
placeholder: 請選擇角色 placeholder: 請選擇角色
roles_preview:
all: 目前所選角色包含所有權限
includes: 目前選定角色所包含的權限:
detail: detail:
title: 用戶詳情 title: 用戶詳情
tabs: tabs:

View File

@ -2,15 +2,18 @@
import { RoutesMenu } from "@/components/menu/RoutesMenu"; import { RoutesMenu } from "@/components/menu/RoutesMenu";
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator"; import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import { SUPER_ROLE_NAME } from "@/constants/constants";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { coreMenuGroups } from "@console/router/constant"; import { coreMenuGroups } from "@console/router/constant";
import { import {
Dialog, Dialog,
IconArrowDownLine,
IconLogoutCircleRLine, IconLogoutCircleRLine,
IconMore, IconMore,
IconSettings3Line, IconSettings3Line,
IconUserSettings, IconShieldUser,
VAvatar, VAvatar,
VDropdown,
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { import {
@ -86,6 +89,12 @@ onMounted(() => {
}); });
const disallowAccessConsole = computed(() => { const disallowAccessConsole = computed(() => {
if (
currentRoles?.value?.some((role) => role.metadata.name === SUPER_ROLE_NAME)
) {
return false;
}
const hasDisallowAccessConsoleRole = currentRoles?.value?.some((role) => { const hasDisallowAccessConsoleRole = currentRoles?.value?.some((role) => {
return ( return (
role.metadata.annotations?.[rbacAnnotations.DISALLOW_ACCESS_CONSOLE] === role.metadata.annotations?.[rbacAnnotations.DISALLOW_ACCESS_CONSOLE] ===
@ -121,7 +130,7 @@ const disallowAccessConsole = computed(() => {
<VAvatar <VAvatar
:src="currentUser?.spec.avatar" :src="currentUser?.spec.avatar"
:alt="currentUser?.spec.displayName" :alt="currentUser?.spec.displayName"
size="md" size="sm"
circle circle
></VAvatar> ></VAvatar>
</div> </div>
@ -129,10 +138,10 @@ const disallowAccessConsole = computed(() => {
<div class="flex text-sm font-medium"> <div class="flex text-sm font-medium">
{{ currentUser?.spec.displayName }} {{ currentUser?.spec.displayName }}
</div> </div>
<div v-if="currentRoles?.[0]" class="flex"> <div v-if="currentRoles?.length" class="flex mt-1">
<VTag> <VTag v-if="currentRoles.length === 1">
<template #leftIcon> <template #leftIcon>
<IconUserSettings /> <IconShieldUser />
</template> </template>
{{ {{
currentRoles[0].metadata.annotations?.[ currentRoles[0].metadata.annotations?.[
@ -140,6 +149,41 @@ const disallowAccessConsole = computed(() => {
] || currentRoles[0].metadata.name ] || currentRoles[0].metadata.name
}} }}
</VTag> </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> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
@ -276,6 +320,7 @@ const disallowAccessConsole = computed(() => {
.profile-placeholder { .profile-placeholder {
height: 70px; height: 70px;
flex: none;
.current-profile { .current-profile {
height: 70px; height: 70px;

View File

@ -5,11 +5,12 @@ import type { DetailedUser, ListedAuthProvider } from "@halo-dev/api-client";
import { consoleApiClient } from "@halo-dev/api-client"; import { consoleApiClient } from "@halo-dev/api-client";
import { import {
Dialog, Dialog,
IconUserSettings, IconShieldUser,
VAlert, VAlert,
VButton, VButton,
VDescription, VDescription,
VDescriptionItem, VDescriptionItem,
VSpace,
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
@ -133,15 +134,17 @@ const emailVerifyModal = ref(false);
:label="$t('core.uc_profile.detail.fields.roles')" :label="$t('core.uc_profile.detail.fields.roles')"
class="!px-2" class="!px-2"
> >
<VSpace>
<VTag v-for="role in user?.roles" :key="role.metadata.name"> <VTag v-for="role in user?.roles" :key="role.metadata.name">
<template #leftIcon> <template #leftIcon>
<IconUserSettings /> <IconShieldUser />
</template> </template>
{{ {{
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] || role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
role.metadata.name role.metadata.name
}} }}
</VTag> </VTag>
</VSpace>
</VDescriptionItem> </VDescriptionItem>
<VDescriptionItem <VDescriptionItem
:label="$t('core.uc_profile.detail.fields.bio')" :label="$t('core.uc_profile.detail.fields.bio')"