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.
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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) ==========
|
||||
|
||||
@@ -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': '編集',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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) ==========
|
||||
|
||||
@@ -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': 'Редактировать',
|
||||
|
||||
@@ -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': 'Мои модели',
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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': '配额限制'
|
||||
};
|
||||
|
||||
@@ -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': '编辑',
|
||||
|
||||
@@ -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': '集群详情',
|
||||
|
||||
@@ -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': '了解企业版'
|
||||
};
|
||||
@@ -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<ListItem> & {
|
||||
type APIKeyAction = Omit<Global.ActionItem<ListItem>, '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 => (
|
||||
<Tooltip title={enterpriseTooltip} placement="left">
|
||||
<span style={{ display: 'inline-block' }}>
|
||||
{intl.formatMessage({ id: labelId })}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
const placeholders: RankedAction[] = [];
|
||||
if (!pluginKeys.has('ipConfig')) {
|
||||
placeholders.push({
|
||||
key: 'ipConfig',
|
||||
label: enterprisePlaceholder('apikeys.button.ipConfig'),
|
||||
locale: false,
|
||||
icon: <IconFont type="icon-safe-ip" />,
|
||||
disabled: true,
|
||||
priority: 12
|
||||
});
|
||||
}
|
||||
if (!pluginKeys.has('quotaLimit')) {
|
||||
placeholders.push({
|
||||
key: 'quotaLimit',
|
||||
label: enterprisePlaceholder('quotaLimits.button.title'),
|
||||
locale: false,
|
||||
icon: <DashboardOutlined />,
|
||||
disabled: true,
|
||||
priority: 14
|
||||
});
|
||||
}
|
||||
|
||||
return [...builtIns, ...fromPlugins, ...placeholders].sort(
|
||||
(a, b) => a.priority - b.priority
|
||||
);
|
||||
}, [configActions, onConfigAction]);
|
||||
}, [intl, configActions, onConfigAction]);
|
||||
|
||||
return useMemo(() => {
|
||||
return [
|
||||
|
||||
@@ -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 (
|
||||
<PageBox>
|
||||
<div
|
||||
style={{
|
||||
// Use a viewport-relative min-height so flex centering has
|
||||
// something to work against — percentage heights through
|
||||
// PageBox's wrapper chain don't always resolve. The offset
|
||||
// accounts for the global header + the page title bar.
|
||||
minHeight: 'calc(100vh - 180px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
icon={
|
||||
<IconFont
|
||||
type="icon-org-outlined"
|
||||
style={{ fontSize: 64, color: 'var(--ant-color-primary)' }}
|
||||
/>
|
||||
}
|
||||
title={intl.formatMessage({ id: 'organizations.upsell.title' })}
|
||||
subTitle={intl.formatMessage({ id: 'organizations.upsell.subtitle' })}
|
||||
extra={
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 520,
|
||||
margin: '0 auto',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={5} style={{ marginBottom: 12 }}>
|
||||
{intl.formatMessage({
|
||||
id: 'organizations.upsell.featuresTitle'
|
||||
})}
|
||||
</Typography.Title>
|
||||
<ul style={{ paddingLeft: 20, marginBottom: 24 }}>
|
||||
{features.map((id) => (
|
||||
<li key={id} style={{ marginBottom: 6 }}>
|
||||
{intl.formatMessage({ id })}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
href="https://gpustack.ai/enterprise"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{intl.formatMessage({ id: 'organizations.upsell.cta' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</PageBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default Organizations;
|
||||
Reference in New Issue
Block a user