refactor: request api of user data (#882)

#### What type of PR is this?

/kind improvement

#### What this PR does / why we need it:

重构获取用户信息的请求方式,无需再请求所有角色即可获取当前用户的角色信息,适配:https://github.com/halo-dev/halo/pull/3372

相关 PR:https://github.com/halo-dev/console/pull/847

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/3342

#### Special notes for your reviewer:

测试方式:

1. Halo 需要切换到 https://github.com/halo-dev/halo/pull/3372 分支。
2. Console 需要 `pnpm install && pnpm build:packages`
3. 测试用户列表、登录、检查角色信息是否显示正确。

#### Does this PR introduce a user-facing change?

```release-note
优化 Console 端用户角色标识的显示名称。
```
pull/883/head^2
Ryan Wang 2023-02-24 14:06:14 +08:00 committed by GitHub
parent 66a626c916
commit 6244e8b5c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 370 additions and 152 deletions

View File

@ -77,6 +77,7 @@ models/counter-request.ts
models/counter.ts
models/custom-templates.ts
models/dashboard-stats.ts
models/detailed-user.ts
models/excerpt.ts
models/extension.ts
models/file-reverse-proxy-provider.ts
@ -96,6 +97,8 @@ models/listed-reply-list.ts
models/listed-reply.ts
models/listed-single-page-list.ts
models/listed-single-page.ts
models/listed-user-list.ts
models/listed-user.ts
models/login-history.ts
models/menu-item-list.ts
models/menu-item-spec.ts

View File

@ -34,11 +34,13 @@ import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } fr
// @ts-ignore
import { ChangePasswordRequest } from '../models'
// @ts-ignore
import { DetailedUser } from '../models'
// @ts-ignore
import { GrantRequest } from '../models'
// @ts-ignore
import { User } from '../models'
import { ListedUserList } from '../models'
// @ts-ignore
import { UserList } from '../models'
import { User } from '../models'
// @ts-ignore
import { UserPermission } from '../models'
/**
@ -174,6 +176,47 @@ export const ApiConsoleHaloRunV1alpha1UserApiAxiosParamCreator = function (confi
options: localVarRequestOptions,
}
},
/**
* Get user detail by name
* @param {string} name User name
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getUserDetail: async (name: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'name' is not null or undefined
assertParamExists('getUserDetail', 'name', name)
const localVarPath = `/apis/api.console.halo.run/v1alpha1/users/{name}`.replace(
`{${'name'}}`,
encodeURIComponent(String(name)),
)
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
let baseOptions
if (configuration) {
baseOptions = configuration.baseOptions
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options }
const localVarHeaderParameter = {} as any
const localVarQueryParameter = {} as any
// authentication BasicAuth required
// http basic authentication required
setBasicAuthToObject(localVarRequestOptions, configuration)
// authentication BearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter)
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}
localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
}
},
/**
* Grant permissions to user
* @param {string} name User name
@ -231,9 +274,9 @@ export const ApiConsoleHaloRunV1alpha1UserApiAxiosParamCreator = function (confi
* @param {string} [keyword]
* @param {string} [role]
* @param {number} [size] Size of one page. Zero indicates no limit.
* @param {number} [page] The page number. Zero indicates no page.
* @param {Array<string>} [labelSelector] Label selector for filtering.
* @param {Array<string>} [fieldSelector] Field selector for filtering.
* @param {number} [page] The page number. Zero indicates no page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -242,9 +285,9 @@ export const ApiConsoleHaloRunV1alpha1UserApiAxiosParamCreator = function (confi
keyword?: string,
role?: string,
size?: number,
page?: number,
labelSelector?: Array<string>,
fieldSelector?: Array<string>,
page?: number,
options: AxiosRequestConfig = {},
): Promise<RequestArgs> => {
const localVarPath = `/apis/api.console.halo.run/v1alpha1/users`
@ -283,10 +326,6 @@ export const ApiConsoleHaloRunV1alpha1UserApiAxiosParamCreator = function (confi
localVarQueryParameter['size'] = size
}
if (page !== undefined) {
localVarQueryParameter['page'] = page
}
if (labelSelector) {
localVarQueryParameter['labelSelector'] = labelSelector
}
@ -295,6 +334,10 @@ export const ApiConsoleHaloRunV1alpha1UserApiAxiosParamCreator = function (confi
localVarQueryParameter['fieldSelector'] = fieldSelector
}
if (page !== undefined) {
localVarQueryParameter['page'] = page
}
setSearchParams(localVarUrlObj, localVarQueryParameter)
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}
localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }
@ -377,7 +420,7 @@ export const ApiConsoleHaloRunV1alpha1UserApiFp = function (configuration?: Conf
*/
async getCurrentUserDetail(
options?: AxiosRequestConfig,
): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DetailedUser>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getCurrentUserDetail(options)
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)
},
@ -394,6 +437,19 @@ export const ApiConsoleHaloRunV1alpha1UserApiFp = function (configuration?: Conf
const localVarAxiosArgs = await localVarAxiosParamCreator.getPermissions(name, options)
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)
},
/**
* Get user detail by name
* @param {string} name User name
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getUserDetail(
name: string,
options?: AxiosRequestConfig,
): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DetailedUser>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserDetail(name, options)
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)
},
/**
* Grant permissions to user
* @param {string} name User name
@ -415,9 +471,9 @@ export const ApiConsoleHaloRunV1alpha1UserApiFp = function (configuration?: Conf
* @param {string} [keyword]
* @param {string} [role]
* @param {number} [size] Size of one page. Zero indicates no limit.
* @param {number} [page] The page number. Zero indicates no page.
* @param {Array<string>} [labelSelector] Label selector for filtering.
* @param {Array<string>} [fieldSelector] Field selector for filtering.
* @param {number} [page] The page number. Zero indicates no page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -426,19 +482,19 @@ export const ApiConsoleHaloRunV1alpha1UserApiFp = function (configuration?: Conf
keyword?: string,
role?: string,
size?: number,
page?: number,
labelSelector?: Array<string>,
fieldSelector?: Array<string>,
page?: number,
options?: AxiosRequestConfig,
): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserList>> {
): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedUserList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listUsers(
sort,
keyword,
role,
size,
page,
labelSelector,
fieldSelector,
page,
options,
)
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)
@ -489,7 +545,7 @@ export const ApiConsoleHaloRunV1alpha1UserApiFactory = function (
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getCurrentUserDetail(options?: AxiosRequestConfig): AxiosPromise<User> {
getCurrentUserDetail(options?: AxiosRequestConfig): AxiosPromise<DetailedUser> {
return localVarFp.getCurrentUserDetail(options).then((request) => request(axios, basePath))
},
/**
@ -504,6 +560,18 @@ export const ApiConsoleHaloRunV1alpha1UserApiFactory = function (
): AxiosPromise<UserPermission> {
return localVarFp.getPermissions(requestParameters.name, options).then((request) => request(axios, basePath))
},
/**
* Get user detail by name
* @param {ApiConsoleHaloRunV1alpha1UserApiGetUserDetailRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getUserDetail(
requestParameters: ApiConsoleHaloRunV1alpha1UserApiGetUserDetailRequest,
options?: AxiosRequestConfig,
): AxiosPromise<DetailedUser> {
return localVarFp.getUserDetail(requestParameters.name, options).then((request) => request(axios, basePath))
},
/**
* Grant permissions to user
* @param {ApiConsoleHaloRunV1alpha1UserApiGrantPermissionRequest} requestParameters Request parameters.
@ -527,16 +595,16 @@ export const ApiConsoleHaloRunV1alpha1UserApiFactory = function (
listUsers(
requestParameters: ApiConsoleHaloRunV1alpha1UserApiListUsersRequest = {},
options?: AxiosRequestConfig,
): AxiosPromise<UserList> {
): AxiosPromise<ListedUserList> {
return localVarFp
.listUsers(
requestParameters.sort,
requestParameters.keyword,
requestParameters.role,
requestParameters.size,
requestParameters.page,
requestParameters.labelSelector,
requestParameters.fieldSelector,
requestParameters.page,
options,
)
.then((request) => request(axios, basePath))
@ -591,6 +659,20 @@ export interface ApiConsoleHaloRunV1alpha1UserApiGetPermissionsRequest {
readonly name: string
}
/**
* Request parameters for getUserDetail operation in ApiConsoleHaloRunV1alpha1UserApi.
* @export
* @interface ApiConsoleHaloRunV1alpha1UserApiGetUserDetailRequest
*/
export interface ApiConsoleHaloRunV1alpha1UserApiGetUserDetailRequest {
/**
* User name
* @type {string}
* @memberof ApiConsoleHaloRunV1alpha1UserApiGetUserDetail
*/
readonly name: string
}
/**
* Request parameters for grantPermission operation in ApiConsoleHaloRunV1alpha1UserApi.
* @export
@ -646,13 +728,6 @@ export interface ApiConsoleHaloRunV1alpha1UserApiListUsersRequest {
*/
readonly size?: number
/**
* The page number. Zero indicates no page.
* @type {number}
* @memberof ApiConsoleHaloRunV1alpha1UserApiListUsers
*/
readonly page?: number
/**
* Label selector for filtering.
* @type {Array<string>}
@ -666,6 +741,13 @@ export interface ApiConsoleHaloRunV1alpha1UserApiListUsersRequest {
* @memberof ApiConsoleHaloRunV1alpha1UserApiListUsers
*/
readonly fieldSelector?: Array<string>
/**
* The page number. Zero indicates no page.
* @type {number}
* @memberof ApiConsoleHaloRunV1alpha1UserApiListUsers
*/
readonly page?: number
}
/**
@ -733,6 +815,22 @@ export class ApiConsoleHaloRunV1alpha1UserApi extends BaseAPI {
.then((request) => request(this.axios, this.basePath))
}
/**
* Get user detail by name
* @param {ApiConsoleHaloRunV1alpha1UserApiGetUserDetailRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiConsoleHaloRunV1alpha1UserApi
*/
public getUserDetail(
requestParameters: ApiConsoleHaloRunV1alpha1UserApiGetUserDetailRequest,
options?: AxiosRequestConfig,
) {
return ApiConsoleHaloRunV1alpha1UserApiFp(this.configuration)
.getUserDetail(requestParameters.name, options)
.then((request) => request(this.axios, this.basePath))
}
/**
* Grant permissions to user
* @param {ApiConsoleHaloRunV1alpha1UserApiGrantPermissionRequest} requestParameters Request parameters.
@ -766,9 +864,9 @@ export class ApiConsoleHaloRunV1alpha1UserApi extends BaseAPI {
requestParameters.keyword,
requestParameters.role,
requestParameters.size,
requestParameters.page,
requestParameters.labelSelector,
requestParameters.fieldSelector,
requestParameters.page,
options,
)
.then((request) => request(this.axios, this.basePath))

View File

@ -0,0 +1,40 @@
/* tslint:disable */
/* eslint-disable */
/**
* Halo Next API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 2.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import { Role } from './role'
// May contain unused imports in some cases
// @ts-ignore
import { User } from './user'
/**
*
* @export
* @interface DetailedUser
*/
export interface DetailedUser {
/**
*
* @type {User}
* @memberof DetailedUser
*/
user: User
/**
*
* @type {Array<Role>}
* @memberof DetailedUser
*/
roles: Array<Role>
}

