Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
207cef350f | ||
|
|
fa3fed8671 | ||
|
|
dc41b61d69 |
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user