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:
gitlawr
2026-05-08 18:29:52 +08:00
committed by jialin
parent 896b8ef326
commit 2889ba7c78
18 changed files with 211 additions and 67 deletions
+31 -21
View File
@@ -1,3 +1,4 @@
import PluginExtraFields from '@/components/plugin-extra-fields';
import {
AutoTooltip,
DropdownActions,
@@ -248,30 +249,39 @@ const BackendCard: React.FC<BackendCardProps> = ({
const source = data.is_built_in
? BackendSourceLabelMap[BackendSourceValueMap.BUILTIN] || ''
: BackendSourceLabelMap[data.backend_source] || '';
const ownerTag = (
<PluginExtraFields
name="BackendOwnerTag"
context={{ organizationId: data.organization_id }}
/>
);
if (!source) {
return null;
return ownerTag;
}
return (
<Tag
color={
TagColorMap[
data.is_built_in
? BackendSourceValueMap.BUILTIN
: data.backend_source
]
}
className="font-400"
variant="filled"
style={{
borderRadius: 'var(--ant-border-radius)',
margin: 0,
width: 'max-content'
}}
>
{intl.formatMessage({
id: source
})}
</Tag>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<Tag
color={
TagColorMap[
data.is_built_in
? BackendSourceValueMap.BUILTIN
: data.backend_source
]
}
className="font-400"
variant="filled"
style={{
borderRadius: 'var(--ant-border-radius)',
margin: 0,
width: 'max-content'
}}
>
{intl.formatMessage({
id: source
})}
</Tag>
{ownerTag}
</div>
);
};
+5 -1
View File
@@ -109,7 +109,11 @@ export const backendActions = [
icon: icons.DeleteOutlined,
locale: 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
}
];
+5
View File
@@ -1,3 +1,4 @@
import PluginExtraFields from '@/components/plugin-extra-fields';
import { PageAction } from '@/config';
import { backendOptionsMap } from '@/pages/llmodels/constants/backend-parameters';
import {
@@ -67,6 +68,10 @@ const BasicForm = () => {
required
></CInput.Input>
</Form.Item>
<PluginExtraFields
name="CreateOrgScopeField"
context={{ action, allowGlobal: true }}
/>
<Form.Item<FormData> hidden name="backend_source">
<CInput.Input></CInput.Input>
</Form.Item>
+9 -1
View File
@@ -166,7 +166,15 @@ const BackendList = () => {
}
// ================ 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({
content: 'backends.title',
operation: 'common.delete.single.confirm',