From 42ac377c17ab03f3fde716f91e9f7fa02e39abaa Mon Sep 17 00:00:00 2001 From: gitlawr Date: Fri, 5 Jun 2026 16:11:52 +0800 Subject: [PATCH] feat: tease enterprise multi-tenancy and API key controls in OSS Add an Organizations menu entry (admin-only) that routes to an upsell page explaining the multi-tenancy module, and surface disabled IP Access Control / Quota Limit items in the API Key dropdown with a tooltip pointing at the enterprise edition. Both placeholders are shadowed by the enterprise plugin at build time: the route merger removes the OSS Organizations entry by name, and the dropdown skips each placeholder when the same key is contributed via configActions. --- config/routes.ts | 14 ++++ src/locales/en-US/apikeys.ts | 4 +- src/locales/en-US/common.ts | 1 + src/locales/en-US/menu.ts | 1 + src/locales/en-US/organizations.ts | 15 ++++ src/locales/ja-JP/apikeys.ts | 4 +- src/locales/ja-JP/common.ts | 1 + src/locales/ja-JP/menu.ts | 1 + src/locales/ja-JP/organizations.ts | 15 ++++ src/locales/ru-RU/apikeys.ts | 4 +- src/locales/ru-RU/common.ts | 1 + src/locales/ru-RU/menu.ts | 1 + src/locales/ru-RU/organizations.ts | 15 ++++ src/locales/tr-TR/apikeys.ts | 4 +- src/locales/tr-TR/common.ts | 1 + src/locales/tr-TR/menu.ts | 1 + src/locales/tr-TR/organizations.ts | 15 ++++ src/locales/zh-CN/apikeys.ts | 4 +- src/locales/zh-CN/common.ts | 1 + src/locales/zh-CN/menu.ts | 1 + src/locales/zh-CN/organizations.ts | 12 +++ src/pages/api-keys/hooks/use-keys-columns.tsx | 60 ++++++++++++-- src/pages/organizations/index.tsx | 80 +++++++++++++++++++ 23 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 src/locales/en-US/organizations.ts create mode 100644 src/locales/ja-JP/organizations.ts create mode 100644 src/locales/ru-RU/organizations.ts create mode 100644 src/locales/tr-TR/organizations.ts create mode 100644 src/locales/zh-CN/organizations.ts create mode 100644 src/pages/organizations/index.tsx diff --git a/config/routes.ts b/config/routes.ts index 5ee51237..416fbade 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -350,6 +350,20 @@ const baseRoutes = [ path: '/access-control', redirect: '/access-control/users' }, + { + name: 'organizations', + path: '/access-control/organizations', + key: 'organizations', + icon: 'icon-org-outlined', + selectedIcon: 'icon-org-filled', + defaultIcon: 'icon-org-outlined', + // OSS exposes the menu to platform admins as a teaser for the + // enterprise multi-tenancy module. The page itself just renders + // an upsell notice — the real CRUD UI lives in the enterprise + // plugin and shadows this route via `routes.extensions.ts`. + access: 'canSeeAdmin', + component: './organizations' + }, { name: 'users', path: '/access-control/users', diff --git a/src/locales/en-US/apikeys.ts b/src/locales/en-US/apikeys.ts index 528689dc..009fc7fd 100644 --- a/src/locales/en-US/apikeys.ts +++ b/src/locales/en-US/apikeys.ts @@ -23,5 +23,7 @@ export default { 'apikeys.accessScope.inference': 'Inference APIs', 'apikeys.access.permissions': 'Access Permissions', 'apikeys.type.auto': 'Auto-generated', - 'apikeys.type.custom': 'Custom' + 'apikeys.type.custom': 'Custom', + 'apikeys.button.ipConfig': 'IP Access Control', + 'quotaLimits.button.title': 'Quota Limit' }; diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 3c7a3a3c..d69c15c1 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -46,6 +46,7 @@ export default { 'common.button.enabled': 'Enabled', 'common.button.disabled': 'Disabled', 'common.button.upgrade': 'Upgrade', + 'common.enterprise.feature': 'Available in GPUStack Enterprise', 'common.input.holder': 'Please enter', 'common.validate.value': '{name} value is required', 'common.button.edit': 'Edit', diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index e4efedbe..4c35707a 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -31,6 +31,7 @@ export default { 'menu.accessControl': 'Access Control', 'menu.accessControl.apikeys': 'API Keys', 'menu.accessControl.users': 'Users', + 'menu.accessControl.organizations': 'Organizations', 'menu.profile': 'Preferences', 'menu.login': 'Login', 'menu.usage': 'Usage', diff --git a/src/locales/en-US/organizations.ts b/src/locales/en-US/organizations.ts new file mode 100644 index 00000000..7b1189f0 --- /dev/null +++ b/src/locales/en-US/organizations.ts @@ -0,0 +1,15 @@ +export default { + 'organizations.upsell.title': 'Organizations are an Enterprise feature', + 'organizations.upsell.subtitle': + 'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.', + 'organizations.upsell.featuresTitle': 'What you get in Enterprise', + 'organizations.upsell.feature.orgs': + 'Create organizations to group users and isolate workloads', + 'organizations.upsell.feature.members': + 'Manage members and roles per organization', + 'organizations.upsell.feature.quotas': + 'Set resource and token quotas per organization', + 'organizations.upsell.feature.isolation': + 'Scope API keys, model deployments, and resources per organization', + 'organizations.upsell.cta': 'Learn about Enterprise' +}; diff --git a/src/locales/ja-JP/apikeys.ts b/src/locales/ja-JP/apikeys.ts index 6fd244c7..38bbf5e2 100644 --- a/src/locales/ja-JP/apikeys.ts +++ b/src/locales/ja-JP/apikeys.ts @@ -23,7 +23,9 @@ export default { 'apikeys.accessScope.inference': 'Inference APIs', 'apikeys.access.permissions': 'Access Permissions', 'apikeys.type.auto': 'Auto-generated', - 'apikeys.type.custom': 'Custom' + 'apikeys.type.custom': 'Custom', + 'apikeys.button.ipConfig': 'IP Access Control', + 'quotaLimits.button.title': 'Quota Limit' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== diff --git a/src/locales/ja-JP/common.ts b/src/locales/ja-JP/common.ts index f8ead094..5fd4608e 100644 --- a/src/locales/ja-JP/common.ts +++ b/src/locales/ja-JP/common.ts @@ -46,6 +46,7 @@ export default { 'common.button.enabled': '有効', 'common.button.disabled': '無効', 'common.button.upgrade': 'アップグレード', + 'common.enterprise.feature': 'Available in GPUStack Enterprise', 'common.input.holder': '入力してください', 'common.validate.value': '{name} の値は必須です', 'common.button.edit': '編集', diff --git a/src/locales/ja-JP/menu.ts b/src/locales/ja-JP/menu.ts index c84e0575..8fa294e0 100644 --- a/src/locales/ja-JP/menu.ts +++ b/src/locales/ja-JP/menu.ts @@ -37,6 +37,7 @@ export default { 'menu.accessControl': 'Access Control', 'menu.accessControl.apikeys': 'API Keys', 'menu.accessControl.users': 'Users', + 'menu.accessControl.organizations': 'Organizations', 'menu.resources.clusters': 'Clusters', 'menu.resources.credentials': 'Cloud Credentials', 'menu.models.userModels': 'My Models', diff --git a/src/locales/ja-JP/organizations.ts b/src/locales/ja-JP/organizations.ts new file mode 100644 index 00000000..7b1189f0 --- /dev/null +++ b/src/locales/ja-JP/organizations.ts @@ -0,0 +1,15 @@ +export default { + 'organizations.upsell.title': 'Organizations are an Enterprise feature', + 'organizations.upsell.subtitle': + 'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.', + 'organizations.upsell.featuresTitle': 'What you get in Enterprise', + 'organizations.upsell.feature.orgs': + 'Create organizations to group users and isolate workloads', + 'organizations.upsell.feature.members': + 'Manage members and roles per organization', + 'organizations.upsell.feature.quotas': + 'Set resource and token quotas per organization', + 'organizations.upsell.feature.isolation': + 'Scope API keys, model deployments, and resources per organization', + 'organizations.upsell.cta': 'Learn about Enterprise' +}; diff --git a/src/locales/ru-RU/apikeys.ts b/src/locales/ru-RU/apikeys.ts index 4c829bc5..b71a13fc 100644 --- a/src/locales/ru-RU/apikeys.ts +++ b/src/locales/ru-RU/apikeys.ts @@ -23,7 +23,9 @@ export default { 'apikeys.accessScope.inference': 'Inference APIs', 'apikeys.access.permissions': 'Access Permissions', 'apikeys.type.auto': 'Auto-generated', - 'apikeys.type.custom': 'Custom' + 'apikeys.type.custom': 'Custom', + 'apikeys.button.ipConfig': 'IP Access Control', + 'quotaLimits.button.title': 'Quota Limit' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== diff --git a/src/locales/ru-RU/common.ts b/src/locales/ru-RU/common.ts index 22036871..b1106944 100644 --- a/src/locales/ru-RU/common.ts +++ b/src/locales/ru-RU/common.ts @@ -46,6 +46,7 @@ export default { 'common.button.enabled': 'Активно', 'common.button.disabled': 'Отключено', 'common.button.upgrade': 'Обновить', + 'common.enterprise.feature': 'Available in GPUStack Enterprise', 'common.input.holder': 'Введите значение', 'common.validate.value': 'Поле {name} обязательно', 'common.button.edit': 'Редактировать', diff --git a/src/locales/ru-RU/menu.ts b/src/locales/ru-RU/menu.ts index 525b7ca5..ae81bd9d 100644 --- a/src/locales/ru-RU/menu.ts +++ b/src/locales/ru-RU/menu.ts @@ -36,6 +36,7 @@ export default { 'menu.accessControl': 'Управление доступом', 'menu.accessControl.apikeys': 'API Ключи', 'menu.accessControl.users': 'Пользователи', + 'menu.accessControl.organizations': 'Организации', 'menu.resources.clusters': 'Кластеры', 'menu.resources.credentials': 'Облачные аккаунты', 'menu.models.userModels': 'Мои модели', diff --git a/src/locales/ru-RU/organizations.ts b/src/locales/ru-RU/organizations.ts new file mode 100644 index 00000000..7b1189f0 --- /dev/null +++ b/src/locales/ru-RU/organizations.ts @@ -0,0 +1,15 @@ +export default { + 'organizations.upsell.title': 'Organizations are an Enterprise feature', + 'organizations.upsell.subtitle': + 'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.', + 'organizations.upsell.featuresTitle': 'What you get in Enterprise', + 'organizations.upsell.feature.orgs': + 'Create organizations to group users and isolate workloads', + 'organizations.upsell.feature.members': + 'Manage members and roles per organization', + 'organizations.upsell.feature.quotas': + 'Set resource and token quotas per organization', + 'organizations.upsell.feature.isolation': + 'Scope API keys, model deployments, and resources per organization', + 'organizations.upsell.cta': 'Learn about Enterprise' +}; diff --git a/src/locales/tr-TR/apikeys.ts b/src/locales/tr-TR/apikeys.ts index 30499244..c9b51333 100644 --- a/src/locales/tr-TR/apikeys.ts +++ b/src/locales/tr-TR/apikeys.ts @@ -23,5 +23,7 @@ export default { 'apikeys.accessScope.inference': 'Inference APIs', 'apikeys.access.permissions': 'Access Permissions', 'apikeys.type.auto': 'Auto-generated', - 'apikeys.type.custom': 'Custom' + 'apikeys.type.custom': 'Custom', + 'apikeys.button.ipConfig': 'IP Access Control', + 'quotaLimits.button.title': 'Quota Limit' }; diff --git a/src/locales/tr-TR/common.ts b/src/locales/tr-TR/common.ts index df7475a9..7fadf6e5 100644 --- a/src/locales/tr-TR/common.ts +++ b/src/locales/tr-TR/common.ts @@ -46,6 +46,7 @@ export default { 'common.button.enabled': 'Etkin', 'common.button.disabled': 'Devre dışı', 'common.button.upgrade': 'Yükselt', + 'common.enterprise.feature': 'Available in GPUStack Enterprise', 'common.input.holder': 'Lütfen girin', 'common.validate.value': '{name} değeri gereklidir', 'common.button.edit': 'Düzenle', diff --git a/src/locales/tr-TR/menu.ts b/src/locales/tr-TR/menu.ts index ef54de7d..71a8455f 100644 --- a/src/locales/tr-TR/menu.ts +++ b/src/locales/tr-TR/menu.ts @@ -29,6 +29,7 @@ export default { 'menu.accessControl': 'Erişim Kontrolü', 'menu.accessControl.apikeys': 'API Anahtarları', 'menu.accessControl.users': 'Kullanıcılar', + 'menu.accessControl.organizations': 'Organizasyonlar', 'menu.profile': 'Preferences', 'menu.login': 'Giriş', 'menu.usage': 'Kullanım', diff --git a/src/locales/tr-TR/organizations.ts b/src/locales/tr-TR/organizations.ts new file mode 100644 index 00000000..7b1189f0 --- /dev/null +++ b/src/locales/tr-TR/organizations.ts @@ -0,0 +1,15 @@ +export default { + 'organizations.upsell.title': 'Organizations are an Enterprise feature', + 'organizations.upsell.subtitle': + 'Multi-tenancy lets you isolate users, resources, and quotas across teams. Upgrade to GPUStack Enterprise to manage organizations.', + 'organizations.upsell.featuresTitle': 'What you get in Enterprise', + 'organizations.upsell.feature.orgs': + 'Create organizations to group users and isolate workloads', + 'organizations.upsell.feature.members': + 'Manage members and roles per organization', + 'organizations.upsell.feature.quotas': + 'Set resource and token quotas per organization', + 'organizations.upsell.feature.isolation': + 'Scope API keys, model deployments, and resources per organization', + 'organizations.upsell.cta': 'Learn about Enterprise' +}; diff --git a/src/locales/zh-CN/apikeys.ts b/src/locales/zh-CN/apikeys.ts index 4d52fd1a..2d05254c 100644 --- a/src/locales/zh-CN/apikeys.ts +++ b/src/locales/zh-CN/apikeys.ts @@ -22,5 +22,7 @@ export default { 'apikeys.accessScope.inference': '推理接口', 'apikeys.access.permissions': '访问权限', 'apikeys.type.auto': '自动生成', - 'apikeys.type.custom': '自定义' + 'apikeys.type.custom': '自定义', + 'apikeys.button.ipConfig': 'IP 访问控制', + 'quotaLimits.button.title': '配额限制' }; diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 786694d3..49a11b89 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -44,6 +44,7 @@ export default { 'common.button.rollback': '回滚', 'common.button.new': '新建{ text }', 'common.button.upgrade': '升级', + 'common.enterprise.feature': 'GPUStack 企业版可用', 'common.input.holder': '请输入', 'common.holder.search': '搜索', 'common.button.edit': '编辑', diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index 3237ccca..7d724ee5 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -38,6 +38,7 @@ export default { 'menu.accessControl': '访问控制', 'menu.accessControl.apikeys': 'API 密钥', 'menu.accessControl.users': '用户', + 'menu.accessControl.organizations': '组织', 'menu.resources.clusters': '集群', 'menu.resources.credentials': '云凭证', 'menu.resources.clusterDetail': '集群详情', diff --git a/src/locales/zh-CN/organizations.ts b/src/locales/zh-CN/organizations.ts new file mode 100644 index 00000000..30fa9bfe --- /dev/null +++ b/src/locales/zh-CN/organizations.ts @@ -0,0 +1,12 @@ +export default { + 'organizations.upsell.title': '组织是企业版功能', + 'organizations.upsell.subtitle': + '多租户可在团队间隔离用户、资源与配额。升级到 GPUStack 企业版即可管理组织。', + 'organizations.upsell.featuresTitle': '企业版包含的能力', + 'organizations.upsell.feature.orgs': '创建组织来分组用户并隔离工作负载', + 'organizations.upsell.feature.members': '为每个组织管理成员与角色', + 'organizations.upsell.feature.quotas': '为每个组织设置资源与 Token 配额', + 'organizations.upsell.feature.isolation': + '按组织隔离 API 密钥、模型部署与资源', + 'organizations.upsell.cta': '了解企业版' +}; diff --git a/src/pages/api-keys/hooks/use-keys-columns.tsx b/src/pages/api-keys/hooks/use-keys-columns.tsx index 774b158e..6f6e5d91 100644 --- a/src/pages/api-keys/hooks/use-keys-columns.tsx +++ b/src/pages/api-keys/hooks/use-keys-columns.tsx @@ -1,15 +1,27 @@ // columns.ts import { tableSorter } from '@/config/settings'; -import { AutoTooltip, DropdownButtons, icons } from '@gpustack/core-ui'; +import { DashboardOutlined } from '@ant-design/icons'; +import { + AutoTooltip, + DropdownButtons, + IconFont, + icons +} from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { MenuProps, Tag } from 'antd'; +import { MenuProps, Tag, Tooltip } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import dayjs from 'dayjs'; import { useMemo } from 'react'; import { ListItem } from '../config/types'; import type { APIKeyConfigAction } from '../plugin'; -type APIKeyAction = Global.ActionItem & { +type APIKeyAction = Omit, 'disabled' | 'label'> & { + // Per-row callback (function) is the host's default; placeholders use a + // plain boolean to render the menu item grayed out unconditionally. + disabled?: boolean | ((record: ListItem) => boolean); + // ReactNode allowed so disabled placeholders can render a Tooltip- + // wrapped label (paired with `locale: false`). + label: string | React.ReactNode; onClick?: (record: ListItem) => void; }; @@ -68,10 +80,48 @@ const useModelsColumns = ({ onClick: (record: ListItem) => onConfigAction?.(a.key, record) })); - return [...builtIns, ...fromPlugins].sort( + // Show disabled placeholders for IP Access Control / Quota Limit in + // the OSS build only — when the enterprise plugin contributes the + // real entry under the same key, skip the placeholder so the live + // action takes over. Keeps the dropdown's surface area consistent + // between editions while making the upgrade path discoverable. + const pluginKeys = new Set(configActions.map((a) => a.key)); + const enterpriseTooltip = intl.formatMessage({ + id: 'common.enterprise.feature' + }); + const enterprisePlaceholder = (labelId: string): React.ReactNode => ( + + + {intl.formatMessage({ id: labelId })} + + + ); + const placeholders: RankedAction[] = []; + if (!pluginKeys.has('ipConfig')) { + placeholders.push({ + key: 'ipConfig', + label: enterprisePlaceholder('apikeys.button.ipConfig'), + locale: false, + icon: , + disabled: true, + priority: 12 + }); + } + if (!pluginKeys.has('quotaLimit')) { + placeholders.push({ + key: 'quotaLimit', + label: enterprisePlaceholder('quotaLimits.button.title'), + locale: false, + icon: , + disabled: true, + priority: 14 + }); + } + + return [...builtIns, ...fromPlugins, ...placeholders].sort( (a, b) => a.priority - b.priority ); - }, [configActions, onConfigAction]); + }, [intl, configActions, onConfigAction]); return useMemo(() => { return [ diff --git a/src/pages/organizations/index.tsx b/src/pages/organizations/index.tsx new file mode 100644 index 00000000..40c59e21 --- /dev/null +++ b/src/pages/organizations/index.tsx @@ -0,0 +1,80 @@ +import { IconFont } from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { Button, Result, Typography } from 'antd'; +import PageBox from '../_components/page-box'; + +// OSS-only upsell page for the multi-tenancy module. The real +// Organizations CRUD UI ships in the enterprise plugin and shadows +// this route via `routes.extensions.ts` — when the enterprise plugin +// is loaded, the merger removes this entry by name. So this file is +// what `canSeeAdmin` users see on the OSS build only. +const Organizations: React.FC = () => { + const intl = useIntl(); + const features = [ + 'organizations.upsell.feature.orgs', + 'organizations.upsell.feature.members', + 'organizations.upsell.feature.quotas', + 'organizations.upsell.feature.isolation' + ]; + return ( + +
+ + } + title={intl.formatMessage({ id: 'organizations.upsell.title' })} + subTitle={intl.formatMessage({ id: 'organizations.upsell.subtitle' })} + extra={ +
+ + {intl.formatMessage({ + id: 'organizations.upsell.featuresTitle' + })} + +
    + {features.map((id) => ( +
  • + {intl.formatMessage({ id })} +
  • + ))} +
+
+ +
+
+ } + /> +
+
+ ); +}; + +export default Organizations;