mirror of https://github.com/halo-dev/halo
299 lines
9.8 KiB
Vue
299 lines
9.8 KiB
Vue
<script lang="ts" setup>
|
||
import { useRoleForm, useRoleTemplateSelection } from "@/composables/use-role";
|
||
import { rbacAnnotations } from "@/constants/annotations";
|
||
import { SUPER_ROLE_NAME } from "@/constants/constants";
|
||
import { pluginLabels, roleLabels } from "@/constants/labels";
|
||
import { formatDatetime } from "@/utils/date";
|
||
import { resolveDeepDependencies } from "@/utils/role";
|
||
import type { Role } from "@halo-dev/api-client";
|
||
import { coreApiClient } from "@halo-dev/api-client";
|
||
import {
|
||
IconShieldUser,
|
||
VAlert,
|
||
VButton,
|
||
VCard,
|
||
VDescription,
|
||
VDescriptionItem,
|
||
VPageHeader,
|
||
VTabbar,
|
||
VTag,
|
||
} from "@halo-dev/components";
|
||
import { useQuery } from "@tanstack/vue-query";
|
||
import { computed, ref, watch } from "vue";
|
||
import { useI18n } from "vue-i18n";
|
||
import { useRoute } from "vue-router";
|
||
|
||
const route = useRoute();
|
||
const { t } = useI18n();
|
||
|
||
const tabActiveId = ref("detail");
|
||
|
||
const { data: roleTemplates } = useQuery({
|
||
queryKey: ["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 { roleTemplateGroups, handleRoleTemplateSelect, selectedRoleTemplates } =
|
||
useRoleTemplateSelection(roleTemplates);
|
||
|
||
const { formState, isSubmitting, handleCreateOrUpdate } = useRoleForm();
|
||
|
||
const isSystemReserved = computed(() => {
|
||
return (
|
||
formState.value.metadata.labels?.[roleLabels.SYSTEM_RESERVED] === "true"
|
||
);
|
||
});
|
||
|
||
const isSuperRole = computed(() => {
|
||
return formState.value.metadata.name === SUPER_ROLE_NAME;
|
||
});
|
||
|
||
const getRoleCountText = computed(() => {
|
||
if (formState.value.metadata.name === SUPER_ROLE_NAME) {
|
||
return t("core.role.common.text.contains_all_permissions");
|
||
}
|
||
|
||
const dependencies = new Set<string>(
|
||
resolveDeepDependencies(formState.value, roleTemplates.value || [])
|
||
);
|
||
|
||
return t("core.role.common.text.contains_n_permissions", {
|
||
count: dependencies.size || 0,
|
||
});
|
||
});
|
||
|
||
watch(
|
||
() => selectedRoleTemplates.value,
|
||
(newValue) => {
|
||
if (formState.value.metadata.annotations) {
|
||
formState.value.metadata.annotations[rbacAnnotations.DEPENDENCIES] =
|
||
JSON.stringify(Array.from(newValue));
|
||
}
|
||
}
|
||
);
|
||
|
||
const { refetch } = useQuery<Role>({
|
||
queryKey: ["role", route.params.name],
|
||
queryFn: async () => {
|
||
const { data } = await coreApiClient.role.getRole({
|
||
name: route.params.name as string,
|
||
});
|
||
return data;
|
||
},
|
||
onSuccess(data) {
|
||
formState.value = data;
|
||
selectedRoleTemplates.value = new Set<string>(
|
||
resolveDeepDependencies(data, roleTemplates.value || [])
|
||
);
|
||
},
|
||
enabled: computed(() => !!roleTemplates.value),
|
||
});
|
||
|
||
const handleUpdateRole = async () => {
|
||
await handleCreateOrUpdate();
|
||
await refetch();
|
||
};
|
||
</script>
|
||
<template>
|
||
<VPageHeader :title="$t('core.role.detail.title')">
|
||
<template #icon>
|
||
<IconShieldUser class="mr-2 self-center" />
|
||
</template>
|
||
</VPageHeader>
|
||
<div class="m-0 md:m-4">
|
||
<VCard :body-class="['!p-0']">
|
||
<template #header>
|
||
<VTabbar
|
||
v-model:active-id="tabActiveId"
|
||
:items="[
|
||
{ id: 'detail', label: $t('core.role.detail.tabs.detail') },
|
||
{
|
||
id: 'permissions',
|
||
label: $t('core.role.detail.tabs.permissions'),
|
||
},
|
||
]"
|
||
class="w-full !rounded-none"
|
||
type="outline"
|
||
></VTabbar>
|
||
</template>
|
||
<div v-if="tabActiveId === 'detail'">
|
||
<div class="px-4 py-4 sm:px-6">
|
||
<h3 class="text-lg font-medium leading-6 text-gray-900">
|
||
{{ $t("core.role.detail.header.title") }}
|
||
</h3>
|
||
<p
|
||
class="mt-1 flex max-w-2xl items-center gap-2 text-sm text-gray-500"
|
||
>
|
||
<span>{{ getRoleCountText }}</span>
|
||
</p>
|
||
</div>
|
||
<div class="border-t border-gray-200">
|
||
<VDescription>
|
||
<VDescriptionItem
|
||
:label="$t('core.role.detail.fields.display_name')"
|
||
>
|
||
{{
|
||
formState.metadata?.annotations?.[
|
||
rbacAnnotations.DISPLAY_NAME
|
||
] || formState.metadata?.name
|
||
}}
|
||
</VDescriptionItem>
|
||
<VDescriptionItem
|
||
:label="$t('core.role.detail.fields.name')"
|
||
:content="formState.metadata?.name"
|
||
/>
|
||
<VDescriptionItem :label="$t('core.role.detail.fields.type')">
|
||
<VTag>
|
||
{{
|
||
isSystemReserved
|
||
? t("core.role.common.text.system_reserved")
|
||
: t("core.role.common.text.custom")
|
||
}}
|
||
</VTag>
|
||
</VDescriptionItem>
|
||
<VDescriptionItem
|
||
:label="$t('core.role.detail.fields.creation_time')"
|
||
:content="formatDatetime(formState.metadata.creationTimestamp)"
|
||
/>
|
||
</VDescription>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="tabActiveId === 'permissions'">
|
||
<div v-if="isSystemReserved" class="px-4 py-5">
|
||
<VAlert
|
||
:title="$t('core.common.text.tip')"
|
||
:description="
|
||
$t(
|
||
'core.role.permissions_detail.system_reserved_alert.description'
|
||
)
|
||
"
|
||
class="w-full sm:w-1/4"
|
||
:closable="false"
|
||
/>
|
||
</div>
|
||
|
||
<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">
|
||
<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, index) in group.roles" :key="index">
|
||
<label
|
||
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary"
|
||
>
|
||
<input
|
||
v-if="!isSuperRole"
|
||
v-model="selectedRoleTemplates"
|
||
:value="role.metadata.name"
|
||
type="checkbox"
|
||
:disabled="isSystemReserved"
|
||
@change="handleRoleTemplateSelect"
|
||
/>
|
||
<input v-else type="checkbox" checked disabled />
|
||
<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>
|
||
<div v-permission="['system:roles:manage']" class="p-4">
|
||
<VButton
|
||
:loading="isSubmitting"
|
||
type="secondary"
|
||
:disabled="isSystemReserved"
|
||
@click="handleUpdateRole"
|
||
>
|
||
{{ $t("core.common.buttons.save") }}
|
||
</VButton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</VCard>
|
||
</div>
|
||
</template>
|