diff --git a/src/pages/benchmark/forms/basic.tsx b/src/pages/benchmark/forms/basic.tsx index 32c4024d..c8222b5a 100644 --- a/src/pages/benchmark/forms/basic.tsx +++ b/src/pages/benchmark/forms/basic.tsx @@ -1,5 +1,7 @@ +import PluginExtraFields from '@/components/plugin-extra-fields'; import { modelNameReg, PageAction } from '@/config'; import { ClusterStatusValueMap } from '@/pages/cluster-management/config'; +import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list'; import { useBenchmarkTargetInstance } from '@/pages/llmodels/hooks/use-run-benchmark'; import { Input as CInput, @@ -8,7 +10,7 @@ import { } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useFormContext } from '../config/form-context'; import { FormData } from '../config/types'; import ModelInstanceForm from './model-instance'; @@ -20,31 +22,70 @@ const BasicForm: React.FC = () => { const { action, open, clusterList } = useFormContext(); const { benchmarkTargetInstance } = useBenchmarkTargetInstance(); + // `organization_id` is owned by the create-scope picker slot (admin "All" + // view). When a platform admin targets an org (and we're not pre-filling + // from a launched instance), fetch *that org's* clusters directly — the + // request header carries the chosen org — rather than filtering the + // page-level list, which is fetched once and may not include the org's + // clusters. The benchmark's owner is derived from the chosen cluster. + const scopeOrgId = Form.useWatch('organization_id', form); + const orgScoped = scopeOrgId != null && !benchmarkTargetInstance.cluster_id; + const { + clusterList: scopedClusterList, + fetchClusterList: fetchScopedClusters + } = useQueryClusterList(); + // The org-scoped fetch returns clusters *visible* to the org — its own plus + // any granted via cluster_access (and the platform principal can see a lot). + // A benchmark's owner is the chosen cluster's owner, so keep only clusters + // actually owned by the selected org. The owner filter also keeps this + // correct if the request header isn't applied (fetch falls back to all). + const effectiveClusterList = useMemo(() => { + if (!orgScoped) { + return clusterList || []; + } + return (scopedClusterList || []).filter( + (item: any) => item.owner_principal_id === scopeOrgId + ); + }, [orgScoped, scopedClusterList, clusterList, scopeOrgId]); + useEffect(() => { + if (action === PageAction.CREATE && orgScoped) { + fetchScopedClusters({ page: -1 }); + } + }, [scopeOrgId, orgScoped, action]); + + useEffect(() => { + if (action !== PageAction.CREATE) { + return; + } + const clusterValue = (item: any) => item?.value ?? item?.id; const initClusterId = (list: any[]) => { // Find default cluster const defaultCluster = list?.find((item) => item.is_default); if (defaultCluster) { - return defaultCluster.id; + return clusterValue(defaultCluster); } - const cluster_id = - list?.find((item) => item.state === ClusterStatusValueMap.Ready)?.id || - list?.[0]?.id; - - return cluster_id; - }; - if ( - clusterList && - clusterList?.length > 0 && - action === PageAction.CREATE - ) { - form.setFieldValue( - 'cluster_id', - benchmarkTargetInstance.cluster_id || initClusterId(clusterList) + const readyCluster = list?.find( + (item) => item.state === ClusterStatusValueMap.Ready ); + return clusterValue(readyCluster) ?? clusterValue(list?.[0]); + }; + const current = form.getFieldValue('cluster_id'); + const stillValid = effectiveClusterList.some( + (item: any) => clusterValue(item) === current + ); + if (stillValid) { + return; } - }, [form, action, clusterList, benchmarkTargetInstance]); + // Re-pick within the (org-scoped) list. When the chosen org owns no + // clusters this resolves to undefined, clearing a stale cross-org cluster + // instead of leaving it selected. + form.setFieldValue( + 'cluster_id', + benchmarkTargetInstance.cluster_id || initClusterId(effectiveClusterList) + ); + }, [form, action, effectiveClusterList, benchmarkTargetInstance]); return ( <> @@ -67,6 +108,7 @@ const BasicForm: React.FC = () => { required > + name="cluster_id" rules={[ @@ -78,7 +120,7 @@ const BasicForm: React.FC = () => { > diff --git a/src/pages/benchmark/forms/model-instance.tsx b/src/pages/benchmark/forms/model-instance.tsx index 0ccf1fed..e928ca8a 100644 --- a/src/pages/benchmark/forms/model-instance.tsx +++ b/src/pages/benchmark/forms/model-instance.tsx @@ -10,7 +10,7 @@ import { useQueryModelList } from '@/pages/llmodels/services/use-query-model-lis import { Cascader as SealCascader, useAppUtils } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form, Tooltip } from 'antd'; -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { useFormContext } from '../config/form-context'; import { FormData } from '../config/types'; @@ -45,6 +45,11 @@ const ModelInstanceForm: React.FC = () => { const form = Form.useFormInstance(); const { getRuleMessage } = useAppUtils(); const { action, open } = useFormContext(); + // Owned by the create-scope picker slot (admin "All" view). The model list + // is tenant-scoped by the request header, so refetch it when the org + // changes so only the chosen org's models/instances are offered. + const scopeOrgId = Form.useWatch('organization_id', form); + const prevScopeRef = useRef(undefined); const [modelList, setModelList] = React.useState([]); const { loading: modelLoading, @@ -158,14 +163,29 @@ const ModelInstanceForm: React.FC = () => { useEffect(() => { if (open && action === PageAction.CREATE) { + // On a genuine org change, clear the stale (possibly cross-org) target + // so the refetched list re-selects within the new org. + if ( + prevScopeRef.current !== undefined && + prevScopeRef.current !== scopeOrgId + ) { + form.setFieldsValue({ + model_name: undefined, + model_id: undefined, + model_instance_name: undefined, + model_instance: undefined + }); + } + prevScopeRef.current = scopeOrgId; initModelInstance(); } if (!open) { + prevScopeRef.current = undefined; cancelModelRequest(); cancelInstanceRequest(); clearBenchmarkTargetInstance(); } - }, [open, benchmarkTargetInstance, action]); + }, [open, benchmarkTargetInstance, action, scopeOrgId]); return (