fix: using select handler for provider hint

This commit is contained in:
jialin
2026-05-27 17:10:15 +08:00
committed by jialin
parent 11cf1329c8
commit 3497590d2e
3 changed files with 45 additions and 37 deletions
+20 -14
View File
@@ -5,7 +5,13 @@ import { ColumnWrapper } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
createCluster, createCluster,
@@ -55,9 +61,6 @@ const MainWrapper = styled.div`
const ClusterCreate: React.FC<{ const ClusterCreate: React.FC<{
action: PageActionType; action: PageActionType;
// Preselect a provider and skip the provider-catalog step. Set by
// empty-state CTAs that already know which kind of cluster the user
// is heading for (e.g. GPU Service's "Add a Kubernetes Cluster").
providerHint?: string; providerHint?: string;
setCurrentTitle?: (title: string) => void; setCurrentTitle?: (title: string) => void;
onClose?: () => void; onClose?: () => void;
@@ -68,9 +71,7 @@ const ClusterCreate: React.FC<{
const [credentialList, setCredentialList] = useState< const [credentialList, setCredentialList] = useState<
Global.BaseOption<number, { provider: ProviderType }>[] Global.BaseOption<number, { provider: ProviderType }>[]
>([]); >([]);
// When the caller already picked a provider for us, start one step const [currentStep, setCurrentStep] = useState<number>(0);
// in — provider catalog is step 0; configure is step 1.
const [currentStep, setCurrentStep] = useState<number>(providerHint ? 1 : 0);
const [registrationInfo, setRegistrationInfo] = useState<{ const [registrationInfo, setRegistrationInfo] = useState<{
token: string; token: string;
image: string; image: string;
@@ -84,7 +85,7 @@ const ClusterCreate: React.FC<{
cluster_id: 0 cluster_id: 0
}); });
const [extraData, setExtraData] = useState<ClusterFormData>({ const [extraData, setExtraData] = useState<ClusterFormData>({
provider: (providerHint as ProviderType) ?? ProviderValueMap.Docker provider: ProviderValueMap.Docker
} as ClusterFormData); } as ClusterFormData);
const [formValues, setFormValues] = useState<Record<string, any>>({}); const [formValues, setFormValues] = useState<Record<string, any>>({});
const [submitLoading, setSubmitLoading] = useState<boolean>(false); const [submitLoading, setSubmitLoading] = useState<boolean>(false);
@@ -217,6 +218,12 @@ const ClusterCreate: React.FC<{
fetchData(); fetchData();
}, []); }, []);
useLayoutEffect(() => {
if (providerHint) {
handleSelectProvider(providerHint);
}
}, [providerHint]);
const renderForms = () => { const renderForms = () => {
const step = steps[currentStep]; const step = steps[currentStep];
const formKeys = step?.showForms || []; const formKeys = step?.showForms || [];
@@ -331,7 +338,11 @@ const ClusterCreate: React.FC<{
<MainWrapper> <MainWrapper>
{!isAddWorkerStep && ( {!isAddWorkerStep && (
<StepWrapper> <StepWrapper>
<ClusterSteps steps={steps} currentStep={currentStep}></ClusterSteps> <ClusterSteps
steps={steps}
currentStep={currentStep}
selectedProvider={extraData.provider}
></ClusterSteps>
</StepWrapper> </StepWrapper>
)} )}
<ColumnWrapper <ColumnWrapper
@@ -357,11 +368,6 @@ const ClusterCreate: React.FC<{
> >
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
{currentStep === 0 && ( {currentStep === 0 && (
// Catalog belongs to step 0 only. The previous gate used
// ``startStep`` which is 1 when ``providerHint`` skips the
// catalog — that wrongly re-rendered it on top of the
// configure form for entry points like "Add a Kubernetes
// Cluster".
<ProviderCatalog <ProviderCatalog
cols={2} cols={2}
dataList={providerList} dataList={providerList}
@@ -1,13 +1,18 @@
import { Steps } from 'antd'; import { Steps, Typography } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ProviderType } from '../config';
// `description` is intentionally omitted: the upstream step list ships const { Text } = Typography;
// hardcoded English copy that isn't translated. Keeping it would cause the
// step to render both the localized title and the English description side const ANTD_STEP_KEYS = [
// by side. Same reason we don't surface `subTitle`. 'title',
const ANTD_STEP_KEYS = ['title', 'icon', 'status', 'disabled'] as const; 'icon',
'status',
'disabled',
'subTitle'
] as const;
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
@@ -31,6 +36,11 @@ const Box = styled.div`
.ant-steps-item-rail-wait { .ant-steps-item-rail-wait {
--steps-item-solid-line-color: var(--ant-color-split); --steps-item-solid-line-color: var(--ant-color-split);
} }
.ant-steps-item-subtitle {
margin-left: 6px;
font-size: 13px;
color: var(--ant-color-text-tertiary);
}
&:not(.ant-steps-panel) { &:not(.ant-steps-panel) {
.ant-steps-item-finish { .ant-steps-item-finish {
--steps-item-icon-bg-color: var(--ant-color-primary); --steps-item-icon-bg-color: var(--ant-color-primary);
@@ -43,17 +53,18 @@ const ClusterSteps: React.FC<{
currentStep: number; currentStep: number;
onChange?: (step: number) => void; onChange?: (step: number) => void;
steps: any[]; steps: any[];
selectedProvider?: ProviderType;
}> = (props) => { }> = (props) => {
const { steps, currentStep = 0, onChange } = props; const { steps, currentStep = 0, onChange } = props;
// Pick only props antd's Step accepts — the upstream step objects carry
// custom keys (showModules/showForms/showButtons/...) that would otherwise
// be forwarded to the DOM and trigger "React does not recognize the X
// prop on a DOM element" warnings. _.pick keeps missing keys missing
// (rather than explicitly `undefined`) so antd's defaults still kick in.
const visibleSteps = steps const visibleSteps = steps
.filter((step) => !step.hideInSteps) .filter((step) => !step.hideInSteps)
.map((step) => _.pick(step, ANTD_STEP_KEYS)); .map((step, index) => {
return {
..._.pick(step, ANTD_STEP_KEYS),
subTitle: index === 0 ? <span>[{props.selectedProvider}]</span> : ''
};
});
const styles: Record<string, any> = { const styles: Record<string, any> = {
root: { root: {
+2 -11
View File
@@ -97,14 +97,10 @@ const GPUService: React.FC = () => {
fetchClusterList({ page: -1 }); fetchClusterList({ page: -1 });
}, []); }, []);
// GPU Service today is Kubernetes-only — Docker / cloud clusters const hasK8sCluster = useMemo(
// can't host the CRDs. Filter so the page reflects scheduling () => clusterList.some((c) => c.provider === ProviderValueMap.Kubernetes),
// reality even when the caller owns non-K8s clusters.
const k8sClusterList = useMemo(
() => clusterList.filter((c) => c.provider === ProviderValueMap.Kubernetes),
[clusterList] [clusterList]
); );
const hasK8sCluster = k8sClusterList.length > 0;
const handleModalOk = async (data: FormData) => { const handleModalOk = async (data: FormData) => {
try { try {
@@ -157,11 +153,6 @@ const GPUService: React.FC = () => {
const renderEmpty = (type?: string) => { const renderEmpty = (type?: string) => {
if (type !== 'Table') return; if (type !== 'Table') return;
// No K8s cluster the caller can schedule on — replace the "no
// instances" empty state with a cluster-bootstrap prompt. The
// "Add cluster" CTA is reserved for callers who can actually
// create one (platform admin / Org owner); members see the
// explanation without a misleading button.
if (!clusterLoading && !hasK8sCluster) { if (!clusterLoading && !hasK8sCluster) {
return ( return (
<NoResult <NoResult