Compare commits

..
Author SHA1 Message Date
gitlawr 207cef350f refactor(models): drop redundant effect dep and cast
Address review: scopeOrgId is already folded into clusterOptions (its
useMemo dep), so the effect still fires on scope changes without listing
it; and the next == null guard narrows next to number, so the cast goes.
2026-07-07 16:32:26 +08:00
gitlawr fa3fed8671 fix(models): preselect the scoped default cluster on deploy
The deploy form's cluster select could open blank even when a valid
(default) cluster exists for the current scope. The open handler seeds
the cluster once, but that seed can be empty (options not loaded yet) or
point outside the resolved scope (the scope id and the option list both
settle after mount, and switching scope re-scopes the list), and nothing
re-picked afterwards — so the field stayed empty.

Make the form enforce the invariant directly: whenever the scope or the
available options change, if the current selection is empty or not among
the options, fall back to the scope's default cluster (then a Ready one,
then the first). A still-valid selection is left untouched.
2026-07-07 16:23:38 +08:00
jialinandjialin dc41b61d69 fix(deployments): display gpu allocated value in gpu selector 2026-07-06 17:48:22 +08:00
5 changed files with 56 additions and 54 deletions
@@ -77,7 +77,7 @@ export default function useAddResource(options?: { onCreated?: () => void }) {
}, [resourceAtom, loadingStatus]); }, [resourceAtom, loadingStatus]);
const contentInfo = useMemo(() => { const contentInfo = useMemo(() => {
if (!resourceCount.cluster_count) { if (!resourceCount?.cluster_count) {
return { return {
title: intl.formatMessage({ id: 'noresult.cluster.title' }), title: intl.formatMessage({ id: 'noresult.cluster.title' }),
subTitle: intl.formatMessage({ id: 'noresult.resources.cluster' }), subTitle: intl.formatMessage({ id: 'noresult.resources.cluster' }),
@@ -98,7 +98,7 @@ export default function useAddResource(options?: { onCreated?: () => void }) {
const handleCreate = () => { const handleCreate = () => {
setHideModalTemporarily(true); setHideModalTemporarily(true);
onCreated?.(); onCreated?.();
if (!resourceCount.cluster_count) { if (!resourceCount?.cluster_count) {
setClusterSession({ setClusterSession({
firstAddWorker: false, firstAddWorker: false,
firstAddCluster: true firstAddCluster: true
+26 -29
View File
@@ -1,6 +1,7 @@
import { convertFileSize } from '@/utils'; import { convertFileSize } from '@/utils';
import { AutoTooltip } from '@gpustack/core-ui'; import { AutoTooltip } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Flex } from 'antd';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import '../style/gpu-card.less'; import '../style/gpu-card.less';
@@ -19,12 +20,17 @@ const Header = styled.div`
width: 100%; width: 100%;
`; `;
const ItemInfo = styled.div` const Metric: React.FC<{ label: React.ReactNode; value: React.ReactNode }> = ({
display: flex; label,
flex-wrap: wrap; value
white-space: break-spaces; }) => (
font-size: 13px; <span style={{ lineHeight: 1.2 }}>
`; {label}:{' '}
<span className="font-500" style={{ color: 'var(--ant-color-text)' }}>
{value}
</span>
</span>
);
const Description = styled.div` const Description = styled.div`
display: flex; display: flex;
@@ -64,29 +70,20 @@ const GPUCard: React.FC<{
} }
description={ description={
info || ( info || (
<> <Flex wrap gap={8} style={{ fontSize: 13 }}>
<ItemInfo> <Metric
{intl.formatMessage({ id: 'resources.table.vram' })}( label={intl.formatMessage({ id: 'resources.table.total' })}
{intl.formatMessage({ id: 'resources.table.used' })}/ value={convertFileSize(data?.memory?.total || 0)}
{intl.formatMessage({ id: 'resources.table.total' })}):{' '} />
<span> <Metric
{convertFileSize( label={intl.formatMessage({ id: 'resources.table.used' })}
data?.memory?.used || data?.memory?.allocated || 0 value={convertFileSize(data?.memory?.used || 0)}
)}{' '} />
/ {convertFileSize(data?.memory?.total || 0)} <Metric
</span> label={intl.formatMessage({ id: 'resources.table.allocated' })}
</ItemInfo> value={convertFileSize(data?.memory?.allocated || 0)}
{/* <ItemInfo> />
<span> </Flex>
{intl.formatMessage({ id: 'resources.table.gpuutilization' })}
:{' '}
</span>
{data?.memory?.used
? _.round(data?.memory?.utilization_rate || 0, 2)
: _.round(data.memory?.allocated / data.memory?.total, 2) * 100}
%
</ItemInfo> */}
</>
) )
} }
></CardContainer> ></CardContainer>
+22 -23
View File
@@ -13,7 +13,7 @@ import {
} from '@gpustack/core-ui'; } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { DeployFormKeyMap, sourceOptions } from '../config'; import { DeployFormKeyMap, sourceOptions } from '../config';
import { useFormContext } from '../config/form-context'; import { useFormContext } from '../config/form-context';
@@ -24,6 +24,7 @@ import CustomBackend from './custom-backend';
import LocalPathSource from './local-path-source'; import LocalPathSource from './local-path-source';
import ModeField from './mode-field'; import ModeField from './mode-field';
import OnlineSource from './online-source'; import OnlineSource from './online-source';
const ClusterOption = styled.span` const ClusterOption = styled.span`
display: flex; display: flex;
padding: 8px 0; padding: 8px 0;
@@ -116,7 +117,6 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
// cluster dropdown to that org's own clusters — the backend derives the // cluster dropdown to that org's own clusters — the backend derives the
// deployment's owner from the chosen cluster, so this keeps them aligned. // deployment's owner from the chosen cluster, so this keeps them aligned.
const scopeOrgId = Form.useWatch('organization_id', form); const scopeOrgId = Form.useWatch('organization_id', form);
const prevScopeRef = useRef<number | null | undefined>(undefined);
const clusterOptions = useMemo(() => { const clusterOptions = useMemo(() => {
return clusterList return clusterList
@@ -139,37 +139,36 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
}); });
}, [clusterList, scopeOrgId]); }, [clusterList, scopeOrgId]);
// On a genuine org change (not the initial value — the modal's open // Keep the cluster selection consistent with the available (scoped)
// handler seeds the first cluster), drop a now-out-of-scope cluster and // options. The modal's open handler seeds a cluster, but that seed can be
// re-pick within the new org so GPU/backend options refetch for it. // empty (options not loaded yet) or point outside the current scope (the
// scope id and the option list both settle after mount, and an org switch
// re-scopes the list) — leaving the field blank even though a valid option
// exists. Whenever the scope or the options change, drop an invalid/empty
// selection and fall back to the scope's default cluster (then a Ready one,
// then the first) so GPU/backend options refetch for it. A selection that's
// still valid is left untouched, so a user's (or edit's) choice is kept.
useEffect(() => { useEffect(() => {
if (prevScopeRef.current === undefined) { if (!clusterOptions?.length) {
prevScopeRef.current = scopeOrgId;
return; return;
} }
if (prevScopeRef.current === scopeOrgId) {
return;
}
prevScopeRef.current = scopeOrgId;
const current = form.getFieldValue('cluster_id'); const current = form.getFieldValue('cluster_id');
const stillValid = clusterOptions?.some((c) => c.value === current); const stillValid = clusterOptions.some((c) => c.value === current);
if (stillValid) { if (current != null && stillValid) {
return; return;
} }
const next = const next =
clusterOptions?.find((c) => c.is_default)?.value ?? clusterOptions.find((c) => c.is_default)?.value ??
clusterOptions?.find((c) => c.state === ClusterStatusValueMap.Ready) clusterOptions.find((c) => c.state === ClusterStatusValueMap.Ready)
?.value ?? ?.value ??
clusterOptions?.[0]?.value ?? clusterOptions[0]?.value ??
null; null;
form.setFieldValue('cluster_id', next ?? null); if (next == null || next === current) {
if (next != null) { return;
handleClusterChange?.(next as number);
} }
// The prevScopeRef guard above makes this a no-op unless scopeOrgId form.setFieldValue('cluster_id', next);
// actually changed, so listing the other deps is safe (no extra runs). handleClusterChange?.(next);
}, [scopeOrgId, clusterOptions, form, handleClusterChange]); }, [clusterOptions, form, handleClusterChange]);
const clusterOptionRender = (option: any) => { const clusterOptionRender = (option: any) => {
const { data } = option; const { data } = option;
@@ -20,6 +20,7 @@ import {
import { useFormContext } from '../config/form-context'; import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import { backendOptionsMap } from '../constants/backend-parameters'; import { backendOptionsMap } from '../constants/backend-parameters';
import '../style/gpu-selector.less';
const InputWrapper = styled.div` const InputWrapper = styled.div`
padding: 8px 4px; padding: 8px 4px;
@@ -0,0 +1,5 @@
.cascader-popup-wrapper.gpu-selector {
.ant-cascader-menu-item-content {
padding-right: 0;
}
}