feat(api-keys): show creator column and filter to org owners

Switch the gate from `currentUser.is_admin` to `access.canSeeOrgAdmin`
so Org owners get the same all-keys view (Creator column, creator
filter, `user_id: '*'` default) that platform admins have. Mirrors
the BE's "platform admin OR current-Org owner" gate on listing every
key in scope.

The user picker now fetches `/user-directory` instead of the
admin-only `/users` endpoint, which would 403 Org owners. Rename the
filter placeholder from the misnamed `models.table.filterByName` to
`common.filter.byCreator`.
This commit is contained in:
gitlawr
2026-06-03 15:25:38 +08:00
committed by jialin
parent 72e794c281
commit 57fec89f2d
8 changed files with 32 additions and 17 deletions
+1
View File
@@ -66,6 +66,7 @@ export default {
'common.search.name.placeholder': 'filter by name', 'common.search.name.placeholder': 'filter by name',
'common.search.id.placeholder': 'filter by ID', 'common.search.id.placeholder': 'filter by ID',
'common.filter.byId': 'filter by ID', 'common.filter.byId': 'filter by ID',
'common.filter.byCreator': 'Filter by creator',
'common.table.type': 'Type', 'common.table.type': 'Type',
'common.table.default': 'Default Value', 'common.table.default': 'Default Value',
'common.copy.success': 'Copied success!', 'common.copy.success': 'Copied success!',
+1
View File
@@ -66,6 +66,7 @@ export default {
'common.search.name.placeholder': '名前でフィルタ', 'common.search.name.placeholder': '名前でフィルタ',
'common.search.id.placeholder': 'IDでフィルタ', 'common.search.id.placeholder': 'IDでフィルタ',
'common.filter.byId': 'IDでフィルタ', 'common.filter.byId': 'IDでフィルタ',
'common.filter.byCreator': '作成者でフィルタ',
'common.table.type': 'タイプ', 'common.table.type': 'タイプ',
'common.table.default': 'デフォルト値', 'common.table.default': 'デフォルト値',
'common.copy.success': 'コピー成功!', 'common.copy.success': 'コピー成功!',
+1
View File
@@ -66,6 +66,7 @@ export default {
'common.search.name.placeholder': 'Фильтр по названию', 'common.search.name.placeholder': 'Фильтр по названию',
'common.search.id.placeholder': 'Фильтр по ID', 'common.search.id.placeholder': 'Фильтр по ID',
'common.filter.byId': 'Фильтр по ID', 'common.filter.byId': 'Фильтр по ID',
'common.filter.byCreator': 'Фильтр по создателю',
'common.table.type': 'Тип', 'common.table.type': 'Тип',
'common.table.default': 'Значение по умолчанию', 'common.table.default': 'Значение по умолчанию',
'common.copy.success': 'Скопировано!', 'common.copy.success': 'Скопировано!',
+1
View File
@@ -66,6 +66,7 @@ export default {
'common.search.name.placeholder': 'ada göre filtrele', 'common.search.name.placeholder': 'ada göre filtrele',
'common.search.id.placeholder': 'kimliğe göre filtrele', 'common.search.id.placeholder': 'kimliğe göre filtrele',
'common.filter.byId': 'kimliğe göre filtrele', 'common.filter.byId': 'kimliğe göre filtrele',
'common.filter.byCreator': 'Oluşturana göre filtrele',
'common.table.type': 'Tür', 'common.table.type': 'Tür',
'common.table.default': 'Varsayılan Değer', 'common.table.default': 'Varsayılan Değer',
'common.copy.success': 'Kopyalama başarılı!', 'common.copy.success': 'Kopyalama başarılı!',
+1
View File
@@ -233,6 +233,7 @@ export default {
'common.text.tips': '提示', 'common.text.tips': '提示',
'settings.system': '系统设置', 'settings.system': '系统设置',
'common.filter.byId': '按 ID 查询', 'common.filter.byId': '按 ID 查询',
'common.filter.byCreator': '按创建者筛选',
'common.appearance': '外观', 'common.appearance': '外观',
'common.appearance.dark': '深色', 'common.appearance.dark': '深色',
'common.appearance.light': '浅色', 'common.appearance.light': '浅色',
@@ -18,7 +18,10 @@ type RankedAction = APIKeyAction & { priority: number };
interface ColumnsHookProps { interface ColumnsHookProps {
handleSelect: (val: string, record: ListItem, item?: APIKeyAction) => void; handleSelect: (val: string, record: ListItem, item?: APIKeyAction) => void;
sortOrder: string[]; sortOrder: string[];
is_admin?: boolean; // Reveal the Creator column to callers who can see other users' keys
// (platform admin or current-Org owner). Members only see their own
// keys, so the column would be redundant for them.
showCreator?: boolean;
configActions?: APIKeyConfigAction[]; configActions?: APIKeyConfigAction[];
// Dispatches the click for a plugin-contributed dropdown entry to the // Dispatches the click for a plugin-contributed dropdown entry to the
// controller `useCreate()` returned for that entry. // controller `useCreate()` returned for that entry.
@@ -28,7 +31,7 @@ interface ColumnsHookProps {
const useModelsColumns = ({ const useModelsColumns = ({
handleSelect, handleSelect,
sortOrder, sortOrder,
is_admin, showCreator,
configActions = [], configActions = [],
onConfigAction onConfigAction
}: ColumnsHookProps): ColumnsType<ListItem> => { }: ColumnsHookProps): ColumnsType<ListItem> => {
@@ -180,7 +183,7 @@ const useModelsColumns = ({
title: intl.formatMessage({ id: 'common.table.creator' }), title: intl.formatMessage({ id: 'common.table.creator' }),
dataIndex: 'user_name', dataIndex: 'user_name',
key: 'user_name', key: 'user_name',
hidden: !is_admin, hidden: !showCreator,
render: (text: string) => ( render: (text: string) => (
<AutoTooltip ghost style={{ maxWidth: 200 }}> <AutoTooltip ghost style={{ maxWidth: 200 }}>
{text || '-'} {text || '-'}
@@ -216,7 +219,7 @@ const useModelsColumns = ({
) )
} }
]; ];
}, [intl, is_admin, handleSelect, actionList]); }, [intl, showCreator, handleSelect, actionList]);
}; };
export default useModelsColumns; export default useModelsColumns;
+11 -8
View File
@@ -3,9 +3,8 @@ import { PaginationKey } from '@/config/settings';
import type { PageActionType } from '@/config/types'; import type { PageActionType } from '@/config/types';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import useQueryUserList from '@/pages/users/services/use-query-user-list'; import useQueryUserList from '@/pages/users/services/use-query-user-list';
import { useModel } from '@@/plugin-model';
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui'; import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useAccess, useIntl } from '@umijs/max';
import useMemoizedFn from 'ahooks/lib/useMemoizedFn'; import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
import { ConfigProvider, Table } from 'antd'; import { ConfigProvider, Table } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
@@ -22,8 +21,12 @@ import {
} from './plugin'; } from './plugin';
const APIKeys: React.FC = () => { const APIKeys: React.FC = () => {
const { initialState } = useModel('@@initialState'); const access = useAccess();
const currentUser = initialState?.currentUser; // `canSeeOrgAdmin` widens to Org owners in the enterprise build —
// mirrors the BE's "platform admin OR current-Org owner" gate on
// listing every key in scope. Personal/member users continue to see
// only their own keys (`user_id: undefined`).
const canSeeAllKeys = !!access.canSeeOrgAdmin;
const { const {
TABLE_SORT_DIRECTIONS, TABLE_SORT_DIRECTIONS,
dataSource, dataSource,
@@ -45,7 +48,7 @@ const APIKeys: React.FC = () => {
deleteAPI: deleteApisKey, deleteAPI: deleteApisKey,
contentForDelete: 'apikeys.table.apikeys', contentForDelete: 'apikeys.table.apikeys',
defaultQueryParams: { defaultQueryParams: {
user_id: currentUser?.is_admin ? '*' : undefined user_id: canSeeAllKeys ? '*' : undefined
} }
}); });
const { const {
@@ -198,7 +201,7 @@ const APIKeys: React.FC = () => {
const columns = useKeysColumns({ const columns = useKeysColumns({
handleSelect: onSelect, handleSelect: onSelect,
sortOrder, sortOrder,
is_admin: currentUser?.is_admin, showCreator: canSeeAllKeys,
configActions, configActions,
onConfigAction: handleConfigAction onConfigAction: handleConfigAction
}); });
@@ -209,10 +212,10 @@ const APIKeys: React.FC = () => {
<FilterBar <FilterBar
marginBottom={22} marginBottom={22}
marginTop={30} marginTop={30}
showSelect={currentUser?.is_admin} showSelect={canSeeAllKeys}
selectOptions={userList} selectOptions={userList}
select={{ showSearch: { optionFilterProp: 'label' } }} select={{ showSearch: { optionFilterProp: 'label' } }}
selectHolder={intl.formatMessage({ id: 'models.table.filterByName' })} selectHolder={intl.formatMessage({ id: 'common.filter.byCreator' })}
buttonText={intl.formatMessage({ id: 'apikeys.button.create' })} buttonText={intl.formatMessage({ id: 'apikeys.button.create' })}
handleSearch={handleSearch} handleSearch={handleSearch}
handleDeleteByBatch={handleDeleteBatch} handleDeleteByBatch={handleDeleteBatch}
@@ -1,25 +1,29 @@
import { useQueryDataList } from '@/hooks/use-query-data-list'; import { useQueryDataList } from '@/hooks/use-query-data-list';
import { useModel } from '@@/plugin-model'; import { useAccess } from '@umijs/max';
import { queryUsersList } from '../apis'; import { queryUserDirectory } from '../apis';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
// Backed by `/user-directory`, which the BE opens to platform admin AND
// Org owners (the admin-only `/users` endpoint would 403 the latter).
// `canSeeOrgAdmin` mirrors the same gate on the FE so non-admin Org
// owners can populate user pickers without hitting an error.
export const useQueryUserList = (optons?: { export const useQueryUserList = (optons?: {
getLabel?: (item: ListItem) => string; getLabel?: (item: ListItem) => string;
getValue?: (item: ListItem) => any; getValue?: (item: ListItem) => any;
}) => { }) => {
const { initialState } = useModel('@@initialState'); const access = useAccess();
const { dataList, loading, fetchData, cancelRequest } = useQueryDataList< const { dataList, loading, fetchData, cancelRequest } = useQueryDataList<
ListItem, ListItem,
Global.SearchParams Global.SearchParams
>({ >({
key: 'userList', key: 'userList',
fetchList: queryUsersList, fetchList: queryUserDirectory,
getLabel: optons?.getLabel, getLabel: optons?.getLabel,
getValue: optons?.getValue getValue: optons?.getValue
}); });
const fetchUserList = (params: Global.SearchParams) => { const fetchUserList = (params: Global.SearchParams) => {
if (!initialState?.currentUser?.is_admin) return; if (!access.canSeeOrgAdmin) return;
return fetchData({ return fetchData({
...params ...params
}); });