feat: plugin extension slots for access, request interceptors, Users actions, and form fields
Four purely-additive seams that let build-time tooling extend host behaviour without forking files: * `src/access.extensions.ts` — identity `applyAccessExtensions` (mirrors `config/routes.extensions.ts`). `src/access.ts` runs the resolved predicate object through it. Adds two new predicates, `canSeeOrgAdmin` and `canManageCurrentOrg`, alongside the existing `canSeeAdmin`. Resources, Models children, Cluster Management, and Resources/Backends are retagged from `canSeeAdmin` to `canSeeOrgAdmin`. Users and Dashboard stay strict. * `src/request.extensions.ts` — identity-empty `extraRequestInterceptors`. `src/request-config.tsx` spreads it into the existing `requestInterceptors` list so extensions can inject context-aware headers without forking the request config. * Users page action column — renders `getGPUStackPlugin()?.components?.UserRowActions` next to the existing DropdownButtons inside a Space when a plugin component is registered. If absent, the cell renders exactly as before. * `src/components/plugin-extra-fields.tsx` — generic component-slot helper. Renders `pluginManager.components.<name>` if registered, forwarding a `context` payload. Used by create/edit forms to let plugins inject extra `Form.Item` fields. Mounted in the relevant create forms — API Keys, Cloud Credentials, Clusters, Model Routes, Model Providers, and Inference Backends — under the slot name `CreateOrgScopeField`. Resources whose org is implicit from a parent (Models / Workers / Benchmarks / Worker Pools / Model Files inherit from the chosen Cluster) deliberately don't mount the slot.
This commit is contained in:
+19
-20
@@ -104,9 +104,19 @@ const baseRoutes = [
|
|||||||
icon: 'icon-layers',
|
icon: 'icon-layers',
|
||||||
selectedIcon: 'icon-layers-filled',
|
selectedIcon: 'icon-layers-filled',
|
||||||
defaultIcon: 'icon-layers',
|
defaultIcon: 'icon-layers',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
component: './llmodels/catalog'
|
component: './llmodels/catalog'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'userModels',
|
||||||
|
path: '/models/user-models',
|
||||||
|
key: 'userModels',
|
||||||
|
icon: 'icon-models',
|
||||||
|
selectedIcon: 'icon-models-filled',
|
||||||
|
defaultIcon: 'icon-models',
|
||||||
|
access: 'canSeeUser',
|
||||||
|
component: './llmodels/user-models'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'deployment',
|
name: 'deployment',
|
||||||
path: '/models/deployments',
|
path: '/models/deployments',
|
||||||
@@ -114,7 +124,7 @@ const baseRoutes = [
|
|||||||
icon: 'icon-rocket-launch1',
|
icon: 'icon-rocket-launch1',
|
||||||
selectedIcon: 'icon-rocket-launch-fill',
|
selectedIcon: 'icon-rocket-launch-fill',
|
||||||
defaultIcon: 'icon-rocket-launch1',
|
defaultIcon: 'icon-rocket-launch1',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
component: './llmodels/index'
|
component: './llmodels/index'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -124,7 +134,7 @@ const baseRoutes = [
|
|||||||
icon: 'icon-captive_portal',
|
icon: 'icon-captive_portal',
|
||||||
selectedIcon: 'icon-captive_portal',
|
selectedIcon: 'icon-captive_portal',
|
||||||
defaultIcon: 'icon-captive_portal',
|
defaultIcon: 'icon-captive_portal',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
component: './model-routes/index'
|
component: './model-routes/index'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -143,20 +153,9 @@ const baseRoutes = [
|
|||||||
icon: 'icon-extension-outline',
|
icon: 'icon-extension-outline',
|
||||||
selectedIcon: 'icon-extension-filled',
|
selectedIcon: 'icon-extension-filled',
|
||||||
defaultIcon: 'icon-extension-outline',
|
defaultIcon: 'icon-extension-outline',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
component: './maas-provider/index'
|
component: './maas-provider/index'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
name: 'userModels',
|
|
||||||
path: '/models/user-models',
|
|
||||||
key: 'userModels',
|
|
||||||
icon: 'icon-models',
|
|
||||||
selectedIcon: 'icon-models-filled',
|
|
||||||
defaultIcon: 'icon-models',
|
|
||||||
access: 'canSeeUser',
|
|
||||||
component: './llmodels/user-models'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'benchmark',
|
name: 'benchmark',
|
||||||
path: '/models/benchmark',
|
path: '/models/benchmark',
|
||||||
@@ -164,7 +163,7 @@ const baseRoutes = [
|
|||||||
icon: 'icon-speed',
|
icon: 'icon-speed',
|
||||||
selectedIcon: 'icon-speed-filled',
|
selectedIcon: 'icon-speed-filled',
|
||||||
defaultIcon: 'icon-speed',
|
defaultIcon: 'icon-speed',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
component: './benchmark/index'
|
component: './benchmark/index'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -174,7 +173,7 @@ const baseRoutes = [
|
|||||||
icon: 'icon-speed',
|
icon: 'icon-speed',
|
||||||
selectedIcon: 'icon-speed-filled',
|
selectedIcon: 'icon-speed-filled',
|
||||||
defaultIcon: 'icon-speed',
|
defaultIcon: 'icon-speed',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
component: './benchmark/details'
|
component: './benchmark/details'
|
||||||
}
|
}
|
||||||
@@ -184,7 +183,7 @@ const baseRoutes = [
|
|||||||
name: 'resources',
|
name: 'resources',
|
||||||
path: '/resources',
|
path: '/resources',
|
||||||
key: 'resources',
|
key: 'resources',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/resources',
|
path: '/resources',
|
||||||
@@ -215,7 +214,7 @@ const baseRoutes = [
|
|||||||
icon: 'icon-backend',
|
icon: 'icon-backend',
|
||||||
selectedIcon: 'icon-backend-filled',
|
selectedIcon: 'icon-backend-filled',
|
||||||
defaultIcon: 'icon-backend',
|
defaultIcon: 'icon-backend',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
component: './backends/index'
|
component: './backends/index'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -233,7 +232,7 @@ const baseRoutes = [
|
|||||||
name: 'clusterManagement',
|
name: 'clusterManagement',
|
||||||
path: '/cluster-management',
|
path: '/cluster-management',
|
||||||
key: 'clusterManagement',
|
key: 'clusterManagement',
|
||||||
access: 'canSeeAdmin',
|
access: 'canSeeOrgAdmin',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/cluster-management',
|
path: '/cluster-management',
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Identity hook for build-time access-predicate extensions. Tooling may
|
||||||
|
// overwrite this file to widen predicates; the original is restored on
|
||||||
|
// cleanup. Mirrors `config/routes.extensions.ts`.
|
||||||
|
export type AccessPredicates = {
|
||||||
|
canSeeAdmin: boolean;
|
||||||
|
canSeeOrgAdmin: boolean;
|
||||||
|
canManageCurrentOrg: boolean;
|
||||||
|
canSeeUser: boolean;
|
||||||
|
canDelete: boolean;
|
||||||
|
canLogin: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyAccessExtensions = <T extends AccessPredicates>(base: T): T =>
|
||||||
|
base;
|
||||||
+20
-4
@@ -1,5 +1,7 @@
|
|||||||
|
import { applyAccessExtensions } from './access.extensions';
|
||||||
|
|
||||||
export default (initialState: { currentUser?: Global.UserInfo }) => {
|
export default (initialState: { currentUser?: Global.UserInfo }) => {
|
||||||
const canSeeAdmin = !!(
|
const isPlatformAdmin = !!(
|
||||||
initialState &&
|
initialState &&
|
||||||
initialState.currentUser &&
|
initialState.currentUser &&
|
||||||
initialState.currentUser.is_admin
|
initialState.currentUser.is_admin
|
||||||
@@ -10,10 +12,24 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
|
|||||||
!initialState.currentUser.is_admin
|
!initialState.currentUser.is_admin
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
// Predicate roles, top-down by strictness:
|
||||||
canSeeAdmin,
|
// * `canSeeAdmin` — strictly platform admin (`users.is_admin`).
|
||||||
|
// Gates Users, Dashboard.
|
||||||
|
// * `canSeeOrgAdmin` — admin-style menus that work cross-org
|
||||||
|
// (Resources, Models, Cluster Management). Defaults to platform
|
||||||
|
// admin; extensions widen to include org admins.
|
||||||
|
// * `canManageCurrentOrg` — pages that only make sense inside a
|
||||||
|
// specific org context (member / group management). Defaults to
|
||||||
|
// `false`; extensions widen when both an org is selected AND
|
||||||
|
// the caller is admin of it.
|
||||||
|
// Pass through `applyAccessExtensions` so build-time tooling can
|
||||||
|
// widen these without editing this file. Default is a no-op.
|
||||||
|
return applyAccessExtensions({
|
||||||
|
canSeeAdmin: isPlatformAdmin,
|
||||||
|
canSeeOrgAdmin: isPlatformAdmin,
|
||||||
|
canManageCurrentOrg: false,
|
||||||
canSeeUser,
|
canSeeUser,
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
canLogin: true
|
canLogin: true
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Render a plugin-provided component registered under
|
||||||
|
// `pluginManager.components.<key>`. If no plugin is registered, or
|
||||||
|
// the plugin doesn't export this slot, renders nothing — host pages
|
||||||
|
// embedding the slot stay unchanged when no plugin is present.
|
||||||
|
//
|
||||||
|
// Used by create / edit forms to let plugins inject extra Form.Item
|
||||||
|
// fields. Because antd `Form` walks the JSX tree to collect named
|
||||||
|
// `Form.Item` values, the plugin's content gets picked up by the
|
||||||
|
// surrounding form automatically — no host-side wiring required.
|
||||||
|
const PluginExtraFields: React.FC<{
|
||||||
|
name: string;
|
||||||
|
// Free-form payload forwarded to the plugin component, e.g. so a
|
||||||
|
// form-specific slot can pass its action / record context if it
|
||||||
|
// needs to.
|
||||||
|
context?: Record<string, any>;
|
||||||
|
}> = ({ name, context }) => {
|
||||||
|
const Slot = getGPUStackPlugin()?.components?.[name];
|
||||||
|
if (!Slot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <Slot context={context} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginExtraFields;
|
||||||
+31
-13
@@ -1,8 +1,13 @@
|
|||||||
import { history, useIntl } from '@umijs/max';
|
import { history, useIntl } from '@umijs/max';
|
||||||
import { Button, Result } from 'antd';
|
import { Button, Result } from 'antd';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { PageContainerInner } from '../pages/_components/page-box';
|
import { PageContainerInner } from '../pages/_components/page-box';
|
||||||
|
|
||||||
|
// On a 403, auto-redirect to root so context changes (e.g. an org
|
||||||
|
// switch into a context that can't see the current route) don't
|
||||||
|
// dead-end the user on a permission page. The 404 path stays as a
|
||||||
|
// classic Result with a Back button — those are usually genuine
|
||||||
|
// "wrong URL" landings the user should see and acknowledge.
|
||||||
const Exception: React.FC<{
|
const Exception: React.FC<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
route?: any;
|
route?: any;
|
||||||
@@ -12,23 +17,36 @@ const Exception: React.FC<{
|
|||||||
noFound?: React.ReactNode;
|
noFound?: React.ReactNode;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
// render custom 404
|
const unaccessible = !!props.route?.unaccessible;
|
||||||
console.log('exception====', props);
|
const customNoAccessible = props.unAccessible || props.noAccessible;
|
||||||
|
const willAutoRedirect = unaccessible && !customNoAccessible;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (willAutoRedirect) {
|
||||||
|
// `replace` instead of `push` so the user's Back button doesn't
|
||||||
|
// bounce them back into the unauthorized page (which would just
|
||||||
|
// redirect forward again).
|
||||||
|
history.replace('/');
|
||||||
|
}
|
||||||
|
}, [willAutoRedirect]);
|
||||||
|
|
||||||
|
// Render `null` during the auto-redirect path so the default 403
|
||||||
|
// Result doesn't flash for one frame before useEffect fires.
|
||||||
|
if (willAutoRedirect) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(!props.route && (props.noFound || props.notFound)) ||
|
(!props.route && (props.noFound || props.notFound)) ||
|
||||||
// render custom 403
|
// render custom 403
|
||||||
(props.route?.unaccessible && (props.unAccessible || props.noAccessible)) ||
|
(unaccessible && customNoAccessible) ||
|
||||||
// render default exception
|
// render default 404
|
||||||
((!props.route || props.route?.unaccessible) && (
|
(!props.route && (
|
||||||
<PageContainerInner>
|
<PageContainerInner>
|
||||||
<Result
|
<Result
|
||||||
status={props.route ? '403' : '404'}
|
status="404"
|
||||||
title={props.route ? '403' : '404'}
|
title="404"
|
||||||
subTitle={
|
subTitle={intl.formatMessage({ id: 'common.permission.404' })}
|
||||||
props.route
|
|
||||||
? intl.formatMessage({ id: 'common.permission.403' })
|
|
||||||
: intl.formatMessage({ id: 'common.permission.404' })
|
|
||||||
}
|
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" onClick={() => history.push('/')}>
|
<Button type="primary" onClick={() => history.push('/')}>
|
||||||
{intl.formatMessage({ id: 'common.button.back' })}
|
{intl.formatMessage({ id: 'common.button.back' })}
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export default (props: any) => {
|
|||||||
config={{
|
config={{
|
||||||
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
||||||
theme: userSettings.theme,
|
theme: userSettings.theme,
|
||||||
iconUrl: '//at.alicdn.com/t/c/font_4613488_ueevrwt2v9.js',
|
iconUrl: '//at.alicdn.com/t/c/font_4613488_tzcsatubq4f.js',
|
||||||
isDarkTheme: userSettings.isDarkTheme,
|
isDarkTheme: userSettings.isDarkTheme,
|
||||||
defaultColorPrimary: COLOR_PRIMARY
|
defaultColorPrimary: COLOR_PRIMARY
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import {
|
import {
|
||||||
@@ -44,6 +45,8 @@ const APIKeyForm: React.FC<{
|
|||||||
></CInput.Input>
|
></CInput.Input>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<PluginExtraFields name="CreateOrgScopeField" context={{ action }} />
|
||||||
|
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name="expires_in"
|
name="expires_in"
|
||||||
rules={[
|
rules={[
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import {
|
import {
|
||||||
AutoTooltip,
|
AutoTooltip,
|
||||||
DropdownActions,
|
DropdownActions,
|
||||||
@@ -248,30 +249,39 @@ const BackendCard: React.FC<BackendCardProps> = ({
|
|||||||
const source = data.is_built_in
|
const source = data.is_built_in
|
||||||
? BackendSourceLabelMap[BackendSourceValueMap.BUILTIN] || ''
|
? BackendSourceLabelMap[BackendSourceValueMap.BUILTIN] || ''
|
||||||
: BackendSourceLabelMap[data.backend_source] || '';
|
: BackendSourceLabelMap[data.backend_source] || '';
|
||||||
|
const ownerTag = (
|
||||||
|
<PluginExtraFields
|
||||||
|
name="BackendOwnerTag"
|
||||||
|
context={{ organizationId: data.organization_id }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return null;
|
return ownerTag;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Tag
|
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||||
color={
|
<Tag
|
||||||
TagColorMap[
|
color={
|
||||||
data.is_built_in
|
TagColorMap[
|
||||||
? BackendSourceValueMap.BUILTIN
|
data.is_built_in
|
||||||
: data.backend_source
|
? BackendSourceValueMap.BUILTIN
|
||||||
]
|
: data.backend_source
|
||||||
}
|
]
|
||||||
className="font-400"
|
}
|
||||||
variant="filled"
|
className="font-400"
|
||||||
style={{
|
variant="filled"
|
||||||
borderRadius: 'var(--ant-border-radius)',
|
style={{
|
||||||
margin: 0,
|
borderRadius: 'var(--ant-border-radius)',
|
||||||
width: 'max-content'
|
margin: 0,
|
||||||
}}
|
width: 'max-content'
|
||||||
>
|
}}
|
||||||
{intl.formatMessage({
|
>
|
||||||
id: source
|
{intl.formatMessage({
|
||||||
})}
|
id: source
|
||||||
</Tag>
|
})}
|
||||||
|
</Tag>
|
||||||
|
{ownerTag}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,11 @@ export const backendActions = [
|
|||||||
icon: icons.DeleteOutlined,
|
icon: icons.DeleteOutlined,
|
||||||
locale: true,
|
locale: true,
|
||||||
danger: true,
|
danger: true,
|
||||||
show: (record: any) => !record.is_built_in
|
// Platform built-ins are admin-curated and not user-deletable.
|
||||||
|
// An org-scoped override of a built-in IS deletable — deleting
|
||||||
|
// the override is how the user reverts to the Platform row.
|
||||||
|
// Plain custom backends are always deletable.
|
||||||
|
show: (record: any) => !record.is_built_in || record.organization_id != null
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { backendOptionsMap } from '@/pages/llmodels/constants/backend-parameters';
|
import { backendOptionsMap } from '@/pages/llmodels/constants/backend-parameters';
|
||||||
import {
|
import {
|
||||||
@@ -67,6 +68,10 @@ const BasicForm = () => {
|
|||||||
required
|
required
|
||||||
></CInput.Input>
|
></CInput.Input>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<PluginExtraFields
|
||||||
|
name="CreateOrgScopeField"
|
||||||
|
context={{ action, allowGlobal: true }}
|
||||||
|
/>
|
||||||
<Form.Item<FormData> hidden name="backend_source">
|
<Form.Item<FormData> hidden name="backend_source">
|
||||||
<CInput.Input></CInput.Input>
|
<CInput.Input></CInput.Input>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -166,7 +166,15 @@ const BackendList = () => {
|
|||||||
}
|
}
|
||||||
// ================ Delete ================
|
// ================ Delete ================
|
||||||
if (item.action === 'delete') {
|
if (item.action === 'delete') {
|
||||||
if (item.data.backend_source === BackendSourceValueMap.COMMUNITY) {
|
// Platform community rows aren't actually deletable — the
|
||||||
|
// "delete" action there is a soft-disable on the admin-curated
|
||||||
|
// catalog. An org-scoped row of any source (including an org's
|
||||||
|
// override of a community backend) IS owner-mutable data and
|
||||||
|
// gets a real DELETE.
|
||||||
|
const isPlatformCommunity =
|
||||||
|
item.data.backend_source === BackendSourceValueMap.COMMUNITY &&
|
||||||
|
item.data.organization_id == null;
|
||||||
|
if (isPlatformCommunity) {
|
||||||
modalRef.current?.show({
|
modalRef.current?.show({
|
||||||
content: 'backends.title',
|
content: 'backends.title',
|
||||||
operation: 'common.delete.single.confirm',
|
operation: 'common.delete.single.confirm',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { Input as CInput, FormDrawer, useAppUtils } from '@gpustack/core-ui';
|
import { Input as CInput, FormDrawer, useAppUtils } from '@gpustack/core-ui';
|
||||||
@@ -77,6 +78,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
required
|
required
|
||||||
></CInput.Input>
|
></CInput.Input>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<PluginExtraFields name="CreateOrgScopeField" context={{ action }} />
|
||||||
{provider === ProviderValueMap.DigitalOcean && (
|
{provider === ProviderValueMap.DigitalOcean && (
|
||||||
<>
|
<>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { json2Yaml, yaml2Json } from '@/pages/backends/config';
|
import { json2Yaml, yaml2Json } from '@/pages/backends/config';
|
||||||
@@ -172,6 +173,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
trim={false}
|
trim={false}
|
||||||
></CInput.Input>
|
></CInput.Input>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<PluginExtraFields name="CreateOrgScopeField" context={{ action }} />
|
||||||
{provider === ProviderValueMap.DigitalOcean && (
|
{provider === ProviderValueMap.DigitalOcean && (
|
||||||
<CloudProvider
|
<CloudProvider
|
||||||
provider={provider}
|
provider={provider}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import {
|
import {
|
||||||
Input as CInput,
|
Input as CInput,
|
||||||
@@ -57,6 +58,7 @@ const Basic: React.FC<{
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<PluginExtraFields name="CreateOrgScopeField" context={{ action }} />
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name={['config', 'type']}
|
name={['config', 'type']}
|
||||||
rules={[
|
rules={[
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import CategorySelect from '@/pages/_components/category-select';
|
import CategorySelect from '@/pages/_components/category-select';
|
||||||
import DocLink from '@/pages/_components/doc-link';
|
import DocLink from '@/pages/_components/doc-link';
|
||||||
import { categoryOptions } from '@/pages/llmodels/config';
|
import { categoryOptions } from '@/pages/llmodels/config';
|
||||||
import { CheckboxField, Input as CInput, useAppUtils } from '@gpustack/core-ui';
|
import { CheckboxField, Input as CInput, useAppUtils } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
|
import { useContext } from 'react';
|
||||||
import { genericReferLink } from '../config';
|
import { genericReferLink } from '../config';
|
||||||
|
import FormContext from '../config/form-context';
|
||||||
import { FormData } from '../config/types';
|
import { FormData } from '../config/types';
|
||||||
|
|
||||||
const Basic = () => {
|
const Basic = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const form = Form.useFormInstance<FormData>();
|
const form = Form.useFormInstance<FormData>();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
|
const { action } = useContext(FormContext);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -28,6 +32,7 @@ const Basic = () => {
|
|||||||
label={intl.formatMessage({ id: 'common.table.name' })}
|
label={intl.formatMessage({ id: 'common.table.name' })}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<PluginExtraFields name="CreateOrgScopeField" context={{ action }} />
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="categories"
|
name="categories"
|
||||||
normalize={(value) => (value ? [value] : [])}
|
normalize={(value) => (value ? [value] : [])}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// columns.ts
|
// columns.ts
|
||||||
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import { tableSorter } from '@/config/settings';
|
import { tableSorter } from '@/config/settings';
|
||||||
import {
|
import {
|
||||||
AutoTooltip,
|
AutoTooltip,
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl, useModel } from '@umijs/max';
|
import { useIntl, useModel } from '@umijs/max';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { Tag } from 'antd';
|
import { Space, Tag } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@@ -197,10 +198,19 @@ const useUsersColumns = ({
|
|||||||
dataIndex: 'operation',
|
dataIndex: 'operation',
|
||||||
span: 3,
|
span: 3,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<DropdownButtons
|
// Plugin slot for per-row user actions. If no plugin is
|
||||||
items={setActions(record)}
|
// registered, `PluginExtraFields` renders nothing and the
|
||||||
onSelect={(val) => handleSelect(val, record)}
|
// cell looks exactly as before.
|
||||||
/>
|
<Space size={4}>
|
||||||
|
<DropdownButtons
|
||||||
|
items={setActions(record)}
|
||||||
|
onSelect={(val) => handleSelect(val, record)}
|
||||||
|
/>
|
||||||
|
<PluginExtraFields
|
||||||
|
name="UserRowActions"
|
||||||
|
context={{ user: record }}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { history, RequestConfig } from '@umijs/max';
|
|||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { DEFAULT_ENTER_PAGE } from './config/settings';
|
import { DEFAULT_ENTER_PAGE } from './config/settings';
|
||||||
import ErrorMessageContent from './pages/_components/error-message-content';
|
import ErrorMessageContent from './pages/_components/error-message-content';
|
||||||
|
import { extraRequestInterceptors } from './request.extensions';
|
||||||
|
|
||||||
// these APIs do not via the GPUSTACK_API_BASE_URL
|
// these APIs do not via the GPUSTACK_API_BASE_URL
|
||||||
const NoBaseURLAPIs = ['/auth', '/v1', '/version', '/proxy', '/update'];
|
const NoBaseURLAPIs = ['/auth', '/v1', '/version', '/proxy', '/update'];
|
||||||
@@ -43,7 +44,10 @@ export const requestConfig: RequestConfig = {
|
|||||||
return { url, options };
|
return { url, options };
|
||||||
}
|
}
|
||||||
return { url, options };
|
return { url, options };
|
||||||
}
|
},
|
||||||
|
// Build-time tooling can plug additional interceptors via
|
||||||
|
// `request.extensions.ts`. Default is an empty list.
|
||||||
|
...extraRequestInterceptors
|
||||||
],
|
],
|
||||||
responseInterceptors: [
|
responseInterceptors: [
|
||||||
(response) => {
|
(response) => {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Identity hook for build-time request-interceptor extensions. Tooling
|
||||||
|
// may overwrite this file to inject extra interceptors (e.g. context
|
||||||
|
// headers); the original is restored on cleanup. Mirrors
|
||||||
|
// `src/access.extensions.ts` and `config/routes.extensions.ts`.
|
||||||
|
//
|
||||||
|
// Each interceptor follows the umi/max signature: it receives
|
||||||
|
// `(url, options)` and returns `{ url, options }` — typed loosely
|
||||||
|
// here so this file has no dependency on the framework's exact
|
||||||
|
// option types.
|
||||||
|
export type RequestInterceptor = (
|
||||||
|
url: string,
|
||||||
|
options: Record<string, any>
|
||||||
|
) => { url: string; options: Record<string, any> };
|
||||||
|
|
||||||
|
export const extraRequestInterceptors: RequestInterceptor[] = [];
|
||||||
Reference in New Issue
Block a user