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:
@@ -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!',
|
||||||
|
|||||||
@@ -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': 'コピー成功!',
|
||||||
|
|||||||
@@ -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': 'Скопировано!',
|
||||||
|
|||||||
@@ -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ı!',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user