View File

@ -31,6 +31,7 @@ export * from './counter-list'
export * from './counter-request'
export * from './custom-templates'
export * from './dashboard-stats'
export * from './detailed-user'
export * from './excerpt'
export * from './extension'
export * from './file-reverse-proxy-provider'
@ -49,6 +50,8 @@ export * from './listed-reply'
export * from './listed-reply-list'
export * from './listed-single-page'
export * from './listed-single-page-list'
export * from './listed-user'
export * from './listed-user-list'
export * from './login-history'
export * from './menu'
export * from './menu-item'

View File

@ -0,0 +1,79 @@
/* tslint:disable */
/* eslint-disable */
/**
* Halo Next API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 2.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import { ListedUser } from './listed-user'
/**
*
* @export
* @interface ListedUserList
*/
export interface ListedUserList {
/**
* Page number, starts from 1. If not set or equal to 0, it means no pagination.
* @type {number}
* @memberof ListedUserList
*/
page: number
/**
* Size of each page. If not set or equal to 0, it means no pagination.
* @type {number}
* @memberof ListedUserList
*/
size: number
/**
* Total elements.
* @type {number}
* @memberof ListedUserList
*/
total: number
/**
* A chunk of items.
* @type {Array<ListedUser>}
* @memberof ListedUserList
*/
items: Array<ListedUser>
/**
* Indicates whether current page is the first page.
* @type {boolean}
* @memberof ListedUserList
*/
first: boolean
/**
* Indicates whether current page is the last page.
* @type {boolean}
* @memberof ListedUserList
*/
last: boolean
/**
* Indicates whether current page has previous page.
* @type {boolean}
* @memberof ListedUserList
*/
hasNext: boolean
/**
* Indicates whether current page has previous page.
* @type {boolean}
* @memberof ListedUserList
*/
hasPrevious: boolean
/**
* Indicates total pages.
* @type {number}
* @memberof ListedUserList
*/
totalPages: number
}

