style: add worker ux
This commit is contained in:
@@ -17,6 +17,11 @@ export const regionInstanceTypeListAtom = atom<
|
||||
{
|
||||
label: string;
|
||||
value: string;
|
||||
description: string;
|
||||
specInfo: Record<string, any>;
|
||||
vendor: string;
|
||||
available: boolean;
|
||||
regions: string[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
@@ -24,6 +29,11 @@ export const regionOSImageListAtom = atom<
|
||||
{
|
||||
label: string;
|
||||
value: string;
|
||||
name: string;
|
||||
description: string;
|
||||
vendor: string;
|
||||
specInfo: Record<string, any>;
|
||||
regions: string[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
@@ -42,3 +52,5 @@ export const allRegionInstanceTypeListAtom = atom<
|
||||
regions: string[];
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
export const fromClusterCreationAtom = atom(false);
|
||||
|
||||
@@ -23,7 +23,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
||||
legendOptions,
|
||||
gridOptions,
|
||||
titleOptions,
|
||||
showArea
|
||||
showArea = 0.25
|
||||
} = props;
|
||||
const {
|
||||
grid,
|
||||
@@ -87,7 +87,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
|
||||
const data = _.map(seriesData, (item: any) => {
|
||||
const colors = genColors({
|
||||
color: item.color,
|
||||
alpha1: 0.5,
|
||||
alpha1: 0.25,
|
||||
alpha2: 0.1
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
|
||||
// import './iconfont/iconfont.js';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_5jrvj6qs39.js'
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_84hy7h371ty.js'
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { AutoComplete, Form, Spin } from 'antd';
|
||||
import type { AutoCompleteProps } from 'antd/lib';
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { SealFormItemProps } from './types';
|
||||
import Wrapper from './wrapper';
|
||||
import AutoCompleteLabel from './wrapper/auto-complete-label';
|
||||
import SelectWrapper from './wrapper/select';
|
||||
|
||||
const SealAutoComplete: React.FC<
|
||||
@@ -24,6 +26,7 @@ const SealAutoComplete: React.FC<
|
||||
...rest
|
||||
} = props;
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
const [_isFocus_, _setIsFocus_] = useState(false);
|
||||
const inputRef = useRef<any>(null);
|
||||
let status = '';
|
||||
if (isInFormItems) {
|
||||
@@ -41,6 +44,7 @@ const SealAutoComplete: React.FC<
|
||||
if (!props.disabled && !isFocus) {
|
||||
inputRef.current?.focus?.();
|
||||
setIsFocus(true);
|
||||
_setIsFocus_(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,17 +53,20 @@ const SealAutoComplete: React.FC<
|
||||
if (trim) {
|
||||
value = value?.trim?.();
|
||||
}
|
||||
_setIsFocus_(false);
|
||||
props.onChange?.(value, option);
|
||||
};
|
||||
|
||||
const handleOnFocus = (e: any) => {
|
||||
setIsFocus(true);
|
||||
_setIsFocus_(true);
|
||||
props.onFocus?.(e);
|
||||
};
|
||||
|
||||
const handleOnBlur = (e: any) => {
|
||||
if (!props.value) {
|
||||
setIsFocus(false);
|
||||
_setIsFocus_(false);
|
||||
}
|
||||
e.target.value = e.target.value?.trim?.();
|
||||
props.onBlur?.(e);
|
||||
@@ -79,6 +86,39 @@ const SealAutoComplete: React.FC<
|
||||
return addAfter;
|
||||
};
|
||||
|
||||
const renderAutoCompleteLabel = () => {
|
||||
console.log(
|
||||
'renderAutoCompleteLabel===',
|
||||
props.value,
|
||||
props.showSearch,
|
||||
_isFocus_,
|
||||
isFocus
|
||||
);
|
||||
if (!props.showSearch || _isFocus_) {
|
||||
return null;
|
||||
}
|
||||
let selectItem: {
|
||||
label: React.ReactNode;
|
||||
value: string | number;
|
||||
} = props.options?.find((item: any) => item.value === props.value) as {
|
||||
label: React.ReactNode;
|
||||
value: string | number;
|
||||
};
|
||||
|
||||
if (!selectItem) {
|
||||
selectItem = { label: props.value, value: props.value };
|
||||
}
|
||||
return (
|
||||
<AutoCompleteLabel
|
||||
className={classNames({
|
||||
disabled: props.disabled
|
||||
})}
|
||||
>
|
||||
{props.labelRender ? props.labelRender(selectItem) : selectItem.label}
|
||||
</AutoCompleteLabel>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectWrapper style={style}>
|
||||
<Wrapper
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import styled from 'styled-components';
|
||||
import { BGCOLOR, INPUT_INNER_PADDING } from '../config';
|
||||
|
||||
const AutoCompleteLabel = styled.span`
|
||||
position: absolute;
|
||||
left: ${INPUT_INNER_PADDING}px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
top: 26px;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s;
|
||||
background-color: var(--ant-color-bg-container);
|
||||
z-index: 10;
|
||||
&.disabled {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
&.isfoucs-has-value {
|
||||
// display: none;
|
||||
z-index: -1;
|
||||
// top: 9px;
|
||||
// font-size: 12px;
|
||||
// color: var(--ant-color-text);
|
||||
// background-color: ${BGCOLOR};
|
||||
// padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default AutoCompleteLabel;
|
||||
@@ -103,7 +103,7 @@ const SelectWrapper = styled.div`
|
||||
}
|
||||
&.ant-select-auto-complete {
|
||||
.ant-select-selection-search {
|
||||
inset-inline-start: ${INPUT_INNER_PADDING}px;
|
||||
padding-inline-start: ${INPUT_INNER_PADDING}px;
|
||||
}
|
||||
}
|
||||
&.ant-select-multiple.ant-cascader .ant-select-selection-search {
|
||||
|
||||
@@ -16,7 +16,9 @@ interface CardProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const CardWrapper = styled.div`
|
||||
const CardWrapper = styled.div.attrs({
|
||||
className: 'template-card-wrapper'
|
||||
})`
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
padding: 22px;
|
||||
@@ -43,37 +45,44 @@ const CardWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
}
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
background-color: var(--ant-color-fill-quaternary);
|
||||
border-style: dashed;
|
||||
}
|
||||
`;
|
||||
|
||||
const CardContent = styled.div`
|
||||
const CardContent = styled.div.attrs({
|
||||
className: 'template-card-content'
|
||||
})`
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
`;
|
||||
|
||||
const Inner = styled.div`
|
||||
const Inner = styled.div.attrs({
|
||||
className: 'template-card-inner'
|
||||
})`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
const Icon = styled.div`
|
||||
const Icon = styled.div.attrs({
|
||||
className: 'template-card-icon'
|
||||
})`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 16px;
|
||||
font-size: 32px;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
const Header = styled.div.attrs({
|
||||
className: 'template-card-header'
|
||||
})`
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-base);
|
||||
display: flex;
|
||||
@@ -110,7 +119,7 @@ const Card: React.FC<CardProps> = (props) => {
|
||||
{icon && <Icon>{icon}</Icon>}
|
||||
<Inner>
|
||||
{header && <Header>{header}</Header>}
|
||||
<CardContent>{children}</CardContent>
|
||||
{children && <CardContent>{children}</CardContent>}
|
||||
{footer}
|
||||
</Inner>
|
||||
</CardWrapper>
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@ import { PageActionType, StatusType } from './types';
|
||||
export const PageAction: Record<string, PageActionType> = {
|
||||
CREATE: 'create',
|
||||
UPDATE: 'update',
|
||||
VIEW: 'view'
|
||||
VIEW: 'view',
|
||||
EDIT: 'edit'
|
||||
};
|
||||
|
||||
export const StatusColorMap: Record<
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ export interface DropDownItem {
|
||||
iconfont?: boolean;
|
||||
}
|
||||
|
||||
export type PageActionType = 'create' | 'update' | 'view';
|
||||
export type PageActionType = 'create' | 'update' | 'view' | 'edit';
|
||||
|
||||
export type StatusType =
|
||||
| 'error'
|
||||
|
||||
@@ -37,5 +37,9 @@ export default {
|
||||
'clusters.create.noInstanceTypes': 'No instance types available',
|
||||
'clusters.create.noRegions': 'No regions available',
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'Number of workers created simultaneously in the Worker pool'
|
||||
'Number of workers created simultaneously in the Worker pool',
|
||||
'clusters.create.addworker.tips':
|
||||
' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
|
||||
'clusters.create.addCommand.tips':
|
||||
' On the Worker that needs to be added, run the following command to join it to the cluster.'
|
||||
};
|
||||
|
||||
@@ -37,7 +37,11 @@ export default {
|
||||
'clusters.create.noInstanceTypes': 'No instance types available',
|
||||
'clusters.create.noRegions': 'No regions available',
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'Number of workers created simultaneously in the Worker pool'
|
||||
'Number of workers created simultaneously in the Worker pool',
|
||||
'clusters.create.addworker.tips':
|
||||
' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
|
||||
'clusters.create.addCommand.tips':
|
||||
' On the Worker that needs to be added, run the following command to join it to the cluster.'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -78,6 +82,8 @@ export default {
|
||||
// 35. 'clusters.create.noImages': 'No images available',
|
||||
// 36. 'clusters.create.noInstanceTypes': 'No instance types available',
|
||||
// 37. 'clusters.create.noRegions': 'No regions available',
|
||||
// 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool'
|
||||
// 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool',
|
||||
// 39. 'clusters.create.addworker.tips': ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
|
||||
// 40. 'clusters.create.addCommand.tips': ' On the Worker that needs to be added, run the following command to join it to the cluster.'
|
||||
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -37,7 +37,11 @@ export default {
|
||||
'clusters.create.noInstanceTypes': 'No instance types available',
|
||||
'clusters.create.noRegions': 'No regions available',
|
||||
'clusters.workerpool.batchSize.desc':
|
||||
'Number of workers created simultaneously in the Worker pool'
|
||||
'Number of workers created simultaneously in the Worker pool',
|
||||
'clusters.create.addworker.tips':
|
||||
' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
|
||||
'clusters.create.addCommand.tips':
|
||||
' On the Worker that needs to be added, run the following command to join it to the cluster.'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -79,5 +83,7 @@ export default {
|
||||
// 36. 'clusters.create.noInstanceTypes': 'No instance types available',
|
||||
// 37. 'clusters.create.noRegions': 'No regions available',
|
||||
// 38. 'clusters.workerpool.batchSize.desc': 'Number of workers created simultaneously in the Worker pool'
|
||||
// 39. 'clusters.create.addworker.tips': ' Please make sure the prerequisites for <a href={link} target="_blank">{label}</a> are met before executing the following command.',
|
||||
// 40. 'clusters.create.addCommand.tips': ' On the Worker that needs to be added, run the following command to join it to the cluster.'
|
||||
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -33,8 +33,12 @@ export default {
|
||||
'clusters.create.execCommand': '执行命令',
|
||||
'clusters.create.supportedGpu': '支持的 GPU',
|
||||
'clusters.create.skipfornow': '暂时跳过',
|
||||
'clusters.create.noImages': '没有可用的镜像',
|
||||
'clusters.create.noInstanceTypes': '没有可用的实例类型',
|
||||
'clusters.create.noRegions': '没有可用的区域',
|
||||
'clusters.workerpool.batchSize.desc': 'Worker池中同时创建的worker数量'
|
||||
'clusters.create.noImages': '无可用的镜像',
|
||||
'clusters.create.noInstanceTypes': '无可用的实例类型',
|
||||
'clusters.create.noRegions': '无可用的区域',
|
||||
'clusters.workerpool.batchSize.desc': 'Worker 池中同时创建的 worker 数量',
|
||||
'clusters.create.addworker.tips':
|
||||
'在执行以下命令之前,请确保已满足 <a href={link} target="_blank">{label}</a> 的先决条件。',
|
||||
'clusters.create.addCommand.tips':
|
||||
' 在需要添加的 Worker 上运行以下命令,将其加入到集群中'
|
||||
};
|
||||
|
||||
@@ -187,7 +187,7 @@ const ClusterCreate = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectProvider = (value: ProviderType) => {
|
||||
const handleSelectProvider = (value: string) => {
|
||||
if (value === extraData.provider) {
|
||||
onNext();
|
||||
return;
|
||||
|
||||
@@ -25,7 +25,7 @@ const AddWorkerCommand: React.FC<ViewModalProps> = ({ registrationInfo }) => {
|
||||
return (
|
||||
<HighlightCode
|
||||
theme="dark"
|
||||
code={code.replace(/\\/g, '')}
|
||||
code={code}
|
||||
copyValue={code}
|
||||
lang="bash"
|
||||
></HighlightCode>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BulbOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Alert } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ProviderType, ProviderValueMap } from '../config';
|
||||
@@ -9,7 +11,42 @@ import SupportedHardware from './support-hardware';
|
||||
const Title = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const Line = styled.div`
|
||||
position: relative;
|
||||
margin: 24px 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--ant-color-border);
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
left: 24px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transform: rotate(45deg);
|
||||
background-color: var(--ant-color-bg-container);
|
||||
border-top: 1px solid var(--ant-color-border);
|
||||
border-left: 1px solid var(--ant-color-border);
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
width: 800px;
|
||||
margin: 0 auto;
|
||||
.command-info {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
// font-weight: 500;
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
type AddModalProps = {
|
||||
@@ -26,19 +63,57 @@ const AddWorkerStep: React.FC<AddModalProps> = ({
|
||||
registrationInfo
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [currentProvider, setCurrentProvider] = React.useState<string>('cuda');
|
||||
const [workerCommand, setWorkerCommand] = React.useState<Record<string, any>>(
|
||||
{
|
||||
label: 'NVIDIA CUDA',
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#nvidia-cuda'
|
||||
}
|
||||
);
|
||||
|
||||
const handleSelectProvider = (provider: string, item: any) => {
|
||||
setCurrentProvider(provider);
|
||||
setWorkerCommand(item);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{intl.formatMessage({ id: 'clusters.create.execCommand' })}</Title>
|
||||
<Container>
|
||||
<Title>
|
||||
{intl.formatMessage({ id: 'clusters.create.supportedGpu' })}
|
||||
</Title>
|
||||
<SupportedHardware
|
||||
onSelect={handleSelectProvider}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
{provider === ProviderValueMap.Custom && (
|
||||
<Content>
|
||||
<Line></Line>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<BulbOutlined />}
|
||||
message={
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: intl.formatMessage(
|
||||
{ id: 'clusters.create.addworker.tips' },
|
||||
{ label: workerCommand.label, link: workerCommand.link }
|
||||
)
|
||||
}}
|
||||
></span>
|
||||
}
|
||||
></Alert>
|
||||
</Content>
|
||||
)}
|
||||
<div className="command-info">
|
||||
{intl.formatMessage({ id: 'clusters.create.addCommand.tips' })}
|
||||
</div>
|
||||
{provider === ProviderValueMap.Kubernetes ? (
|
||||
<RegisterClusterInner registrationInfo={registrationInfo} />
|
||||
) : (
|
||||
<AddWorkerCommand registrationInfo={registrationInfo} />
|
||||
)}
|
||||
<Title style={{ marginTop: 32 }}>
|
||||
{intl.formatMessage({ id: 'clusters.create.supportedGpu' })}
|
||||
</Title>
|
||||
<SupportedHardware />
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { fromClusterCreationAtom } from '@/atoms/clusters';
|
||||
import SealSelect from '@/components/seal-form/seal-select';
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Link, useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@@ -14,7 +16,7 @@ import { useProviderRegions } from '../hooks/use-provider-regions';
|
||||
type OptionData = {
|
||||
label: string;
|
||||
datacenter: string;
|
||||
value: string;
|
||||
value: string | number;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
@@ -70,14 +72,24 @@ const NotFoundContent: React.FC<{ loading: boolean }> = ({ loading }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const optionRender = (
|
||||
option: Global.BaseOption<
|
||||
number,
|
||||
{
|
||||
data: OptionData;
|
||||
}
|
||||
>
|
||||
): React.ReactNode => {
|
||||
const NotFoundCredentialContent: React.FC = () => {
|
||||
const [, setFromClusterCreation] = useAtom(fromClusterCreationAtom);
|
||||
const intl = useIntl();
|
||||
const handleOnClick = () => {
|
||||
console.log('click add credential');
|
||||
setFromClusterCreation(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<NoContent>
|
||||
<Link to={'/cluster-management/credentials'} onClick={handleOnClick}>
|
||||
{intl.formatMessage({ id: 'clusters.button.addCredential' })}
|
||||
</Link>
|
||||
</NoContent>
|
||||
);
|
||||
};
|
||||
|
||||
const optionRender = (option: any): React.ReactNode => {
|
||||
const { value } = option;
|
||||
const data = option.data!;
|
||||
|
||||
@@ -127,8 +139,8 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
|
||||
}, [credentialID]);
|
||||
|
||||
const labelRender = (props: {
|
||||
label: string;
|
||||
value: string;
|
||||
label: React.ReactNode;
|
||||
value: string | number;
|
||||
}): React.ReactNode => {
|
||||
const data = regions.find((item) => item.value === props.value);
|
||||
if (!data) return props.label;
|
||||
@@ -164,6 +176,7 @@ const CloudProvider: React.FC<CloudProviderProps> = (props) => {
|
||||
]}
|
||||
>
|
||||
<SealSelect
|
||||
notFoundContent={<NotFoundCredentialContent />}
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({ id: 'clusters.credential.title' })}
|
||||
required
|
||||
|
||||
@@ -72,7 +72,7 @@ const OptionItem = styled.div.attrs({
|
||||
const DescriptionWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
gap: 4px 8px;
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
@@ -94,34 +94,48 @@ const NotFoundImageContent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const RenderInstanceOption = (option: any) => {
|
||||
const { data, styles } = option;
|
||||
const RenderLabel = (data: {
|
||||
label: React.ReactNode;
|
||||
vendor: string;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const { label, vendor, style } = data;
|
||||
return (
|
||||
<div style={style} className="flex-center gap-8">
|
||||
<IconFont type={_.get(vendorIconMap, vendor, 'icon-gpu1')}></IconFont>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const RenderOption = (option: any) => {
|
||||
const { data = {}, styles } = option;
|
||||
const entries = Object.entries(data?.specInfo || {});
|
||||
return (
|
||||
<CardContainer
|
||||
key={data.value}
|
||||
header={
|
||||
<span>
|
||||
<IconFont
|
||||
type={_.get(vendorIconMap, data.vendor, 'icon-gpu1')}
|
||||
className="m-r-8"
|
||||
></IconFont>
|
||||
{data.description}
|
||||
</span>
|
||||
<RenderLabel
|
||||
label={data.description || data.label}
|
||||
vendor={data.vendor}
|
||||
style={styles?.header}
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<DescriptionWrapper style={{ ...(styles?.description || {}) }}>
|
||||
{Object.entries(data?.specInfo)
|
||||
.filter(([key, value]) => value)
|
||||
.map(([key, value]) => (
|
||||
<OptionItem key={key}>
|
||||
<span className="label">
|
||||
{_.get(instanceTypeFieldMap, key, key)}:
|
||||
</span>
|
||||
<span className="value">{value as string}</span>
|
||||
</OptionItem>
|
||||
))}
|
||||
</DescriptionWrapper>
|
||||
entries.length > 0 && (
|
||||
<DescriptionWrapper style={{ ...styles?.description }}>
|
||||
{Object.entries(data?.specInfo)
|
||||
.filter(([key, value]) => value)
|
||||
.map(([key, value]) => (
|
||||
<OptionItem key={key}>
|
||||
<span className="label">
|
||||
{_.get(instanceTypeFieldMap, key, key)}:
|
||||
</span>
|
||||
<span className="value">{value as string}</span>
|
||||
</OptionItem>
|
||||
))}
|
||||
</DescriptionWrapper>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -186,19 +200,45 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
const imageLabelRender = (data: { label: string; value: string }) => {
|
||||
console.log('imageLabelRender========', data);
|
||||
const imageLabelRender = (data: {
|
||||
label: React.ReactNode;
|
||||
value: string | number;
|
||||
}) => {
|
||||
if (action === PageAction.EDIT) {
|
||||
return currentData?.image_name || currentData?.os_image;
|
||||
console.log('imageLabelRender========::', action, currentData, data);
|
||||
// return currentData?.image_name || currentData?.os_image;
|
||||
const vendor = _.split(currentData?.image_name || '', ' ')[0];
|
||||
const iconType = _.get(vendorIconMap, vendor.toLowerCase());
|
||||
return (
|
||||
<div className="flex-center gap-8">
|
||||
{iconType && <IconFont type={iconType}></IconFont>}
|
||||
{currentData?.image_name || currentData?.os_image}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return data.label;
|
||||
const selectImage = osImageList.find((item) => item.value === data.value);
|
||||
console.log('imageLabelRender========::', selectImage);
|
||||
if (selectImage) {
|
||||
return (
|
||||
<RenderLabel label={data.label} vendor={selectImage.vendor || ''} />
|
||||
);
|
||||
}
|
||||
return data.value;
|
||||
};
|
||||
|
||||
const instanceLabelRender = (data: { label: string; value: string }) => {
|
||||
if (action === PageAction.EDIT) {
|
||||
return currentData?.instance_spec?.label || currentData?.instance_type;
|
||||
}
|
||||
return data.label;
|
||||
const instanceLabelRender = (data: {
|
||||
label: React.ReactNode;
|
||||
value: string | number;
|
||||
}) => {
|
||||
const currentInstanceSpec =
|
||||
instanceTypeList.find((item) => item.value === data.value) ||
|
||||
instanceSpec;
|
||||
return (
|
||||
<RenderLabel
|
||||
label={data.label || currentInstanceSpec?.label || data.value}
|
||||
vendor={currentInstanceSpec?.vendor || ''}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleOsImageChange = (value: string) => {
|
||||
@@ -329,7 +369,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
required
|
||||
options={instanceTypeList}
|
||||
disabled={action === PageAction.EDIT}
|
||||
optionRender={RenderInstanceOption}
|
||||
optionRender={RenderOption}
|
||||
onChange={handleInstanceTypeChange}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
@@ -383,11 +423,16 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||
>
|
||||
<AutoComplete
|
||||
showSearch
|
||||
notFoundContent={<NotFoundImageContent />}
|
||||
onChange={handleOsImageChange}
|
||||
filterOption={filterImageOption}
|
||||
optionRender={RenderInstanceOption}
|
||||
optionRender={(option) =>
|
||||
RenderOption({
|
||||
...option,
|
||||
styles: { header: { marginBlock: 5 } }
|
||||
})
|
||||
}
|
||||
labelRender={imageLabelRender}
|
||||
placeholder={currentData?.image_name}
|
||||
options={osImageList}
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({
|
||||
|
||||
@@ -4,14 +4,20 @@ import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ProviderType } from '../config';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
const Wrapper = styled.div<{ $cols?: number }>`
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 22px;
|
||||
grid-template-columns: repeat(${(props) => props.$cols || 3}, 1fr);
|
||||
gap: 16px;
|
||||
.template-card-wrapper {
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
width: 800px;
|
||||
margin: 0 auto;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`;
|
||||
@@ -22,19 +28,20 @@ const Title = styled.span`
|
||||
justify-content: space-between;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
margin-block: 16px 24px;
|
||||
margin-block: 20px 16px;
|
||||
`;
|
||||
|
||||
interface ProviderCatalogProps {
|
||||
onSelect?: (provider: ProviderType) => void;
|
||||
currentProvider?: ProviderType;
|
||||
onSelect?: (provider: string, item: any) => void;
|
||||
cols?: number;
|
||||
currentProvider?: ProviderType | string;
|
||||
clickable?: boolean;
|
||||
dataList: {
|
||||
label: string;
|
||||
key: string;
|
||||
locale?: boolean;
|
||||
disabled?: boolean;
|
||||
icon: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
description?: string;
|
||||
group?: string;
|
||||
}[];
|
||||
@@ -42,6 +49,7 @@ interface ProviderCatalogProps {
|
||||
|
||||
const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
|
||||
onSelect,
|
||||
cols = 3,
|
||||
dataList,
|
||||
clickable,
|
||||
currentProvider
|
||||
@@ -66,12 +74,12 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
|
||||
{groupName !== 'default' && (
|
||||
<Title>{intl.formatMessage({ id: groupName })}</Title>
|
||||
)}
|
||||
<Wrapper>
|
||||
<Wrapper $cols={cols}>
|
||||
{items?.map((action) => (
|
||||
<Card
|
||||
height="auto"
|
||||
height="80px"
|
||||
key={action.key}
|
||||
onClick={() => onSelect?.(action.key as ProviderType)}
|
||||
onClick={() => onSelect?.(action.key as string, action)}
|
||||
active={currentProvider === action.key}
|
||||
disabled={action.disabled}
|
||||
clickable={clickable}
|
||||
@@ -82,7 +90,7 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
|
||||
}
|
||||
icon={action.icon}
|
||||
>
|
||||
{action.description || 'This is a description'}
|
||||
{action.description}
|
||||
</Card>
|
||||
))}
|
||||
</Wrapper>
|
||||
|
||||
@@ -4,8 +4,8 @@ import hyponPNG from '@/assets/logo/hygon.png';
|
||||
import iluvatarWEBP from '@/assets/logo/Iluvatar.png';
|
||||
import metaxLogo from '@/assets/logo/metax.png';
|
||||
import moorePNG from '@/assets/logo/moore _threads.png';
|
||||
import nvidiaLogo from '@/assets/logo/nvidia.png';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import styled from 'styled-components';
|
||||
import ProviderCatalog from './provider-catalog';
|
||||
|
||||
const ProviderImage = ({ src, showBg }: { src: string; showBg?: boolean }) => {
|
||||
@@ -24,15 +24,19 @@ const supportedHardPlatforms = [
|
||||
{
|
||||
label: 'NVIDIA CUDA',
|
||||
value: 'cuda',
|
||||
key: 'nvidia',
|
||||
description: '',
|
||||
key: 'cuda',
|
||||
locale: false,
|
||||
icon: <ProviderImage src={nvidiaLogo} showBg />
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#nvidia-cuda',
|
||||
icon: <IconFont type="icon-nvidia2" style={{ fontSize: 32 }} />
|
||||
},
|
||||
{
|
||||
label: 'AMD ROCm',
|
||||
description: '',
|
||||
value: 'rocm',
|
||||
key: 'amd',
|
||||
key: 'rocm',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#amd-rocm',
|
||||
icon: (
|
||||
<IconFont
|
||||
type="icon-amd"
|
||||
@@ -42,30 +46,38 @@ const supportedHardPlatforms = [
|
||||
},
|
||||
{
|
||||
label: 'Ascend CANN',
|
||||
description: '',
|
||||
value: 'npu',
|
||||
key: 'ascend',
|
||||
key: 'npu',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#ascend-cann',
|
||||
icon: <ProviderImage src={ascendLogo} showBg />
|
||||
},
|
||||
{
|
||||
label: 'Hygon DTK',
|
||||
description: '',
|
||||
value: 'dcu',
|
||||
key: 'hygon',
|
||||
key: 'dcu',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#hygon-dtk',
|
||||
icon: <ProviderImage src={hyponPNG} />
|
||||
},
|
||||
{
|
||||
label: 'Moore Threads MUSA',
|
||||
label: 'Moore Threads',
|
||||
description: '',
|
||||
value: 'musa',
|
||||
key: 'musa',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#moore-threads-musa',
|
||||
icon: <ProviderImage src={moorePNG} />
|
||||
},
|
||||
{
|
||||
label: 'Iluvatar Corex',
|
||||
description: '',
|
||||
value: 'corex',
|
||||
key: 'corex',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#iluvatar-corex',
|
||||
icon: <ProviderImage src={iluvatarWEBP} showBg />
|
||||
},
|
||||
{
|
||||
@@ -73,6 +85,7 @@ const supportedHardPlatforms = [
|
||||
value: 'cambricon',
|
||||
key: 'cambricon',
|
||||
locale: false,
|
||||
link: 'https://docs.gpustack.ai/latest/installation/installation-requirements/#cambricon-mlu',
|
||||
icon: <ProviderImage src={CambriconPNG} />
|
||||
},
|
||||
{
|
||||
@@ -84,9 +97,50 @@ const supportedHardPlatforms = [
|
||||
}
|
||||
];
|
||||
|
||||
const SupportedHardware = () => {
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
`;
|
||||
|
||||
const renderHeader = (item: any) => {
|
||||
return (
|
||||
<ProviderCatalog dataList={supportedHardPlatforms} clickable={false} />
|
||||
<Header>
|
||||
<span className="icon">{item.icon}</span>
|
||||
<span className="label">{item.label}</span>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
const dataList = supportedHardPlatforms.map((item) => {
|
||||
return {
|
||||
label: renderHeader(item),
|
||||
value: item.value,
|
||||
key: item.key,
|
||||
locale: item.locale
|
||||
};
|
||||
});
|
||||
|
||||
interface SupportedHardwareProps {
|
||||
onSelect?: (provider: string, item: any) => void;
|
||||
currentProvider?: string;
|
||||
}
|
||||
|
||||
const SupportedHardware: React.FC<SupportedHardwareProps> = ({
|
||||
onSelect,
|
||||
currentProvider
|
||||
}) => {
|
||||
return (
|
||||
<ProviderCatalog
|
||||
onSelect={onSelect}
|
||||
currentProvider={currentProvider}
|
||||
dataList={supportedHardPlatforms}
|
||||
clickable={true}
|
||||
cols={4}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fromClusterCreationAtom } from '@/atoms/clusters';
|
||||
import DeleteModal from '@/components/delete-modal';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { FilterBar } from '@/components/page-tools';
|
||||
@@ -5,9 +6,10 @@ import { PageAction } from '@/config';
|
||||
import type { PageActionType } from '@/config/types';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useIntl, useNavigate } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { ConfigProvider, Empty, Table, message } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
createCredential,
|
||||
@@ -52,8 +54,9 @@ const Credentials: React.FC = () => {
|
||||
deleteAPI: deleteCredential,
|
||||
contentForDelete: 'menu.clusterManagement.credentials'
|
||||
});
|
||||
|
||||
const [isFromCluster] = useAtom(fromClusterCreationAtom);
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const [openModalStatus, setOpenModalStatus] = useState<{
|
||||
provider: ProviderType;
|
||||
open: boolean;
|
||||
@@ -97,6 +100,9 @@ const Credentials: React.FC = () => {
|
||||
});
|
||||
} else {
|
||||
await createCredential({ data: params });
|
||||
// if (isFromCluster) {
|
||||
// navigate(-1);
|
||||
// }
|
||||
}
|
||||
fetchData();
|
||||
setOpenModalStatus({ ...openModalStatus, open: false });
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import DropdownButtons from '@/components/drop-down-buttons';
|
||||
import { SealColumnProps } from '@/components/seal-table/types';
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import type { SortOrder } from 'antd/es/table/interface';
|
||||
import dayjs from 'dayjs';
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { RenderInstanceOption } from '../components/pool-form';
|
||||
import { RenderOption } from '../components/pool-form';
|
||||
import { NodePoolListItem as ListItem } from '../config/types';
|
||||
|
||||
const actionItems = [
|
||||
@@ -30,7 +30,7 @@ const actionItems = [
|
||||
const usePoolsColumns = (
|
||||
handleSelect: (val: string, record: ListItem) => void,
|
||||
sortOrder?: SortOrder
|
||||
): ColumnsType<ListItem & { dataIndex: string }> => {
|
||||
): SealColumnProps[] => {
|
||||
const intl = useIntl();
|
||||
|
||||
return useMemo(() => {
|
||||
@@ -66,7 +66,7 @@ const usePoolsColumns = (
|
||||
render: (text: string, record: ListItem) => (
|
||||
<AutoTooltip
|
||||
title={
|
||||
<RenderInstanceOption
|
||||
<RenderOption
|
||||
styles={{
|
||||
description: {
|
||||
color: 'var(--color-white-quaternary)'
|
||||
@@ -180,6 +180,7 @@ const usePoolsColumns = (
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.operation' }),
|
||||
key: 'operations',
|
||||
dataIndex: 'operations',
|
||||
span: 3,
|
||||
style: {
|
||||
paddingLeft: 36
|
||||
|
||||
@@ -67,15 +67,19 @@ const formatSpec = (spec: ParsedSpec): string => {
|
||||
|
||||
if (spec.vram) parts.push(`${spec.vram} VRAM`);
|
||||
if (spec.vcpus) parts.push(`${spec.vcpus} vCPUs`);
|
||||
if (spec.bootDisk) parts.push(`${spec.bootDisk} Boot disk`);
|
||||
if (spec.ram) parts.push(`${spec.ram} RAM`);
|
||||
if (spec.scratchDisk) {
|
||||
parts.push(`${spec.scratchDisk} Scratch disk`);
|
||||
}
|
||||
// if (spec.bootDisk) parts.push(`${spec.bootDisk} Boot disk`);
|
||||
// if (spec.scratchDisk) {
|
||||
// parts.push(`${spec.scratchDisk} Scratch disk`);
|
||||
// }
|
||||
|
||||
return parts.join(' / ');
|
||||
};
|
||||
|
||||
const formatLabel = (instanceSpec: any): string => {
|
||||
return `${_.toUpper(instanceSpec.gpu_info?.model.replace(/_/g, ' '))}`;
|
||||
};
|
||||
|
||||
export const useProviderRegions = () => {
|
||||
const [regions, setRegions] = useAtom(regionListAtom);
|
||||
const [, setInstanceTypes] = useAtom(regionInstanceTypeListAtom);
|
||||
@@ -119,10 +123,12 @@ export const useProviderRegions = () => {
|
||||
?.filter((sItem: any) => sItem.gpu_info && sItem.available)
|
||||
.map((item: any) => {
|
||||
const specInfo = parseSpec(item);
|
||||
const label = formatLabel(item);
|
||||
const description = `${label} ${item.gpu_info?.count}X`;
|
||||
return {
|
||||
label: formatSpec(specInfo),
|
||||
label: `${description} - ${formatSpec(specInfo)}`,
|
||||
value: item.slug,
|
||||
description: item.description,
|
||||
description: description,
|
||||
specInfo: specInfo,
|
||||
vendor: _.get(_.split(item.gpu_info?.model, '_'), 0),
|
||||
available: item.available,
|
||||
@@ -148,10 +154,7 @@ export const useProviderRegions = () => {
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
vendor: _.camelCase(item.distribution),
|
||||
specInfo: {
|
||||
size: `${item.size_gigabytes} GiB`,
|
||||
minDiskSize: `${item.min_disk_size} GiB`
|
||||
},
|
||||
specInfo: {},
|
||||
regions: item.regions || []
|
||||
};
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ const BasicForm = forwardRef((props: BasicFormProps, ref) => {
|
||||
return (
|
||||
<div>
|
||||
<PageTools
|
||||
marginBottom={26}
|
||||
marginBottom={16}
|
||||
left={
|
||||
<Title>
|
||||
{intl.formatMessage({ id: 'clusters.create.configBasic' })}
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../style/gpu-card.less';
|
||||
|
||||
const CardWrapper = styled.div`
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
@@ -18,7 +19,6 @@ const CardWrapper = styled.div`
|
||||
|
||||
const Header = styled.div`
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
const Description = styled.div`
|
||||
@@ -36,7 +36,7 @@ export const CardContainer: React.FC<{
|
||||
return (
|
||||
<CardWrapper>
|
||||
<Header>{header}</Header>
|
||||
<Description>{description}</Description>
|
||||
{description && <Description>{description}</Description>}
|
||||
</CardWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -64,7 +64,9 @@ export const addWorkerGuide: Record<string, any> = {
|
||||
--ipc=host \\
|
||||
-v gpustack-data:/var/lib/gpustack \\
|
||||
${params.image} \\
|
||||
--server-url ${params.server} --registration-token ${params.token} --worker-ip ${params.workerip}`;
|
||||
--server-url ${params.server} \\
|
||||
--registration-token ${params.token} \\
|
||||
--worker-ip ${params.workerip}`;
|
||||
}
|
||||
},
|
||||
npu: {
|
||||
|
||||
Reference in New Issue
Block a user