style: no resource modal

This commit is contained in:
jialin
2025-12-17 19:58:28 +08:00
parent 30536cb04a
commit e4bd753355
10 changed files with 218 additions and 123 deletions
+4
View File
@@ -792,3 +792,7 @@ body {
.ant-page-header .ant-page-header-heading-extra { .ant-page-header .ant-page-header-heading-extra {
margin-block: 0; margin-block: 0;
} }
.ant-pro-footer-bar {
padding-inline: 0 !important;
}
+1 -1
View File
@@ -32,7 +32,7 @@ export default {
'clusters.create.configBasic': '基本配置', 'clusters.create.configBasic': '基本配置',
'clusters.create.execCommand': '执行命令', 'clusters.create.execCommand': '执行命令',
'clusters.create.supportedGpu': '支持的 GPU', 'clusters.create.supportedGpu': '支持的 GPU',
'clusters.create.skipfornow': '暂时跳过', 'clusters.create.skipfornow': '稍后再说',
'clusters.create.noImages': '无可用的镜像', 'clusters.create.noImages': '无可用的镜像',
'clusters.create.noInstanceTypes': '无可用的实例类型', 'clusters.create.noInstanceTypes': '无可用的实例类型',
'clusters.create.noRegions': '无可用的区域', 'clusters.create.noRegions': '无可用的区域',
@@ -146,6 +146,7 @@ const AddWorkerSteps: React.FC<AddWorkerProps> = (props) => {
)} )}
{addedCount > 0 && ( {addedCount > 0 && (
<Alert <Alert
style={{ width: 'max-content' }}
message={intl.formatMessage( message={intl.formatMessage(
{ {
id: 'clusters.addworker.message.success' id: 'clusters.addworker.message.success'
@@ -11,10 +11,16 @@ import AddWorkerStep from './add-worker-step';
import { StepName } from './config'; import { StepName } from './config';
const Footer = styled.div` const Footer = styled.div`
position: relative;
display: flex; display: flex;
justify-content: space-between; gap: 12px;
justify-content: center;
align-items: center; align-items: center;
padding-top: 12px; padding-top: 12px;
.tips {
position: absolute;
left: 0px;
}
`; `;
type AddWorkerProps = { type AddWorkerProps = {
@@ -113,7 +119,7 @@ const AddWorker: React.FC<AddWorkerProps> = (props) => {
maxContentHeight={'max(calc(100vh - 200px), 600px)'} maxContentHeight={'max(calc(100vh - 200px), 600px)'}
footer={ footer={
<Footer> <Footer>
<span> <span className="tips">
{addedCount > 0 && ( {addedCount > 0 && (
<Alert <Alert
message={intl.formatMessage( message={intl.formatMessage(
@@ -23,9 +23,11 @@ const SwitchSetting: React.FC<{
tips?: React.ReactNode; tips?: React.ReactNode;
errorMessage?: React.ReactNode; errorMessage?: React.ReactNode;
extra?: React.ReactNode; extra?: React.ReactNode;
showSwitch?: boolean;
onInputChange?: (value: string) => void; onInputChange?: (value: string) => void;
onChange: (checked: boolean) => void; onChange: (checked: boolean) => void;
}> = ({ }> = ({
showSwitch = true,
label, label,
checked, checked,
onChange, onChange,
@@ -42,7 +44,7 @@ const SwitchSetting: React.FC<{
<span style={{ color: 'var(--ant-color-text)', fontWeight: 500 }}> <span style={{ color: 'var(--ant-color-text)', fontWeight: 500 }}>
<span>{label}</span> <span>{label}</span>
</span> </span>
<Switch checked={checked} onChange={onChange}></Switch> {showSwitch && <Switch checked={checked} onChange={onChange}></Switch>}
</ButtonWrapper> </ButtonWrapper>
{tips && ( {tips && (
<Tips <Tips
@@ -97,12 +99,12 @@ const SpecifyArguments = () => {
}; };
const containerNameConfig = summary.get('containerNameConfig') || { const containerNameConfig = summary.get('containerNameConfig') || {
enable: false, enable: true,
name: '' name: ''
}; };
const gpustackDataVolumeConfig = summary.get('gpustackDataVolumeConfig') || { const gpustackDataVolumeConfig = summary.get('gpustackDataVolumeConfig') || {
enable: false, enable: true,
path: '' path: ''
}; };
@@ -179,12 +181,12 @@ const SpecifyArguments = () => {
}); });
updateField('containerNameConfig', { updateField('containerNameConfig', {
enable: false, enable: true,
name: '' name: ''
}); });
updateField('gpustackDataVolumeConfig', { updateField('gpustackDataVolumeConfig', {
enable: false, enable: true,
path: '' path: ''
}); });
}, []); }, []);
@@ -272,56 +274,23 @@ const SpecifyArguments = () => {
) )
} }
></SwitchSetting> ></SwitchSetting>
{/* container name config */} {/* cache directory config */}
<SwitchSetting <SwitchSetting
label={intl.formatMessage({ id: 'clusters.addworker.containerName' })} label={intl.formatMessage({ id: 'clusters.addworker.cacheVolume' })}
tips={intl.formatMessage({ tips={intl.formatMessage({
id: 'clusters.addworker.containerName.tips' id: 'clusters.addworker.cacheVolume.tips'
})} })}
placeholder={intl.formatMessage( placeholder={intl.formatMessage({
{ id: 'clusters.addworker.cacheVolume.holder'
id: 'common.help.default' })}
}, checked={cacheDirConfig.enable}
{ content: 'gpustack-worker' } value={cacheDirConfig.path}
)}
checked={containerNameConfig.enable}
value={containerNameConfig.name}
onChange={(checked) => onChange={(checked) =>
updateField('containerNameConfig', { setCacheDirConfig({ ...cacheDirConfig, enable: checked })
...containerNameConfig,
enable: checked
})
} }
onInputChange={(value) => onInputChange={(value) =>
updateField('containerNameConfig', { setCacheDirConfig({
...containerNameConfig, ...cacheDirConfig,
name: value
})
}
></SwitchSetting>
{/* gpustack data volume config */}
<SwitchSetting
label={intl.formatMessage({ id: 'clusters.addworker.dataVolume' })}
tips={intl.formatMessage({
id: 'clusters.addworker.dataVolume.tips'
})}
placeholder={intl.formatMessage(
{
id: 'common.help.default'
},
{ content: 'gpustack-data' }
)}
checked={gpustackDataVolumeConfig.enable}
value={gpustackDataVolumeConfig.path}
onChange={(checked) =>
updateField('gpustackDataVolumeConfig', {
...gpustackDataVolumeConfig,
enable: checked
})
}
onInputChange={(value) =>
updateField('gpustackDataVolumeConfig', {
...gpustackDataVolumeConfig,
path: value path: value
}) })
} }
@@ -347,27 +316,57 @@ const SpecifyArguments = () => {
}) })
} }
></SwitchSetting> ></SwitchSetting>
{/* cache directory config */}
{/* gpustack data volume config */}
<SwitchSetting <SwitchSetting
label={intl.formatMessage({ id: 'clusters.addworker.cacheVolume' })} showSwitch={false}
tips={intl.formatMessage({ label={intl.formatMessage({ id: 'clusters.addworker.dataVolume' })}
id: 'clusters.addworker.cacheVolume.tips' placeholder={intl.formatMessage(
})} {
placeholder={intl.formatMessage({ id: 'common.help.default'
id: 'clusters.addworker.cacheVolume.holder' },
})} { content: 'gpustack-data' }
checked={cacheDirConfig.enable} )}
value={cacheDirConfig.path} checked={gpustackDataVolumeConfig.enable}
value={gpustackDataVolumeConfig.path}
onChange={(checked) => onChange={(checked) =>
setCacheDirConfig({ ...cacheDirConfig, enable: checked }) updateField('gpustackDataVolumeConfig', {
...gpustackDataVolumeConfig,
enable: checked
})
} }
onInputChange={(value) => onInputChange={(value) =>
setCacheDirConfig({ updateField('gpustackDataVolumeConfig', {
...cacheDirConfig, ...gpustackDataVolumeConfig,
path: value path: value
}) })
} }
></SwitchSetting> ></SwitchSetting>
{/* container name config */}
<SwitchSetting
showSwitch={false}
label={intl.formatMessage({ id: 'clusters.addworker.containerName' })}
placeholder={intl.formatMessage(
{
id: 'common.help.default'
},
{ content: 'gpustack-worker' }
)}
checked={containerNameConfig.enable}
value={containerNameConfig.name}
onChange={(checked) =>
updateField('containerNameConfig', {
...containerNameConfig,
enable: checked
})
}
onInputChange={(value) =>
updateField('containerNameConfig', {
...containerNameConfig,
name: value
})
}
></SwitchSetting>
</div> </div>
</StepCollapse> </StepCollapse>
); );
@@ -99,16 +99,6 @@ const SummaryData: React.FC = () => {
path: '' path: ''
}; };
const containerNameConfig = summary.get('containerNameConfig') || {
enable: false,
name: ''
};
const gpustackDataVolumeConfig = summary.get('gpustackDataVolumeConfig') || {
enable: false,
path: ''
};
return ( return (
<ConfigWrapper> <ConfigWrapper>
<Title> <Title>
@@ -143,18 +133,6 @@ const SummaryData: React.FC = () => {
required={true} required={true}
></DataItem> ></DataItem>
<DataItem
label={intl.formatMessage({ id: 'clusters.addworker.containerName' })}
enable={containerNameConfig.enable}
value={containerNameConfig.name}
></DataItem>
<DataItem
label={intl.formatMessage({ id: 'clusters.addworker.dataVolume' })}
enable={gpustackDataVolumeConfig.enable}
value={gpustackDataVolumeConfig.path}
></DataItem>
<DataItem <DataItem
label={intl.formatMessage({ id: 'clusters.addworker.extraVolume' })} label={intl.formatMessage({ id: 'clusters.addworker.extraVolume' })}
enable={modelDirConfig.enable} enable={modelDirConfig.enable}
@@ -9,6 +9,7 @@ const Title = styled.span`
justify-content: flex-end; justify-content: flex-end;
font-weight: 700; font-weight: 700;
font-size: 16px; font-size: 16px;
padding-inline: 24px;
.text { .text {
font-size: 20px; font-size: 20px;
} }
@@ -0,0 +1,135 @@
import IconFont from '@/components/icon-font';
import ScrollerModal from '@/components/scroller-modal/index';
import useClusterList from '@/pages/cluster-management/hooks/use-cluster-list';
import { useIntl, useNavigate } from '@umijs/max';
import { Button } from 'antd';
import { useMemo, useState } from 'react';
import styled from 'styled-components';
const Content = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 32px auto 0px;
.title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
}
.tips,
.sub-title {
color: var(--ant-color-text-tertiary);
}
.icon {
font-size: 32px;
width: 64px;
height: 64px;
border-radius: 32px;
margin-bottom: 16px;
display: flex;
justify-content: center;
align-items: center;
color: var(--ant-color-primary);
background-color: var(--ant-color-primary-bg);
}
.desc {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.btn-wrapper {
width: 100%;
padding: 0 16px;
margin-top: 32px;
display: flex;
gap: 16px;
justify-content: center;
align-items: center;
}
`;
export default function useAddResource() {
const intl = useIntl();
const navigate = useNavigate();
const { fetchAll, clusterList, workerList, clustersAtom, workersAtom } =
useClusterList();
const [hiddenModal, setHiddenModal] = useState(false);
const [loadingStatus, setLoadingStatus] = useState({
loading: false,
loadend: false
});
const isNoResource = useMemo(() => {
const noResource = workerList.length === 0 || clusterList.length === 0;
return noResource && !loadingStatus.loading && loadingStatus.loadend;
}, [workersAtom.length, clustersAtom.length, loadingStatus]);
const contentInfo = useMemo(() => {
if (clusterList.length === 0) {
return {
title: intl.formatMessage({ id: 'noresult.cluster.title' }),
subTitle: intl.formatMessage({ id: 'noresult.resources.cluster' }),
btnText: intl.formatMessage({ id: 'noresult.resources.gotocluster' })
};
}
return {
title: intl.formatMessage({ id: 'noresult.workers.title' }),
subTitle: intl.formatMessage({ id: 'noresult.resources.worker' }),
btnText: intl.formatMessage({ id: 'noresult.workers.button.add' })
};
}, [clusterList.length, workerList.length, intl]);
const open: boolean = useMemo(() => {
return isNoResource && !hiddenModal;
}, [isNoResource, hiddenModal]);
const handleCreate = () => {};
const handleCancel = () => {
setHiddenModal(true);
};
const Modal = (
<ScrollerModal
open={open}
footer={null}
maskClosable={false}
closeIcon={null}
onCancel={handleCancel}
>
<Content>
<div className="icon">
<IconFont type="icon-dashboard" />
</div>
<div className="title">{contentInfo.title}</div>
<div className="desc">
<div className="sub-title">{contentInfo.subTitle}</div>
</div>
<div className="btn-wrapper">
<Button onClick={handleCancel} type="default" style={{ flex: 1 }}>
{intl.formatMessage({ id: 'clusters.create.skipfornow' })}
</Button>
<Button onClick={handleCreate} type="primary" style={{ flex: 1 }}>
{contentInfo.btnText}
</Button>
</div>
</Content>
</ScrollerModal>
);
return {
open,
contentInfo,
loadingStatus,
clusterList,
Modal,
handleCreate,
setLoadingStatus,
handleCancel,
fetchAll
};
}
+5 -35
View File
@@ -1,6 +1,3 @@
import useClusterList from '@/pages/cluster-management/hooks/use-cluster-list';
import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Spin } from 'antd'; import { Spin } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -9,36 +6,12 @@ import { queryDashboardData } from './apis';
import DashboardInner from './components/dahboard-inner'; import DashboardInner from './components/dahboard-inner';
import DashboardContext from './config/dashboard-context'; import DashboardContext from './config/dashboard-context';
import { DashboardProps } from './config/types'; import { DashboardProps } from './config/types';
import useAddResource from './hooks/use-add-resource';
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const intl = useIntl(); const { setLoadingStatus, fetchAll, Modal, loadingStatus, clusterList } =
const [loadingStatus, setLoadingStatus] = useState({ useAddResource();
loading: false,
loadend: false
});
const [data, setData] = useState<DashboardProps>({} as DashboardProps); const [data, setData] = useState<DashboardProps>({} as DashboardProps);
const { fetchAll, clusterList, workerList, clustersAtom, workersAtom } =
useClusterList();
const { noResourceResult } = useNoResourceResult({
loadend: true,
loading: false,
dataSource: clusterList.length > 0 ? workerList : [],
queryParams: {},
iconType: 'icon-dashboard',
title:
clusterList.length > 0
? intl.formatMessage({ id: 'noresult.workers.title' })
: intl.formatMessage({ id: 'noresult.cluster.title' }),
noClusters: !clusterList.length,
noWorkers: workerList.length === 0 && clusterList.length > 0,
defaultContent: {
subTitle: intl.formatMessage({ id: 'noresult.workers.subTitle' }),
noFoundText: intl.formatMessage({ id: 'noresult.workers.nofound' }),
buttonText: intl.formatMessage({ id: 'noresult.workers.button.add' }),
onClick: () => {}
}
});
const fetchDashboardData = useMemoizedFn(async () => { const fetchDashboardData = useMemoizedFn(async () => {
try { try {
@@ -80,13 +53,10 @@ const Dashboard: React.FC = () => {
> >
<PageBox> <PageBox>
<Spin spinning={loadingStatus.loading} style={{ minHeight: 300 }}> <Spin spinning={loadingStatus.loading} style={{ minHeight: 300 }}>
{workersAtom.length > 0 && clustersAtom.length > 0 ? ( <DashboardInner />
<DashboardInner />
) : loadingStatus.loading || !loadingStatus.loadend ? null : (
noResourceResult
)}
</Spin> </Spin>
</PageBox> </PageBox>
{Modal}
</DashboardContext.Provider> </DashboardContext.Provider>
); );
}; };
@@ -111,7 +111,8 @@ const useNoResourceResult = (props: {
></NoResult> ></NoResult>
); );
return { return {
noResourceResult noResourceResult,
handleCreate: handleClick
}; };
}; };