View File

@ -0,0 +1,40 @@
/* tslint:disable */
/* eslint-disable */
/**
* Halo Next API
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 2.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
// May contain unused imports in some cases
// @ts-ignore
import { Role } from './role'
// May contain unused imports in some cases
// @ts-ignore
import { User } from './user'
/**
* A chunk of items.
* @export
* @interface ListedUser
*/
export interface ListedUser {
/**
*
* @type {User}
* @memberof ListedUser
*/
user: User
/**
*
* @type {Array<Role>}
* @memberof ListedUser
*/
roles: Array<Role>
}

View File

@ -18,7 +18,7 @@ import {
useRouter,
type RouteRecordRaw,
} from "vue-router";
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import axios from "axios";
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
import LoginModal from "@/components/login/LoginModal.vue";
@ -29,7 +29,7 @@ import { hasPermission } from "@/utils/permission";
import { useUserStore } from "@/stores/user";
import { rbacAnnotations } from "@/constants/annotations";
import { useScroll } from "@vueuse/core";
import { defineStore } from "pinia";
import { defineStore, storeToRefs } from "pinia";
const route = useRoute();
const router = useRouter();
@ -39,6 +39,8 @@ const moreMenuRootVisible = ref(false);
const userStore = useUserStore();
const { currentRoles, currentUser } = storeToRefs(userStore);
const handleLogout = () => {
Dialog.warning({
title: "确定要退出登录吗?",
@ -58,19 +60,6 @@ const handleLogout = () => {
});
};
const currentRole = computed(() => {
const names = JSON.parse(
userStore.currentUser?.metadata.annotations?.[rbacAnnotations.ROLE_NAMES] ||
"[]"
);
if (names.length === 0) {
return;
}
return roleStore.getRoleDisplayName(names[0]);
});
// Global Search
const globalSearchVisible = ref(false);
@ -249,24 +238,28 @@ onMounted(() => {
</div>
<div class="profile-placeholder">
<div class="current-profile">
<div v-if="userStore.currentUser?.spec.avatar" class="profile-avatar">
<div v-if="currentUser?.spec.avatar" class="profile-avatar">
<VAvatar
:src="userStore.currentUser?.spec.avatar"
:alt="userStore.currentUser?.spec.displayName"
:src="currentUser?.spec.avatar"
:alt="currentUser?.spec.displayName"
size="md"
circle
></VAvatar>
</div>
<div class="profile-name">
<div class="flex text-sm font-medium">
{{ userStore.currentUser?.spec.displayName }}
{{ currentUser?.spec.displayName }}
</div>
<div v-if="currentRole" class="flex">
<div v-if="currentRoles?.[0]" class="flex">
<VTag>
<template #leftIcon>
<IconUserSettings />
</template>
{{ currentRole }}
{{
currentRoles[0].metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
] || currentRoles[0].metadata.name
}}
</VTag>
</div>
</div>

View File

@ -244,9 +244,6 @@ async function initApp() {
await loadUserPermissions();
const roleStore = useRoleStore();
await roleStore.fetchRoles();
try {
await loadPluginModules();
} catch (e) {

View File

@ -36,9 +36,7 @@ import Fuse from "fuse.js";
import { usePermission } from "@/utils/permission";
import { roleLabels } from "@/constants/labels";
import { SUPER_ROLE_NAME } from "@/constants/constants";
import { useRoleStore } from "@/stores/role";
const roleStore = useRoleStore();
const { currentUserHasPermission } = usePermission();
const editingModal = ref<boolean>(false);
@ -93,7 +91,6 @@ const handleOpenEditingModal = (role: Role) => {
const onEditingModalClose = () => {
selectedRole.value = undefined;
handleFetchRoles();
roleStore.fetchRoles();
};
const handleCloneRole = async (role: Role) => {
@ -143,7 +140,6 @@ const handleDelete = async (role: Role) => {
console.error("Failed to delete role", e);
} finally {
handleFetchRoles();
roleStore.fetchRoles();
}
},
});

View File

@ -1,26 +1,13 @@
<script lang="ts" setup>
import { IconUserSettings, VTag } from "@halo-dev/components";
import type { Ref } from "vue";
import { computed, inject } from "vue";
import { inject } from "vue";
import { useRouter } from "vue-router";
import type { User } from "@halo-dev/api-client";
import type { DetailedUser } from "@halo-dev/api-client";
import { rbacAnnotations } from "@/constants/annotations";
import { formatDatetime } from "@/utils/date";
import { useRoleStore } from "@/stores/role";
const user = inject<Ref<User>>("user");
const roleStore = useRoleStore();
const roles = computed(() => {
const names = JSON.parse(
user?.value?.metadata?.annotations?.[rbacAnnotations.ROLE_NAMES] || "[]"
);
return names.map((name: string) => {
return roleStore.getRoleDisplayName(name);
});
});
const user = inject<Ref<DetailedUser | undefined>>("user");
const router = useRouter();
</script>
@ -32,7 +19,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">显示名称</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user?.spec?.displayName }}
{{ user?.user.spec?.displayName }}
</dd>
</div>
<div
@ -40,7 +27,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">用户名</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user?.metadata?.name }}
{{ user?.user.metadata?.name }}
</dd>
</div>
<div
@ -48,7 +35,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">电子邮箱</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user?.spec?.email || "未设置" }}
{{ user?.user.spec?.email || "未设置" }}
</dd>
</div>
<div
@ -57,14 +44,22 @@ const router = useRouter();
<dt class="text-sm font-medium text-gray-900">角色</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
<VTag
v-for="(role, index) in roles"
v-for="(role, index) in user?.roles"
:key="index"
@click="router.push({ name: 'RoleDetail', params: { name: role } })"
@click="
router.push({
name: 'RoleDetail',
params: { name: role.metadata.name },
})
"
>
<template #leftIcon>
<IconUserSettings />
</template>
{{ role }}
{{
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
role.metadata.name
}}
</VTag>
</dd>
</div>
@ -73,7 +68,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">描述</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user?.spec?.bio || "无" }}
{{ user?.user.spec?.bio || "无" }}
</dd>
</div>
<div
@ -83,7 +78,7 @@ const router = useRouter();
<dd
class="mt-1 text-sm tabular-nums text-gray-900 sm:col-span-3 sm:mt-0"
>
{{ formatDatetime(user?.metadata?.creationTimestamp) }}
{{ formatDatetime(user?.user.metadata?.creationTimestamp) }}
</dd>
</div>
<!-- TODO: add display last login time support -->
@ -93,7 +88,7 @@ const router = useRouter();
>
<dt class="text-sm font-medium text-gray-900">最近登录时间</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
{{ user?.metadata?.creationTimestamp }}
{{ user?.user.metadata?.creationTimestamp }}
</dd>
</div>
</dl>

View File

@ -25,13 +25,12 @@ import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
import GrantPermissionModal from "./components/GrantPermissionModal.vue";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
import type { Role, User, UserList } from "@halo-dev/api-client";
import type { Role, User, ListedUserList } from "@halo-dev/api-client";
import { rbacAnnotations } from "@/constants/annotations";
import { formatDatetime } from "@/utils/date";
import { useRouteQuery } from "@vueuse/router";
import { usePermission } from "@/utils/permission";
import { useUserStore } from "@/stores/user";
import { useRoleStore } from "@/stores/role";
import { getNode } from "@formkit/core";
import FilterTag from "@/components/filter/FilterTag.vue";
import { useFetchRole } from "../roles/composables/use-role";
@ -44,7 +43,7 @@ const editingModal = ref<boolean>(false);
const passwordChangeModal = ref<boolean>(false);
const grantPermissionModal = ref<boolean>(false);
const users = ref<UserList>({
const users = ref<ListedUserList>({
page: 1,
size: 20,
total: 0,
@ -62,7 +61,6 @@ const keyword = ref("");
const refreshInterval = ref();
const userStore = useUserStore();
const roleStore = useRoleStore();
const ANONYMOUSUSER_NAME = "anonymousUser";
const DELETEDUSER_NAME = "ghost";
@ -99,7 +97,7 @@ const handleFetchUsers = async (options?: {
users.value = data;
const deletedUsers = users.value.items.filter(
(user) => !!user.metadata.deletionTimestamp
(user) => !!user.user.metadata.deletionTimestamp
);
if (deletedUsers.length) {
@ -188,7 +186,7 @@ const handleCheckAllChange = (e: Event) => {
if (checked) {
selectedUserNames.value =
users.value.items.map((user) => {
return user.metadata.name;
return user.user.metadata.name;
}) || [];
} else {
selectedUserNames.value.length = 0;
@ -215,16 +213,6 @@ const handleOpenGrantPermissionModal = (user: User) => {
grantPermissionModal.value = true;
};
const getRoles = (user: User) => {
const names = JSON.parse(
user.metadata.annotations?.[rbacAnnotations.ROLE_NAMES] || "[]"
);
return names.map((name: string) => {
return roleStore.getRoleDisplayName(name);
});
};
onMounted(() => {
handleFetchUsers();
});
@ -521,14 +509,14 @@ const hasFilters = computed(() => {
role="list"
>
<li v-for="(user, index) in users.items" :key="index">
<VEntity :is-selected="checkSelection(user)">
<VEntity :is-selected="checkSelection(user.user)">
<template
v-if="currentUserHasPermission(['system:users:manage'])"
#checkbox
>
<input
v-model="selectedUserNames"
:value="user.metadata.name"
:value="user.user.metadata.name"
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
name="post-checkbox"
type="checkbox"
@ -538,18 +526,18 @@ const hasFilters = computed(() => {
<VEntityField>
<template #description>
<VAvatar
:alt="user.spec.displayName"
:src="user.spec.avatar"
:alt="user.user.spec.displayName"
:src="user.user.spec.avatar"
size="md"
></VAvatar>
</template>
</VEntityField>
<VEntityField
:title="user.spec.displayName"
:description="user.metadata.name"
:title="user.user.spec.displayName"
:description="user.user.metadata.name"
:route="{
name: 'UserDetail',
params: { name: user.metadata.name },
params: { name: user.user.metadata.name },
}"
/>
</template>
@ -557,17 +545,21 @@ const hasFilters = computed(() => {
<VEntityField>
<template #description>
<div
v-for="(role, roleIndex) in getRoles(user)"
v-for="(role, roleIndex) in user.roles"
:key="roleIndex"
class="flex items-center"
>
<VTag>
{{ role }}
{{
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
] || role.metadata.name
}}
</VTag>
</div>
</template>
</VEntityField>
<VEntityField v-if="user.metadata.deletionTimestamp">
<VEntityField v-if="user.user.metadata.deletionTimestamp">
<template #description>
<VStatusDot v-tooltip="``" state="warning" animate />
</template>
@ -575,7 +567,7 @@ const hasFilters = computed(() => {
<VEntityField>
<template #description>
<span class="truncate text-xs tabular-nums text-gray-500">
{{ formatDatetime(user.metadata.creationTimestamp) }}
{{ formatDatetime(user.user.metadata.creationTimestamp) }}
</span>
</template>
</VEntityField>
@ -588,35 +580,37 @@ const hasFilters = computed(() => {
v-close-popper
block
type="secondary"
@click="handleOpenCreateModal(user)"
@click="handleOpenCreateModal(user.user)"
>
修改资料
</VButton>
<VButton
v-close-popper
block
@click="handleOpenPasswordChangeModal(user)"
@click="handleOpenPasswordChangeModal(user.user)"
>
修改密码
</VButton>
<VButton
v-if="
userStore.currentUser?.metadata.name !== user.metadata.name
userStore.currentUser?.metadata.name !==
user.user.metadata.name
"
v-close-popper
block
@click="handleOpenGrantPermissionModal(user)"
@click="handleOpenGrantPermissionModal(user.user)"
>
分配角色
</VButton>
<VButton
v-if="
userStore.currentUser?.metadata.name !== user.metadata.name
userStore.currentUser?.metadata.name !==
user.user.metadata.name
"
v-close-popper
block
type="danger"
@click="handleDelete(user)"
@click="handleDelete(user.user)"
>
删除
</VButton>

View File

@ -2,9 +2,9 @@
import BasicLayout from "@/layouts/BasicLayout.vue";
import { apiClient } from "@/utils/api-client";
import { VButton, VSpace, VTabbar, VAvatar } from "@halo-dev/components";
import { computed, onMounted, provide, ref, watch } from "vue";
import { computed, onMounted, provide, ref, watch, type Ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import type { User } from "@halo-dev/api-client";
import type { DetailedUser } from "@halo-dev/api-client";
import UserEditingModal from "../components/UserEditingModal.vue";
import UserPasswordChangeModal from "../components/UserPasswordChangeModal.vue";
import { usePermission } from "@/utils/permission";
@ -26,7 +26,7 @@ const tabs = [
// },
];
const user = ref<User>();
const user = ref<DetailedUser>();
const loading = ref();
const editingModal = ref(false);
const passwordChangeModal = ref(false);
@ -40,7 +40,7 @@ const handleFetchUser = async () => {
const { data } = await apiClient.user.getCurrentUserDetail();
user.value = data;
} else {
const { data } = await apiClient.extension.user.getv1alpha1User({
const { data } = await apiClient.user.getUserDetail({
name: params.name as string,
});
user.value = data;
@ -56,10 +56,12 @@ const isCurrentUser = computed(() => {
if (params.name === "-") {
return true;
}
return user.value?.metadata.name === userStore.currentUser?.metadata.name;
return (
user.value?.user.metadata.name === userStore.currentUser?.metadata.name
);
});
provide("user", user);
provide<Ref<DetailedUser | undefined>>("user", user);
const activeTab = ref();
@ -92,12 +94,12 @@ const handleTabChange = (id: string) => {
<BasicLayout>
<UserEditingModal
v-model:visible="editingModal"
:user="user"
:user="user?.user"
@close="handleFetchUser"
/>
<UserPasswordChangeModal
v-model:visible="passwordChangeModal"
:user="user"
:user="user?.user"
@close="handleFetchUser"
/>
<header class="bg-white">
@ -107,8 +109,8 @@ const handleTabChange = (id: string) => {
<div class="h-20 w-20">
<VAvatar
v-if="user"
:src="user?.spec.avatar"
:alt="user?.spec.displayName"
:src="user.user.spec.avatar"
:alt="user.user.spec.displayName"
circle
width="100%"
height="100%"
@ -117,10 +119,10 @@ const handleTabChange = (id: string) => {
</div>
<div class="block">
<h1 class="truncate text-lg font-bold text-gray-900">
{{ user?.spec.displayName }}
{{ user?.user.spec.displayName }}
</h1>
<span v-if="!loading" class="text-sm text-gray-600">
@{{ user?.metadata.name }}
@{{ user?.user.metadata.name }}
</span>
</div>
</div>

View File

@ -1,37 +1,12 @@
import { defineStore } from "pinia";
import type { Role, UserPermission } from "@halo-dev/api-client";
import type { UserPermission } from "@halo-dev/api-client";
import { ref } from "vue";
import { apiClient } from "@/utils/api-client";
import { roleLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations";
export const useRoleStore = defineStore("role", () => {
const roles = ref<Role[]>([]);
const permissions = ref<UserPermission>({
roles: [],
uiPermissions: [],
});
async function fetchRoles() {
try {
const { data } = await apiClient.extension.role.listv1alpha1Role(
{
page: 0,
size: 0,
labelSelector: [`!${roleLabels.TEMPLATE}`],
},
{ mute: true }
);
roles.value = data.items;
} catch (error) {
console.error("Failed to fetch roles", error);
}
}
function getRoleDisplayName(name: string) {
const role = roles.value.find((role) => role.metadata.name === name);
return role?.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] || name;
}
return { roles, permissions, fetchRoles, getRoleDisplayName };
return { permissions };
});

View File

@ -1,9 +1,10 @@
import { apiClient } from "@/utils/api-client";
import type { User } from "@halo-dev/api-client";
import type { Role, User } from "@halo-dev/api-client";
import { defineStore } from "pinia";
interface UserStoreState {
currentUser?: User;
currentRoles?: Role[];
isAnonymous: boolean;
loginModalVisible: boolean;
}
@ -11,6 +12,7 @@ interface UserStoreState {
export const useUserStore = defineStore("user", {
state: (): UserStoreState => ({
currentUser: undefined,
currentRoles: [],
isAnonymous: true,
loginModalVisible: false,
}),
@ -18,8 +20,9 @@ export const useUserStore = defineStore("user", {
async fetchCurrentUser() {
try {
const { data } = await apiClient.user.getCurrentUserDetail();
this.currentUser = data;
this.isAnonymous = data.metadata.name === "anonymousUser";
this.currentUser = data.user;
this.currentRoles = data.roles;
this.isAnonymous = data.user.metadata.name === "anonymousUser";
} catch (e) {
console.error("Failed to fetch current user", e);
}