feat: distinguish same-name GPU service resources across users

Resource names are unique per owning principal, so different users
can legally create same-name GPU instances, templates, SSH public
keys, storage volumes and storage types — but the admin's
cross-tenant lists and the instance-create template picker rendered
them indistinguishably.

- Group the template picker by owning scope: Your Templates, Global
  Templates, then — admin cross-tenant view only — one group per
  owner. A single group renders flat without headers. A plugin can
  take over grouping via hooks.useTemplateOwnerGroups when its
  principal model scopes templates beyond USER owners; a plugin
  without the hook keeps the flat list.
- Add an admin-only Creator column to the Instances, SSH Public
  Keys, Storage and Storage Types tables, sitting right before
  Created (mirroring the API Keys page convention). Resolves
  creator_id to a username via the user directory, falling back to
  owner_principal_id for legacy rows — a personal-scope row's
  creator IS its owner.
- Tag template cards with their owner scope (Global / username) for
  admins.

The Creator column and the card tag defer to a plugin that registers
the page's list columns / the OwnerScopeTag slot. Requires the
gpustack server change that records creator_id on templates, SSH
public keys and storage types.
This commit is contained in:
gitlawr
2026-06-12 20:22:56 +08:00
committed by Lawrence Li
parent 778ea36a9d
commit 7a76bda484
20 changed files with 395 additions and 54 deletions
@@ -1,3 +1,4 @@
import useCreatorColumn from '@/pages/gpu-service/hooks/use-creator-column';
import { usePluginListColumns } from '@/plugins/list-extra-columns';
import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
@@ -20,6 +21,7 @@ const useStorageColumns = ({
}: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
const pluginCols = usePluginListColumns('gpuStorage');
const creatorCols = useCreatorColumn<ListItem>('gpuStorage');
return useMemo(() => {
const pluginRendered = pluginCols.map((c) => ({
title: intl.formatMessage({ id: c.titleId }),
@@ -68,6 +70,7 @@ const useStorageColumns = ({
sorter: false,
render: (value: string) => (value ? value.replace(/Gi$/, 'GB') : '-')
},
...creatorCols,
// {
// title: intl.formatMessage({ id: 'common.table.status' }),
// dataIndex: ['status', 'phase'],
@@ -111,7 +114,14 @@ const useStorageColumns = ({
)
}
];
}, [handleSelect, sortOrder, storageClassList, intl, pluginCols]);
}, [
handleSelect,
sortOrder,
storageClassList,
intl,
pluginCols,
creatorCols
]);
};
export default useStorageColumns;