diff --git a/config/routes.ts b/config/routes.ts index ed5a225a..43703822 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -54,16 +54,6 @@ export default [ defaultIcon: 'icon-reranker', component: './playground/rerank' }, - // { - // name: 'text2images', - // title: 'Text2Images', - // path: keepAliveRoutes.text2images, - // key: 'text2images', - // icon: 'icon-image1', - // selectedIcon: 'icon-image-filled', - // defaultIcon: 'icon-image1', - // component: './playground/images' - // }, { name: 'speech', title: 'Speech', diff --git a/src/assets/styles/common.less b/src/assets/styles/common.less index 0d6af475..993469d8 100644 --- a/src/assets/styles/common.less +++ b/src/assets/styles/common.less @@ -324,3 +324,7 @@ textarea:hover { .ant-result-extra { color: var(--ant-color-text-secondary); } + +.text-success { + color: var(--ant-color-success); +} diff --git a/src/components/echarts/config.ts b/src/components/echarts/config.ts index 389ed2d1..bbddb374 100644 --- a/src/components/echarts/config.ts +++ b/src/components/echarts/config.ts @@ -24,6 +24,8 @@ export default function useChartConfig() { splitLineColor: token.colorBorder, tickLineColor: token.colorSplit, axislabelColor: token.colorTextTertiary, + colorSecondary: token.colorTextSecondary, + colorTertiary: token.colorTextTertiary, gaugeBgColor: token.colorFillSecondary, gaugeSplitLineColor: isDarkTheme ? 'rgba(255,255,255,.3)' @@ -172,8 +174,8 @@ export default function useChartConfig() { } }, axisTick: { - distance: -11, - length: 6, + distance: -10, + length: 5, splitNumber: 5, lineStyle: { width: 1.5, @@ -206,13 +208,13 @@ export default function useChartConfig() { rich: { value: { fontSize: 16, - fontWeight: '500', + fontWeight: '600', color: chartColorMap.titleColor }, unit: { fontSize: 14, color: chartColorMap.titleColor, - fontWeight: '500', + fontWeight: '600', padding: [0, 0, 0, 2] } } diff --git a/src/components/echarts/gauge.tsx b/src/components/echarts/gauge.tsx index 5e41cacd..a4dc5e9d 100644 --- a/src/components/echarts/gauge.tsx +++ b/src/components/echarts/gauge.tsx @@ -1,7 +1,7 @@ import Chart from '@/components/echarts/chart'; import useChartConfig from '@/components/echarts/config'; import EmptyData from '@/components/empty-data'; -import React, { memo } from 'react'; +import React from 'react'; import { ChartProps } from './types'; const strokeColorFunc = (percent: number) => { @@ -34,10 +34,19 @@ const GaugeChart: React.FC> = ( ...gaugeItemConfig, ...gaugeConfig }; + + combineGaugeConfig.detail.rich.value.color = colorValue; + combineGaugeConfig.detail.rich.unit.color = colorValue; + return { title: { ...titleConfig, text: title, + textStyle: { + fontSize: 12, + color: chartColorMap.colorSecondary, + fontWeight: 400 + }, top: '0', left: 'center' }, @@ -58,7 +67,7 @@ const GaugeChart: React.FC> = ( color: 'transparent' }, detail: { - ...gaugeItemConfig.detail, + ...combineGaugeConfig.detail, formatter: labelFormatter || gaugeItemConfig.detail.formatter }, data: [{ value }] @@ -78,4 +87,4 @@ const GaugeChart: React.FC> = ( ); }; -export default memo(GaugeChart); +export default GaugeChart; diff --git a/src/components/scroller-modal/index.tsx b/src/components/scroller-modal/index.tsx index 1c83e926..dcee81fc 100644 --- a/src/components/scroller-modal/index.tsx +++ b/src/components/scroller-modal/index.tsx @@ -1,10 +1,20 @@ -import OverlayScroller from '@/components/overlay-scroller'; import useBodyScroll from '@/hooks/use-body-scroll'; +import useOverlayScroller from '@/hooks/use-overlay-scroller'; import { Modal, type ModalProps } from 'antd'; import React from 'react'; +import styled from 'styled-components'; + +const Wrapper = styled.div<{ $maxHeight?: number }>` + max-height: ${({ $maxHeight }) => + typeof $maxHeight === 'number' ? `${$maxHeight}px` : $maxHeight}; + overflow-y: auto; + width: 100%; +`; const ScrollerModal = (props: ModalProps & { maxContentHeight?: number }) => { + const scroller = React.useRef(null); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); + const { initialize, destroyInstance } = useOverlayScroller(); React.useEffect(() => { if (props.open) { @@ -14,6 +24,24 @@ const ScrollerModal = (props: ModalProps & { maxContentHeight?: number }) => { } }, [props.open]); + // init scroller, delay to ensure modal is fully open + React.useEffect(() => { + let timeout = null; + if (props.open) { + timeout = setTimeout(() => { + if (scroller.current) { + initialize(scroller.current); + } + }, 100); + } + return () => { + if (timeout) { + clearTimeout(timeout); + } + destroyInstance(); + }; + }, [props.open, initialize]); + return ( { } }} > - + ); }; diff --git a/src/components/tags-wrapper/theme-tag.tsx b/src/components/tags-wrapper/theme-tag.tsx index 00077a7c..3d7ecfe5 100644 --- a/src/components/tags-wrapper/theme-tag.tsx +++ b/src/components/tags-wrapper/theme-tag.tsx @@ -1,6 +1,18 @@ import useUserSettings from '@/hooks/use-user-settings'; import { Tag, TagProps } from 'antd'; import React from 'react'; +import styled from 'styled-components'; + +const TagWrapper = styled(Tag)` + display: flex; + align-items: center; + justify-content: center; + padding: 2px 6px; + border-radius: 4px; + font-size: 12px; + height: 22px; + opacity: 0.7; +`; const ThemeTag: React.FC = ({ opacity, @@ -11,7 +23,7 @@ const ThemeTag: React.FC = ({ const { userSettings } = useUserSettings(); const { isDarkTheme } = userSettings; return ( - = ({ }} > {children} - + ); }; diff --git a/src/components/templates/card.tsx b/src/components/templates/card.tsx index 78266460..4c1311b4 100644 --- a/src/components/templates/card.tsx +++ b/src/components/templates/card.tsx @@ -8,13 +8,14 @@ interface CardProps { children?: React.ReactNode; clickable?: boolean; ghost?: boolean; + header?: React.ReactNode; + footer?: React.ReactNode; onClick?: () => void; } const CardWrapper = styled.div` overflow: hidden; display: flex; - padding: 16px 20px; flex-direction: column; justify-content: flex-start; align-items: flex-start; @@ -39,6 +40,12 @@ const CardWrapper = styled.div` } `; +const CardContent = styled.div` + padding: 16px 20px; + width: 100%; + flex: 1; +`; + const Card: React.FC = (props) => { const { className, @@ -46,6 +53,8 @@ const Card: React.FC = (props) => { children, clickable = true, ghost = false, + header, + footer, onClick } = props; @@ -58,7 +67,9 @@ const Card: React.FC = (props) => { style={{ height: height || '180px' }} onClick={onClick} > - {children} + {header} + {children} + {footer} ); }; diff --git a/src/hooks/use-body-scroll.ts b/src/hooks/use-body-scroll.ts index 5e3e2b48..41b77941 100644 --- a/src/hooks/use-body-scroll.ts +++ b/src/hooks/use-body-scroll.ts @@ -55,5 +55,9 @@ export default function useBodyScroll() { }; }, []); - return { saveScrollHeight, restoreScrollHeight }; + return { + saveScrollHeight, + restoreScrollHeight, + bodyScroller: bodyScroller.current + }; } diff --git a/src/hooks/use-overlay-scroller.ts b/src/hooks/use-overlay-scroller.ts index 1daab35d..c0b4f273 100644 --- a/src/hooks/use-overlay-scroller.ts +++ b/src/hooks/use-overlay-scroller.ts @@ -199,6 +199,12 @@ export default function useOverlayScroller(data?: { [initialize, instance] ); + const destroyInstance = () => { + instanceRef.current?.destroy?.(); + removeWheelEvent(); + instanceRef.current = null; + }; + useEffect(() => { return () => { instanceRef.current?.destroy?.(); @@ -212,6 +218,7 @@ export default function useOverlayScroller(data?: { scrollEventElement: scrollEventElement.current, initialized: initialized.current, generateInstance, + destroyInstance: destroyInstance, updateScrollerPosition: throttledUpdateScrollerPosition, updateScrollerPositionToTop: updateScrollerPositionToTop }; diff --git a/src/locales/en-US/resources.ts b/src/locales/en-US/resources.ts index f9d66b57..0fb9918c 100644 --- a/src/locales/en-US/resources.ts +++ b/src/locales/en-US/resources.ts @@ -44,8 +44,7 @@ export default { 'resources.worker.add.step1': 'Get Token (Run on the server)', 'resources.worker.add.step2': 'Register Worker', - 'resources.worker.add.step2.tips': - '(Run on the worker to be added, token is the value obtained in the first step.)', + 'resources.worker.add.step2.tips': '(Run on the worker to be added.)', 'resources.worker.add.step3': 'After success, refresh the workers list to view the new worker.', 'resources.worker.container.supported': 'Do not support macOS or Windows.', diff --git a/src/locales/ja-JP/resources.ts b/src/locales/ja-JP/resources.ts index c9fc5629..276cc085 100644 --- a/src/locales/ja-JP/resources.ts +++ b/src/locales/ja-JP/resources.ts @@ -44,8 +44,7 @@ export default { 'resources.worker.add.step1': 'トークンを取得 (サーバーで実行)', 'resources.worker.add.step2': 'ワーカーを登録', - 'resources.worker.add.step2.tips': - '(追加するワーカーで実行し、トークン は最初のステップで取得した値です。)', + 'resources.worker.add.step2.tips': '(追加するワーカーで実行し。)', 'resources.worker.add.step3': '成功後、ワーカーリストを更新して新しいワーカーを確認してください。', 'resources.worker.container.supported': diff --git a/src/locales/ru-RU/resources.ts b/src/locales/ru-RU/resources.ts index de45a880..17495a84 100644 --- a/src/locales/ru-RU/resources.ts +++ b/src/locales/ru-RU/resources.ts @@ -44,8 +44,7 @@ export default { 'resources.worker.add.step1': 'Получить токен (Запустить на сервере)', 'resources.worker.add.step2': 'Зарегистрировать воркер', - 'resources.worker.add.step2.tips': - '(Запустить на добавляемом воркере, token — это значение, полученное на первом шаге.)', // Translated + 'resources.worker.add.step2.tips': '(Запустить на добавляемом воркере.)', // Translated 'resources.worker.add.step3': 'После успешной регистрации обновите список воркеров.', 'resources.worker.container.supported': 'Только для Linux.', diff --git a/src/locales/zh-CN/resources.ts b/src/locales/zh-CN/resources.ts index 1d47a093..6e27cb2d 100644 --- a/src/locales/zh-CN/resources.ts +++ b/src/locales/zh-CN/resources.ts @@ -44,8 +44,7 @@ export default { 'resources.worker.add.step1': '获取 Token(在 Server 上运行)', 'resources.worker.add.step2': '注册 Worker', - 'resources.worker.add.step2.tips': - '(在需要添加的 Worker 上运行,token 为第一步获取到的值。)', + 'resources.worker.add.step2.tips': '(在需要添加的 Worker 上运行。)', 'resources.worker.add.step3': '成功后,刷新 Worker 列表即可看到新的 Worker', 'resources.worker.container.supported': '不支持 macOS 和 Windows', 'resources.worker.current.version': '当前版本为 {version}', diff --git a/src/pages/cluster-management/clusters.tsx b/src/pages/cluster-management/clusters.tsx index 8aa0a648..48918cc7 100644 --- a/src/pages/cluster-management/clusters.tsx +++ b/src/pages/cluster-management/clusters.tsx @@ -25,7 +25,6 @@ import { updateCredential } from './apis'; import AddCluster from './components/add-cluster'; -import AddPool from './components/add-pool'; import ClusterItem from './components/cluster-item'; import { ClusterDataList } from './config'; import { @@ -146,7 +145,10 @@ const Credentials: React.FC = () => { }); const intl = useIntl(); - const [open, setOpen] = useState(false); + const [openAddWorker, setOpenAddWorker] = useState({ + open: false, + token: '' + }); const [openAddModal, setOpenAddModal] = useState(false); const [provider, setProvider] = useState('custom'); const [action, setAction] = useState(PageAction.CREATE); @@ -222,7 +224,7 @@ const Credentials: React.FC = () => { setOpenAddModal(false); }; - const handleEditUser = (row: ListItem) => { + const handleEditCluster = (row: ListItem) => { setCurrentData(row); setOpenAddModal(true); setAction(PageAction.EDIT); @@ -231,11 +233,14 @@ const Credentials: React.FC = () => { const handleSelect = (val: any, row: ListItem) => { if (val === 'edit') { - handleEditUser(row); + handleEditCluster(row); } else if (val === 'delete') { handleDelete({ ...row, name: row.name }); } else if (val === 'add_worker') { - setOpen(true); + setOpenAddWorker({ + open: true, + token: '${token}' + }); setCurrentData(row); } else if (val === 'addPool') { handleAddPool(row.provider); @@ -308,29 +313,11 @@ const Credentials: React.FC = () => { onCancel={handleModalCancel} onOk={handleModalOk} > - { - setAddPoolStatus({ - open: false, - action: PageAction.CREATE, - title: '', - provider: 'digitalocean' - }); - }} - onOk={() => { - setAddPoolStatus({ - open: false, - action: PageAction.CREATE, - title: '', - provider: 'digitalocean' - }); - }} - > - setOpen(false)}> + setOpenAddWorker({ open: false, token: '' })} + token={openAddWorker.token} + > ); diff --git a/src/pages/cluster-management/components/add-cluster.tsx b/src/pages/cluster-management/components/add-cluster.tsx index 436e48ec..9ff50641 100644 --- a/src/pages/cluster-management/components/add-cluster.tsx +++ b/src/pages/cluster-management/components/add-cluster.tsx @@ -2,15 +2,18 @@ import ModalFooter from '@/components/modal-footer'; import ScrollerModal from '@/components/scroller-modal/index'; import SealInput from '@/components/seal-form/seal-input'; import { PageActionType } from '@/config/types'; -import { CloseOutlined } from '@ant-design/icons'; +import ContainerInstall from '@/pages/resources/components/container-install'; +import { CheckCircleFilled } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Button, Form } from 'antd'; -import React from 'react'; +import { Form } from 'antd'; +import React, { useMemo } from 'react'; +import { ProviderValueMap } from '../config'; import { ClusterFormData as FormData, ClusterListItem as ListItem } from '../config/types'; import CloudProvider from './cloud-provider-form'; +import RegisterClusterInner from './resiter-cluster-inner'; type AddModalProps = { title: string; @@ -18,7 +21,6 @@ type AddModalProps = { open: boolean; provider: string; // 'kubernetes' | 'custom' | 'digitalocean'; onOk: (values: FormData) => void; - data?: ListItem; onCancel: () => void; }; const AddCluster: React.FC = ({ @@ -27,11 +29,14 @@ const AddCluster: React.FC = ({ open, provider, onOk, - data, onCancel }) => { const [form] = Form.useForm(); const intl = useIntl(); + const [submissionStatus, setSubmissionStatus] = React.useState<{ + success: boolean; + data: ListItem; + }>({ success: true, data: {} as ListItem }); const handleSubmit = () => { form.submit(); @@ -42,56 +47,98 @@ const AddCluster: React.FC = ({ onCancel(); }; + const renderAddWorkerContent = () => { + if (provider === ProviderValueMap.Custom) { + return ; + } + if (provider === ProviderValueMap.Kubernetes) { + return ( + + ); + } + return null; + }; + const modalTitle = useMemo(() => { + if (submissionStatus.success) { + return ( +
+ + Cluster added! Now you can register a worker. +
+ ); + } + return title; + }, [submissionStatus.success, title]); + + const renderFooter = () => { + if (submissionStatus.success) { + return ( + + ); + } + return ; + }; + return ( - {title} - - - } + title={modalTitle} open={open} - onClose={onCancel} + onCancel={handleCancel} destroyOnClose={true} - closeIcon={false} + closeIcon={true} maskClosable={false} keyboard={false} - width={600} - footer={ - - } + width={680} + footer={renderFooter()} > -
- - name="display_name" - rules={[ - { - required: true, - message: intl.formatMessage( - { id: 'common.form.rule.input' }, - { - name: intl.formatMessage({ id: 'common.table.name' }) - } - ) - } - ]} - > - - - {provider === 'digitalocean' && ( - - )} - name="description" rules={[{ required: false }]}> - - - + {submissionStatus.success ? ( + renderAddWorkerContent() + ) : ( +
+ + name="display_name" + rules={[ + { + required: true, + message: intl.formatMessage( + { id: 'common.form.rule.input' }, + { + name: intl.formatMessage({ id: 'common.table.name' }) + } + ) + } + ]} + > + + + {provider === 'digitalocean' && ( + + )} + name="description" rules={[{ required: false }]}> + + + + )}
); }; diff --git a/src/pages/cluster-management/components/cluster-detail.tsx b/src/pages/cluster-management/components/cluster-detail.tsx new file mode 100644 index 00000000..ca0b97be --- /dev/null +++ b/src/pages/cluster-management/components/cluster-detail.tsx @@ -0,0 +1,140 @@ +import GaugeChart from '@/components/echarts/gauge'; +import { PageAction } from '@/config'; +import { Col, Row } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; +import { ProviderValueMap } from '../config'; +import { ClusterListItem, NodePoolListItem } from '../config/types'; +import AddPool from './add-pool'; +import WorkerPools from './worker-pools'; + +const SubTitle = styled.div` + font-size: var(--font-size-middle); + font-weight: 500; + color: var(--ant-color-text); + margin-block: 24px 16px; +`; + +interface ClusterDetailProps { + provider: string; + data: ClusterListItem; + show: boolean; +} + +const gaugeConfig = { + radius: '100%', + progress: { + show: true, + roundCap: false, + width: 8 + }, + axisLine: { + roundCap: false, + lineStyle: { + width: 8, + color: [ + [0.5, 'rgba(84, 204, 152, 80%)'], + [0.8, 'rgba(250, 173, 20, 80%)'], + [1, 'rgba(255, 77, 79, 80%)'] + ] + } + } +}; + +const ClusterDetail: React.FC = ({ data, show }) => { + const chartHeight = 160; + const [addPoolStatus, setAddPoolStatus] = React.useState({ + open: false, + action: PageAction.CREATE, + title: '', + provider: 'digitalocean' + }); + + // pool action handler + const handleOnAction = (action: string, record: NodePoolListItem) => { + if (action === 'edit') { + setAddPoolStatus({ + open: true, + action: PageAction.CREATE, + title: 'Edit Worker Pool', + provider: data.provider + }); + } + }; + + return ( +
+
+ + + + + + + + + + + + + + +
+ {data.provider === ProviderValueMap.DigitalOcean && ( + <> + Worker Pools + + + )} + { + setAddPoolStatus({ + open: false, + action: PageAction.CREATE, + title: '', + provider: 'digitalocean' + }); + }} + onOk={() => { + setAddPoolStatus({ + open: false, + action: addPoolStatus.action, + title: '', + provider: 'digitalocean' + }); + }} + > +
+ ); +}; + +export default ClusterDetail; diff --git a/src/pages/cluster-management/components/cluster-item.tsx b/src/pages/cluster-management/components/cluster-item.tsx index b32ea247..a5011c65 100644 --- a/src/pages/cluster-management/components/cluster-item.tsx +++ b/src/pages/cluster-management/components/cluster-item.tsx @@ -2,13 +2,15 @@ import DropdownButtons from '@/components/drop-down-buttons'; import GaugeChart from '@/components/echarts/gauge'; import IconFont from '@/components/icon-font'; import StatusTag from '@/components/status-tag'; +import ThemeTag from '@/components/tags-wrapper/theme-tag'; import Card from '@/components/templates/card'; +import { PageAction } from '@/config'; import { DeleteOutlined, EditOutlined, KubernetesOutlined } from '@ant-design/icons'; -import { Card as ACard, Button, Col, Row, Tag } from 'antd'; +import { Card as ACard, Col, Collapse, Row } from 'antd'; import React, { useMemo } from 'react'; import styled from 'styled-components'; import { @@ -18,65 +20,21 @@ import { ProviderValueMap } from '../config'; import { ClusterListItem as ListItem, NodePoolListItem } from '../config/types'; +import AddPool from './add-pool'; +import RegisterCluster from './register-cluster'; import WorkerPools from './worker-pools'; -const CollapseTitle = styled.div` - display: flex; - align-items: center; - gap: 8px; - font-size: var(--font-size-middle); - font-weight: 500; - color: var(--ant-color-text); - height: 32px; - cursor: pointer; -`; - -const Content = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - .chart-wrapper { - flex: 1; - } -`; - -const CardWrapper = styled(ACard)` - text-align: center; - box-shadow: none; - flex: 1; - .ant-card { - box-shadow: none; - } - .ant-card-body { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - gap: 8px; - } - - .label { - font-weight: 500; - font-size: var(--font-size-middle); - } - .value { - font-weight: 500; - color: var(--ant-color-text); - } -`; - -const CardBox = styled.div` - display: flex; - gap: 16px; - flex: 0.5; -`; - const actionItems = [ { key: 'edit', label: 'common.button.edit', icon: }, + { + key: 'view', + label: 'common.button.view', + icon: + }, { key: 'add_worker', label: 'Add Worker', @@ -108,6 +66,60 @@ const actionItems = [ } ]; +const CollapseTitle = styled.div` + display: flex; + align-items: center; + gap: 8px; + font-size: var(--font-size-middle); + font-weight: 500; + color: var(--ant-color-text); + height: 32px; + cursor: pointer; +`; + +const Content = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + .chart-wrapper { + flex: 1; + } +`; + +const CardWrapper = styled(ACard)` + text-align: center; + box-shadow: none !important; + flex: 1; + .ant-card { + box-shadow: none; + } + .ant-card-body { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + padding: 6px 10px; + } + + .label { + font-weight: 400; + margin-bottom: 12px; + font-size: var(--font-size-middle); + color: var(--ant-color-text-secondary); + } + .value { + font-weight: 600; + color: var(--ant-color-text); + font-size: var(--font-size-large); + } +`; + +const CardBox = styled.div` + display: flex; + gap: 16px; + flex: 1; +`; + const Inner = styled.div` display: flex; flex-direction: column; @@ -116,7 +128,7 @@ const Inner = styled.div` cursor: default; .title { - margin-bottom: 16px; + margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between; @@ -139,6 +151,38 @@ const Inner = styled.div` } `; +const CollapseWrapper = styled(Collapse)` + width: 100%; + border: none; + border-radius: 0; + border-top: 1px solid var(--ant-color-split); + background-color: var(--ant-color-fill-quaternary); + .ant-collapse-content { + border-top: 1px solid var(--ant-color-split); + } + .ant-collapse-header { + font-size: var(--font-size-middle); + padding-block: 10px !important; + .ant-collapse-expand-icon { + margin-left: 0 !important; + } + } + table .ant-table-thead tr { + background-color: transparent !important; + th { + border-bottom: 1px solid var(--ant-color-split) !important; + font-weight: 500 !important; + } + } +`; + +const SubTitle = styled.div` + font-size: var(--font-size-middle); + font-weight: 500; + color: var(--ant-color-text); + margin-block: 24px 16px; +`; + interface CardProps { data: ListItem; onSelect?: (key: string, row: ListItem) => void; @@ -165,14 +209,51 @@ const gaugeConfig = { }; const CardItem: React.FC = (props) => { + const chartHeight = 160; const { data, onSelect } = props; const [show, setShow] = React.useState(false); + const [addPoolStatus, setAddPoolStatus] = React.useState({ + open: false, + action: PageAction.CREATE, + title: '', + provider: 'digitalocean' + }); + const [registerClusterStatus, setRegisterClusterStatus] = React.useState({ + open: false + }); + // cluster action handler const handleOnSelect = (key: string) => { + if (key === 'addPool') { + setAddPoolStatus({ + open: true, + action: PageAction.CREATE, + title: 'Add Worker Pool', + provider: data.provider + }); + return; + } + + if (key === 'register_cluster') { + setRegisterClusterStatus({ + open: true + }); + return; + } onSelect?.(key, data); }; - const handleOnAction = (action: string, record: NodePoolListItem) => {}; + // pool action handler + const handleOnAction = (action: string, record: NodePoolListItem) => { + if (action === 'edit') { + setAddPoolStatus({ + open: true, + action: PageAction.CREATE, + title: 'Edit Worker Pool', + provider: data.provider + }); + } + }; const actions = useMemo(() => { return actionItems.filter((item) => { @@ -184,14 +265,82 @@ const CardItem: React.FC = (props) => { }, [data.provider]); return ( - + setShow(!show)} + expandIconPosition="end" + expandIcon={({ isActive }) => ( + + )} + items={[ + { + key: '1', + label: 'More Information', + children: ( + <> +
+ + + + + + + + + + + + + + +
+ {data.provider === ProviderValueMap.DigitalOcean && ( + <> + Worker Pools + + + )} + + ) + } + ]} + > + } + >
{data.name} - - {ProviderLabelMap[data.provider]} - + {ProviderLabelMap[data.provider]} = (props) => {
Workers
-
1
+
1/1
GPUs
@@ -221,64 +370,40 @@ const CardItem: React.FC = (props) => {
2
-
- - - - - - - - - - - - - - -
- {data.provider === ProviderValueMap.DigitalOcean && ( - <> - - - - - - )} + { + setAddPoolStatus({ + open: false, + action: PageAction.CREATE, + title: '', + provider: 'digitalocean' + }); + }} + onOk={() => { + setAddPoolStatus({ + open: false, + action: addPoolStatus.action, + title: '', + provider: 'digitalocean' + }); + }} + > + { + setRegisterClusterStatus({ + open: false + }); + }} + > ); }; diff --git a/src/pages/cluster-management/components/register-cluster.tsx b/src/pages/cluster-management/components/register-cluster.tsx index 647eb2b1..b826c2df 100644 --- a/src/pages/cluster-management/components/register-cluster.tsx +++ b/src/pages/cluster-management/components/register-cluster.tsx @@ -1,44 +1,21 @@ -import ModalFooter from '@/components/modal-footer'; import ScrollerModal from '@/components/scroller-modal/index'; -import SealInput from '@/components/seal-form/seal-input'; -import { PageActionType } from '@/config/types'; -import { useIntl } from '@umijs/max'; -import { Form } from 'antd'; import React from 'react'; -import { - ClusterFormData as FormData, - ClusterListItem as ListItem -} from '../config/types'; -import CloudProvider from './cloud-provider-form'; -import K8SProvider from './k8s-provider-form'; +import { ClusterListItem as ListItem } from '../config/types'; +import RegisterClusterInner from './resiter-cluster-inner'; type AddModalProps = { title: string; - action: PageActionType; open: boolean; - provider: string; // 'kubernetes' | 'custom' | 'digitalocean'; - onOk: (values: FormData) => void; - data?: ListItem; + data: ListItem; onCancel: () => void; }; const AddCluster: React.FC = ({ title, - action, open, - provider, - onOk, data, onCancel }) => { - const [form] = Form.useForm(); - const intl = useIntl(); - - const handleSubmit = () => { - form.submit(); - }; - const handleCancel = () => { - form.resetFields(); onCancel(); }; @@ -52,42 +29,9 @@ const AddCluster: React.FC = ({ maskClosable={false} keyboard={false} width={600} - footer={ - - } + footer={false} > -
- - name="display_name" - rules={[ - { - required: true, - message: intl.formatMessage( - { id: 'common.form.rule.input' }, - { - name: intl.formatMessage({ id: 'common.table.name' }) - } - ) - } - ]} - > - - - {provider === 'digitalocean' && ( - - )} - {provider === 'kubernetes' && ( - - )} - name="description" rules={[{ required: false }]}> - - - + ); }; diff --git a/src/pages/cluster-management/components/resiter-cluster-inner.tsx b/src/pages/cluster-management/components/resiter-cluster-inner.tsx new file mode 100644 index 00000000..c078c652 --- /dev/null +++ b/src/pages/cluster-management/components/resiter-cluster-inner.tsx @@ -0,0 +1,53 @@ +import HighlightCode from '@/components/highlight-code'; +import React, { useEffect } from 'react'; +import { queryClusterToken } from '../apis'; +import { generateRegisterCommand } from '../config'; +import { ClusterListItem as ListItem } from '../config/types'; + +type AddModalProps = { + data: ListItem; +}; +const AddCluster: React.FC = ({ data }) => { + const [code, setCode] = React.useState(''); + const getToken = async () => { + const res = await queryClusterToken(data?.id); + return res.data?.token || ''; + }; + + const getCode = async () => { + try { + const token = await getToken(); + const command = generateRegisterCommand({ + server: window.location.origin, + clusterId: data?.id || 0, + registrationToken: token + }); + setCode(command); + } catch (error) { + setCode( + generateRegisterCommand({ + server: window.location.origin, + clusterId: data?.id || 0, + registrationToken: '{token}' + }) + ); + } + }; + + useEffect(() => { + getCode(); + }, []); + + return ( +
+ +
+ ); +}; + +export default AddCluster; diff --git a/src/pages/cluster-management/components/worker-pools.tsx b/src/pages/cluster-management/components/worker-pools.tsx index 2adfd25e..18e00c9b 100644 --- a/src/pages/cluster-management/components/worker-pools.tsx +++ b/src/pages/cluster-management/components/worker-pools.tsx @@ -71,9 +71,9 @@ const WorkerPools: React.FC = ({ const columns = [ { - title: 'Type', - dataIndex: 'type', - key: 'type' + title: 'Instance Type', + dataIndex: 'instance_type', + key: 'instance_type' }, { title: 'Replicas', @@ -126,7 +126,7 @@ const WorkerPools: React.FC = ({ const mockData = Array.from({ length: 3 }, (_, index) => ({ id: index + 1, key: index, - type: `Type ${index + 1}`, + instance_type: `Type ${index + 1}`, replicas: Math.floor(Math.random() * 10) + 1, batchSize: Math.floor(Math.random() * 100) + 1, gpu: `NVIDIA 4090`, diff --git a/src/pages/cluster-management/config/index.ts b/src/pages/cluster-management/config/index.ts index b182c4dc..e23562f0 100644 --- a/src/pages/cluster-management/config/index.ts +++ b/src/pages/cluster-management/config/index.ts @@ -59,3 +59,12 @@ export const ProviderLabelMap = { [ProviderValueMap.DigitalOcean]: 'Digital Ocean', [ProviderValueMap.Custom]: 'Custom' }; + +export const generateRegisterCommand = (params: { + server: string; + clusterId: number; + registrationToken: string; +}) => { + return `curl -k -L '${params.server}/v2/clusters/${params.clusterId}/manifests' \\ +--header 'Authorization: Bearer ${params.registrationToken}'`; +}; diff --git a/src/pages/dashboard/components/over-view.tsx b/src/pages/dashboard/components/over-view.tsx index 43c91279..c4d60cd2 100644 --- a/src/pages/dashboard/components/over-view.tsx +++ b/src/pages/dashboard/components/over-view.tsx @@ -20,8 +20,8 @@ const renderCardItem = (data: { className={styles['card-body']} >
-
{label}
-
{value}
+
{label}
+
{value}
); diff --git a/src/pages/llmodels/components/gguf-result.tsx b/src/pages/llmodels/components/gguf-result.tsx index d45b7b4a..487e774e 100644 --- a/src/pages/llmodels/components/gguf-result.tsx +++ b/src/pages/llmodels/components/gguf-result.tsx @@ -12,7 +12,7 @@ const GGUFResult: React.FC = () => { /> } title={false} - subTitle="GGUF model is not supported yet." + subTitle="GGUF model is not supported." /> ); }; diff --git a/src/pages/resources/components/add-worker.tsx b/src/pages/resources/components/add-worker.tsx index 59002569..10c3cdb0 100644 --- a/src/pages/resources/components/add-worker.tsx +++ b/src/pages/resources/components/add-worker.tsx @@ -1,64 +1,17 @@ import ScrollerModal from '@/components/scroller-modal'; -import { - AppleOutlined, - LinuxOutlined, - WindowsOutlined -} from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { TabsProps } from 'antd'; import React from 'react'; -import MacOS from './add-worker-macos'; import ContainerInstall from './container-install'; type ViewModalProps = { open: boolean; + token: string; onCancel: () => void; }; const AddWorker: React.FC = (props) => { - const { open, onCancel } = props || {}; + const { open, onCancel, token = '${token}' } = props || {}; const intl = useIntl(); - const [token, setToken] = React.useState(''); - const [activeKey, setActiveKey] = React.useState('container'); - - const items: TabsProps['items'] = [ - { - key: 'container', - label: 'Linux', - icon: , - children: - }, - { - key: 'macos', - label: 'macOS', - icon: , - children: ( - - ) - }, - { - key: 'windows', - label: 'Windows', - icon: , - children: ( - - ) - } - ]; return ( = (props) => { return commandCode?.registerWorker({ server: origin, tag: tag, - token: '${token}', + token: props.token || '${token}', workerip: '${workerip}' }); }, [versionInfo, activeKey, props.token, npuKey]); @@ -53,38 +53,8 @@ const AddWorker: React.FC = (props) => { return (
-
    -
  • - {intl.formatMessage( - { id: 'resources.worker.current.version' }, - { version: versionInfo.version } - )} -
  • -
  • - -
  • -
-

- 1.{' '} - -

-

- 2. {intl.formatMessage({ id: 'resources.worker.add.step2' })}{' '} + 1. {intl.formatMessage({ id: 'resources.worker.add.step2' })}{' '} = (props) => { > )}

- 3. {intl.formatMessage({ id: 'resources.worker.add.step3' })} + 2. {intl.formatMessage({ id: 'resources.worker.add.step3' })}

); diff --git a/src/pages/resources/config/index.ts b/src/pages/resources/config/index.ts index 91302f5b..bcb96d0b 100644 --- a/src/pages/resources/config/index.ts +++ b/src/pages/resources/config/index.ts @@ -22,14 +22,14 @@ export const addWorkerGuide: Record = { mac: { getToken: 'cat /var/lib/gpustack/token', registerWorker(params: { server: string; token: string }) { - return `curl -sfL https://get.gpustack.ai | sh -s - --server-url ${params.server} --token ${params.token}`; + return `curl -sfL https://get.gpustack.ai | sh -s - --server-url ${params.server} --registration ${params.token}`; } }, win: { getToken: 'Get-Content -Path (Join-Path -Path $env:APPDATA -ChildPath "gpustack\\token") -Raw', registerWorker(params: { server: string; token: string }) { - return `Invoke-Expression "& { $((Invoke-WebRequest -Uri 'https://get.gpustack.ai' -UseBasicParsing).Content) } --server-url '${params.server}' --token '${params.token}'"`; + return `Invoke-Expression "& { $((Invoke-WebRequest -Uri 'https://get.gpustack.ai' -UseBasicParsing).Content) } --server-url '${params.server}' --registration '${params.token}'"`; } }, cuda: { @@ -41,11 +41,14 @@ export const addWorkerGuide: Record = { token: string; workerip: string; }) { - return `docker run -d \\ - --net=host \\ - -v /var/lib/gpustack:/var/lib/gpustack \\ - --privileged gpustack/gpustack:xxxx \\ - --registration ${params.token}`; + return `docker run -d --name gpustack \\ + --restart=unless-stopped \\ + --gpus all \\ + --network=host \\ + --ipc=host \\ + -v gpustack-data:/var/lib/gpustack \\ + gpustack/gpustack:${params.tag} \\ + --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`; } }, npu: { @@ -73,7 +76,7 @@ export const addWorkerGuide: Record = { --ipc=host \\ -v gpustack-data:/var/lib/gpustack \\ gpustack/gpustack:${params.tag} \\ - --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; + --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`; } }, npu310p: { @@ -101,7 +104,7 @@ export const addWorkerGuide: Record = { --ipc=host \\ -v gpustack-data:/var/lib/gpustack \\ gpustack/gpustack:${params.tag}-310p \\ - --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; + --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`; } }, musa: { @@ -119,7 +122,7 @@ export const addWorkerGuide: Record = { --ipc=host \\ -v gpustack-data:/var/lib/gpustack \\ gpustack/gpustack:${params.tag} \\ - --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; + --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`; } }, cpu: { @@ -136,7 +139,7 @@ export const addWorkerGuide: Record = { --network=host \\ -v gpustack-data:/var/lib/gpustack \\ gpustack/gpustack:${params.tag} \\ - --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; + --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`; } }, rocm: { @@ -157,7 +160,7 @@ export const addWorkerGuide: Record = { --security-opt seccomp=unconfined \\ -v gpustack-data:/var/lib/gpustack \\ gpustack/gpustack:${params.tag} \\ - --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; + --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`; } }, dcu: { @@ -180,7 +183,7 @@ export const addWorkerGuide: Record = { --security-opt seccomp=unconfined \\ -v gpustack-data:/var/lib/gpustack \\ gpustack/gpustack:${params.tag} \\ - --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; + --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`; } }, corex: { @@ -201,7 +204,7 @@ export const addWorkerGuide: Record = { --ipc=host \\ -v gpustack-data:/var/lib/gpustack \\ gpustack/gpustack:${params.tag} \\ - --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; + --server-url ${params.server} --registration ${params.token} --worker-ip ${params.workerip}`; } }, container: { @@ -216,8 +219,8 @@ export const containerInstallOptions = [ { label: 'Ascend CANN', value: 'npu' }, { label: 'Hygon DTK', value: 'dcu' }, { label: 'Moore Threads MUSA', value: 'musa' }, - { label: 'Iluvatar Corex', value: 'corex' }, - { label: 'CPU', value: 'cpu' } + { label: 'Iluvatar Corex', value: 'corex' } + // { label: 'CPU', value: 'cpu' } ]; export const ModelfileStateMap = {