diff --git a/src/components/collapse-container/index.tsx b/src/components/collapse-container/index.tsx index 5a5b2807..0e4b5477 100644 --- a/src/components/collapse-container/index.tsx +++ b/src/components/collapse-container/index.tsx @@ -14,11 +14,15 @@ const CardStyled = styled(Card)` } } .ant-card-head { + cursor: pointer; background-color: var(--ant-color-fill-quaternary); border-bottom: none; border-radius: var(--ant-border-radius); &:hover { background-color: var(--ant-color-fill-secondary); + .del-btn { + display: block; + } } } `; @@ -28,12 +32,17 @@ const useStyles = createStyles(({ css, token }) => { title: css` font-weight: 400; height: 56px; - font-size: ${token.fontSizeLG}; + font-size: var(--font-size-base); display: flex; justify-content: space-between; align-items: center; cursor: pointer; `, + expandIcon: css` + display: flex; + align-items: center; + gap: 8px; + `, subtitle: css` font-size: 12px; color: ${token.colorTextSecondary}; @@ -48,6 +57,9 @@ const useStyles = createStyles(({ css, token }) => { display: flex; align-items: center; gap: 8px; + .del-btn { + display: none; + } ` }; }); @@ -56,6 +68,7 @@ export interface CollapsibleContainerProps { title?: React.ReactNode; subtitle?: React.ReactNode; right?: React.ReactNode; + deleteBtn?: React.ReactNode; defaultOpen?: boolean; open?: boolean; collapsible?: boolean; @@ -70,6 +83,7 @@ export default function CollapsibleContainer({ title, subtitle, right, + deleteBtn, defaultOpen = true, open, onToggle, @@ -105,19 +119,22 @@ export default function CollapsibleContainer({ return (
- {title &&
{title}
} +
+ + {title &&
{title}
} +
{subtitle &&
{subtitle}
}
- {right} - + {right && {right}} + {deleteBtn && {deleteBtn}}
); diff --git a/src/components/seal-table/components/cell-content.tsx b/src/components/seal-table/components/cell-content.tsx new file mode 100644 index 00000000..d97eb123 --- /dev/null +++ b/src/components/seal-table/components/cell-content.tsx @@ -0,0 +1,178 @@ +import { CheckOutlined, FormOutlined, UndoOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, Input, InputNumber, Tooltip } from 'antd'; +import _ from 'lodash'; +import React, { useContext, useEffect } from 'react'; +import styled from 'styled-components'; +import RowContext from '../row-context'; +import { CellContentProps } from '../types'; + +const CellContentWrapper = styled.div` + max-width: 100%; + display: flex; + align-items: center; +`; + +interface EditButtonsProps { + isEditing: boolean; + editable?: any; + handleSubmit: () => void; + handleUndo: () => void; + handleEdit: () => void; +} + +interface ContentProps { + isEditing: boolean; + current: any; + editable: any; + onChange: (val: any) => void; + row?: any; + render?: (text: any, record: any) => React.ReactNode; +} + +const EditButtons: React.FC = (props) => { + const intl = useIntl(); + const { isEditing, editable, handleSubmit, handleUndo, handleEdit } = props; + if (!editable) { + return null; + } + + if (isEditing) { + return ( + + + + + + + + + ); + } + return ( + + {editable.title || ''} + ) + } + > + + + + ); +}; + +const Content: React.FC = (props) => { + const { editable, current, isEditing, row, render, onChange } = props; + if (isEditing && editable) { + const isNumType = + typeof editable === 'object' && editable?.valueType === 'number'; + return isNumType ? ( + + ) : ( + onChange(e.target.value)} /> + ); + } + + if (render) { + return render(current, row); + } + return current; +}; + +const CellContent: React.FC = (props) => { + const { row, onCell } = useContext(RowContext); + const { dataIndex, render, editable } = props; + const [isEditing, setIsEditing] = React.useState(false); + const [current, setCurrent] = React.useState(row[dataIndex]); + const cachedValue = React.useRef(null); + + const handleEdit = () => { + setIsEditing(true); + }; + + const handleSubmit = async () => { + cachedValue.current = current; + await onCell?.( + { + ...row, + [dataIndex]: current + }, + dataIndex + ); + setIsEditing(false); + }; + + const handleUndo = () => { + setCurrent(cachedValue.current); + setIsEditing(false); + }; + + const handleValueChange = (val: any) => { + setCurrent(val); + }; + + useEffect(() => { + cachedValue.current = row[dataIndex]; + setCurrent(row[dataIndex]); + }, [row[dataIndex]]); + + return ( + + + + + ); +}; + +export default CellContent; diff --git a/src/components/seal-table/components/header.tsx b/src/components/seal-table/components/header.tsx index bd57d386..6bbd0d13 100644 --- a/src/components/seal-table/components/header.tsx +++ b/src/components/seal-table/components/header.tsx @@ -18,6 +18,7 @@ const Header: React.FC = (props) => { title, dataIndex, align, + width, span, headerStyle, sortOrder, @@ -31,6 +32,7 @@ const Header: React.FC = (props) => { sorter={sorter} dataIndex={dataIndex} sortOrder={sortOrder} + width={width} defaultSortOrder={defaultSortOrder} title={title} style={headerStyle} diff --git a/src/components/seal-table/components/seal-column.tsx b/src/components/seal-table/components/table-cell.tsx similarity index 79% rename from src/components/seal-table/components/seal-column.tsx rename to src/components/seal-table/components/table-cell.tsx index d610a41a..9a5ff53e 100644 --- a/src/components/seal-table/components/seal-column.tsx +++ b/src/components/seal-table/components/table-cell.tsx @@ -4,9 +4,34 @@ import { Button, Input, InputNumber, Tooltip } from 'antd'; import classNames from 'classnames'; import _ from 'lodash'; import React, { useContext, useEffect } from 'react'; +import styled from 'styled-components'; import RowContext from '../row-context'; -import '../styles/cell.less'; import { SealColumnProps } from '../types'; +import CellContent from './cell-content'; + +const CellWrapper = styled.div` + padding: var(--ant-table-cell-padding-block) + var(--ant-table-cell-padding-inline); + display: flex; + align-items: center; + justify-content: flex-start; + min-height: 68px; + word-break: break-word; + min-width: 20px; + overflow: hidden; + + &.left { + justify-content: flex-start; + } + + &.right { + justify-content: flex-end; + } + + &.center { + justify-content: center; + } +`; interface EditButtonsProps { isEditing: boolean; @@ -89,7 +114,7 @@ const EditButtons: React.FC = (props) => { ); }; -const CellContent: React.FC = (props) => { +const Content: React.FC = (props) => { const { editable, current, isEditing, row, render, onChange } = props; if (isEditing && editable) { const isNumType = @@ -112,7 +137,7 @@ const CellContent: React.FC = (props) => { return current; }; -const SealColumn: React.FC = (props) => { +const TableCell: React.FC = (props) => { const { row, onCell } = useContext(RowContext); const { dataIndex, render, align, editable } = props; const [isEditing, setIsEditing] = React.useState(false); @@ -150,22 +175,22 @@ const SealColumn: React.FC = (props) => { }, [row[dataIndex]]); return ( -
- - + + > = (props) => { handleSubmit={handleSubmit} handleUndo={handleUndo} > - -
+ */} + + ); }; -export default SealColumn; +export default TableCell; diff --git a/src/components/seal-table/components/table-header.tsx b/src/components/seal-table/components/table-header.tsx index 168f4b39..22deb3fd 100644 --- a/src/components/seal-table/components/table-header.tsx +++ b/src/components/seal-table/components/table-header.tsx @@ -15,6 +15,7 @@ const TableHeader: React.FC = (props) => { sortOrder, onSort, sorter, + width, dataIndex } = props; @@ -27,7 +28,7 @@ const TableHeader: React.FC = (props) => { }; return (
- + ); })} diff --git a/src/components/seal-table/row-context.ts b/src/components/seal-table/row-context.ts index 710cd575..4f810604 100644 --- a/src/components/seal-table/row-context.ts +++ b/src/components/seal-table/row-context.ts @@ -1,5 +1,10 @@ import React from 'react'; -const RowContext = React.createContext({}); +interface RowContextType { + row: Record; + onCell?: (record: any, dataIndex: string) => any; +} + +const RowContext = React.createContext({} as RowContextType); export default RowContext; diff --git a/src/components/seal-table/styles/cell.less b/src/components/seal-table/styles/cell.less index fd9d838d..f6a5260a 100644 --- a/src/components/seal-table/styles/cell.less +++ b/src/components/seal-table/styles/cell.less @@ -14,15 +14,15 @@ line-height: 18px; } - &-left { + &.left { justify-content: flex-start; } - &-right { + &.right { justify-content: flex-end; } - &-center { + &.center { justify-content: center; } } diff --git a/src/components/seal-table/types.ts b/src/components/seal-table/types.ts index 58419054..e07f6f1f 100644 --- a/src/components/seal-table/types.ts +++ b/src/components/seal-table/types.ts @@ -1,5 +1,16 @@ import React from 'react'; +export interface CellContentProps { + dataIndex: string; + render?: (text: any, record: any) => React.ReactNode; + editable?: + | boolean + | { + valueType?: 'text' | 'number' | 'date' | 'datetime' | 'time'; + title?: React.ReactNode; + }; +} + export interface SealColumnProps { title: React.ReactNode; render?: (text: any, record: any) => React.ReactNode; @@ -33,6 +44,7 @@ export interface TableHeaderProps { firstCell?: boolean; lastCell?: boolean; align?: 'left' | 'center' | 'right'; + width?: number | string; } export interface RowSelectionProps { diff --git a/src/components/status-tag/index.tsx b/src/components/status-tag/index.tsx index c74f642f..7d3a662c 100644 --- a/src/components/status-tag/index.tsx +++ b/src/components/status-tag/index.tsx @@ -20,6 +20,7 @@ export const StatusMaps = { }; type StatusTagProps = { + style?: React.CSSProperties; statusValue: { status: StatusType; text: string; @@ -42,6 +43,7 @@ type StatusTagProps = { }; const StatusTag: React.FC = ({ + style, statusValue, download, extra, @@ -149,7 +151,8 @@ const StatusTag: React.FC = ({ })} style={{ color: statusColor?.text, - border: `1px solid ${statusColor?.border || statusColor?.text}` + border: `1px solid ${statusColor?.border || statusColor?.text}`, + ...style }} > {statusValue.message ? ( diff --git a/src/components/templates/card.tsx b/src/components/templates/card.tsx index 3739dce4..fbe05832 100644 --- a/src/components/templates/card.tsx +++ b/src/components/templates/card.tsx @@ -70,12 +70,12 @@ const Icon = styled.div` display: flex; align-items: center; margin-right: 16px; - font-size: 46px; + font-size: 32px; `; const Header = styled.div` font-weight: bold; - font-size: 16px; + font-size: var(--font-size-base); display: flex; align-items: center; justify-content: space-between; diff --git a/src/config/theme/dark.ts b/src/config/theme/dark.ts index 5c2ac79e..91c774c1 100644 --- a/src/config/theme/dark.ts +++ b/src/config/theme/dark.ts @@ -7,6 +7,9 @@ export default { inputFontSize: 14, inputFontSizeLG: 14 }, + Steps: { + descriptionMaxWidth: 200 + }, Table: { headerBorderRadius: 4, cellPaddingInline: 16, diff --git a/src/config/theme/light.ts b/src/config/theme/light.ts index 817d1fac..d9971fd0 100644 --- a/src/config/theme/light.ts +++ b/src/config/theme/light.ts @@ -7,6 +7,9 @@ export default { inputFontSize: 14, inputFontSizeLG: 14 }, + Steps: { + descriptionMaxWidth: 200 + }, Table: { headerBorderRadius: 4, cellPaddingInline: 16, diff --git a/src/global.less b/src/global.less index fe9b0504..594276f3 100644 --- a/src/global.less +++ b/src/global.less @@ -827,6 +827,7 @@ body { .ant-pro-page-container-affix .ant-affix .ant-pro-page-container-warp { box-shadow: none; + border-bottom: 1px solid var(--ant-color-split); } .ant-page-header .ant-page-header-heading-extra { diff --git a/src/hooks/use-watch-list.ts b/src/hooks/use-watch-list.ts new file mode 100644 index 00000000..dc62e8c3 --- /dev/null +++ b/src/hooks/use-watch-list.ts @@ -0,0 +1,83 @@ +import useSetChunkRequest from '@/hooks/use-chunk-request'; +import useUpdateChunkedList from '@/hooks/use-update-chunk-list'; +import { request } from '@umijs/max'; +import { useMemoizedFn } from 'ahooks'; +import _ from 'lodash'; +import { useEffect, useRef, useState } from 'react'; + +export default function useWatchList>(API: string) { + const watchAPI = API; + const [watchDataList, setWatchDataList] = useState([]); + const chunkRequestRef = useRef(null); + const listRequestTokenRef = useRef(null); + + const { setChunkRequest, createAxiosToken } = useSetChunkRequest(); + + const { updateChunkedList, cacheDataListRef: cacheWatchDataListRef } = + useUpdateChunkedList({ + dataList: watchDataList, + limit: 100, + setDataList: setWatchDataList + }); + + const updateWatchDataListHandler = (list: any) => { + // filter the data + _.each(list, (data: any) => { + updateChunkedList(data); + }); + }; + + const createWatchChunkRequest = useMemoizedFn(async () => { + chunkRequestRef.current?.current?.cancel?.(); + try { + chunkRequestRef.current = setChunkRequest({ + url: `${watchAPI}`, + params: {}, + handler: updateWatchDataListHandler + }); + } catch (error) { + // ignore + } + }); + + const queryAllDataList = async ( + params: Global.SearchParams, + options?: any + ) => { + return request>(watchAPI, { + params, + method: 'GET', + cancelToken: options?.token + }); + }; + + const getAllDataList = useMemoizedFn(async () => { + try { + listRequestTokenRef.current?.cancel?.(); + listRequestTokenRef.current = createAxiosToken(); + const params = { + page: 1, + perPage: 100 + }; + const res: any = await queryAllDataList(params, { + token: listRequestTokenRef.current.token + }); + cacheWatchDataListRef.current = res.items || []; + setWatchDataList(res.items || []); + } catch (error) { + // ignore + } + }); + + useEffect(() => { + createWatchChunkRequest(); + return () => { + chunkRequestRef.current?.cancel?.(); + listRequestTokenRef.current?.cancel?.(); + }; + }, []); + + return { + watchDataList + }; +} diff --git a/src/pages/cluster-management/cluster-create.tsx b/src/pages/cluster-management/cluster-create.tsx index 4177753d..b7c9ad1f 100644 --- a/src/pages/cluster-management/cluster-create.tsx +++ b/src/pages/cluster-management/cluster-create.tsx @@ -5,11 +5,11 @@ import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; import _ from 'lodash'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; -import { createCluster, queryCredentialList } from './apis'; +import { createCluster, queryClusterToken, queryCredentialList } from './apis'; import ClusterSteps from './components/cluster-steps'; import FooterButtons from './components/footer-buttons'; import ProviderCatalog from './components/provider-catalog'; -import { providerList, ProviderType } from './config'; +import { providerList, ProviderType, ProviderValueMap } from './config'; import { ClusterFormData } from './config/types'; import { moduleMap, moduleRegistry } from './step-forms/module-registry'; import useStepList from './step-forms/use-step-list'; @@ -21,6 +21,19 @@ const Container = styled.div` align-items: center; `; +const Nav = styled.div` + display: flex; + align-items: center; + height: 72px; + font-weight: 400; + font-size: 20px; + color: var(--ant-color-text-tertiary); + .level-2 { + color: var(--ant-color-text); + font-weight: 600; + } +`; + const Content = styled.div` width: 600px; `; @@ -28,11 +41,9 @@ const Content = styled.div` const HeaderContainer = styled.div` position: relative; display: grid; - grid-template-columns: auto 1fr; + grid-template-columns: 1fr; align-items: center; padding-inline: var(--layout-content-header-inlinepadding); - border-bottom: 1px solid var(--ant-color-split); - min-height: 72px; .text { margin-right: 16px; padding-right: 16px; @@ -67,7 +78,7 @@ const ClusterCreate = () => { cluster_id: 0 }); const [extraData, setExtraData] = useState({ - provider: null + provider: ProviderValueMap.Custom } as ClusterFormData); const [formValues, setFormValues] = useState>({}); @@ -237,7 +248,12 @@ const ClusterCreate = () => { ...extraData, ...(typeof values === 'object' ? values : {}) }; - await createCluster({ data }); + const res = await createCluster({ data }); + const info = await queryClusterToken({ id: res.id }); + setRegistrationInfo({ + ...info, + cluster_id: res.id + }); return true; }; @@ -252,10 +268,6 @@ const ClusterCreate = () => { return ( { showButtons={showButtons} /> ]} + header={{ + title: ( +
+ + +
+ ), + style: { + paddingInline: 'var(--layout-content-header-inlinepadding)' + }, + breadcrumb: {} + }} pageHeaderRender={() => ( - - Create Cluster - + { } = useTableFetch({ fetchAPI: queryClusterList, deleteAPI: deleteCluster, + watch: true, + API: CLUSTERS_API, contentForDelete: 'menu.clusterManagement.clusters' }); + const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API); const [expandAtom, setExpandAtom] = useAtom(expandKeysAtom); const { handleExpandChange, @@ -103,13 +110,13 @@ const Credentials: React.FC = () => { action: PageActionType; currentData?: ListItem; title: string; - provider: string; + provider: ProviderType; }>({ open: false, action: PageAction.CREATE, currentData: undefined, title: '', - provider: '' + provider: null }); const [credentialList, setCredentialList] = useState< @@ -145,7 +152,7 @@ const Credentials: React.FC = () => { { id: 'clusters.add.cluster' }, { cluster: clusterLabel } ), - provider: value + provider: value as ProviderType }); }; @@ -154,14 +161,13 @@ const Credentials: React.FC = () => { open: true, action: PageAction.CREATE, title: intl.formatMessage({ id: 'clusters.button.addNodePool' }), - provider: row.provider, + provider: row.provider as ProviderType, clusterId: row.id }); }; const handleClickDropdown = (item: any) => { navigate(`/cluster-management/clusters/create?action=${PageAction.CREATE}`); - // handleAddCluster(item.key); }; const handleModalOk = async (data: FormData) => { @@ -183,7 +189,7 @@ const Credentials: React.FC = () => { action: PageAction.CREATE, currentData: undefined, title: '', - provider: '' + provider: null }); message.success(intl.formatMessage({ id: 'common.message.success' })); } catch (error) { @@ -192,7 +198,7 @@ const Credentials: React.FC = () => { action: PageAction.CREATE, currentData: undefined, title: '', - provider: '' + provider: null }); } }; @@ -204,7 +210,7 @@ const Credentials: React.FC = () => { action: PageAction.CREATE, currentData: undefined, title: '', - provider: '' + provider: null }); }; @@ -300,6 +306,10 @@ const Credentials: React.FC = () => { } ); + const setDisableExpand = (row: ClusterListItem) => { + return row.provider !== ProviderValueMap.DigitalOcean; + }; + useEffect(() => { const fetchCredentialList = async () => { const data = await queryCredentialList({ page: 1, perPage: 100 }); @@ -356,60 +366,39 @@ const Credentials: React.FC = () => { handleDeleteByBatch={handleDeleteBatch} handleClickPrimary={handleClickDropdown} > - - {/* */} + > + +
{ open: false, action: PageAction.CREATE, title: '', - provider: ProviderValueMap.DigitalOcean, + provider: ProviderValueMap.DigitalOcean as ProviderType, clusterId: 0 }); }} diff --git a/src/pages/cluster-management/components/add-cluster.tsx b/src/pages/cluster-management/components/add-cluster.tsx index 29b3e59d..dd3516fa 100644 --- a/src/pages/cluster-management/components/add-cluster.tsx +++ b/src/pages/cluster-management/components/add-cluster.tsx @@ -2,6 +2,7 @@ import ModalFooter from '@/components/modal-footer'; import ScrollerModal from '@/components/scroller-modal/index'; import { PageActionType } from '@/config/types'; import React, { useRef } from 'react'; +import { ProviderType } from '../config'; import { ClusterFormData as FormData, ClusterListItem as ListItem @@ -13,7 +14,7 @@ type AddModalProps = { action: PageActionType; open: boolean; currentData?: ListItem; // Used when action is EDIT - provider: string; + provider: ProviderType; credentialList: Global.BaseOption[]; onOk: (values: FormData) => void; onCancel: () => void; diff --git a/src/pages/cluster-management/components/add-worker-step.tsx b/src/pages/cluster-management/components/add-worker-step.tsx index f2273306..0a99776d 100644 --- a/src/pages/cluster-management/components/add-worker-step.tsx +++ b/src/pages/cluster-management/components/add-worker-step.tsx @@ -20,10 +20,10 @@ type AddModalProps = { const AddWorkerStep: React.FC = ({ registrationInfo }) => { return (
- Supported Hardware Platforms - - Execute Command + Execute Command + Supported GPUs +
); }; diff --git a/src/pages/cluster-management/components/cloud-provider-form.tsx b/src/pages/cluster-management/components/cloud-provider-form.tsx index 561a4092..db041ae9 100644 --- a/src/pages/cluster-management/components/cloud-provider-form.tsx +++ b/src/pages/cluster-management/components/cloud-provider-form.tsx @@ -92,12 +92,7 @@ const CloudProvider: React.FC = (props) => { rules={[ { required: true, - message: intl.formatMessage( - { id: 'common.form.rule.input' }, - { - name: 'credential' - } - ) + message: getRuleMessage('input', 'clusters.credential.title') } ]} > diff --git a/src/pages/cluster-management/components/cluster-steps.tsx b/src/pages/cluster-management/components/cluster-steps.tsx index 6b0ac765..60b93be1 100644 --- a/src/pages/cluster-management/components/cluster-steps.tsx +++ b/src/pages/cluster-management/components/cluster-steps.tsx @@ -5,14 +5,24 @@ import styled from 'styled-components'; const { Step } = Steps; const Wrapper = styled.div` - padding-block: 20px; + padding-block: 30px; background-color: var(--ant-color-bg-container); .ant-steps-item-description { max-width: 300px !important; color: var(--ant-color-text-description) !important; } .ant-steps-item-content > .ant-steps-item-title { - font-weight: 600; + // font-weight: 600; + } + .ant-steps-item { + .ant-steps-item-container { + // .ant-steps-item-icon { + // margin-inline-start: 0 !important; + // } + // .ant-steps-item-tail { + // margin-inline-start: 0 !important; + // } + } } `; @@ -25,7 +35,7 @@ const ClusterSteps: React.FC<{ return ( - + {steps.map((step) => ( = forwardRef((props, ref) => { const intl = useIntl(); const { getRuleMessage } = useAppUtils(); const labels = Form.useWatch('labels', form); - const instance_type = Form.useWatch('instance_type', form); + const title = Form.useWatch('name', form); useEffect(() => { if (currentData) { @@ -88,13 +91,12 @@ const PoolForm: React.FC = forwardRef((props, ref) => { return ( - {showDelete && ( -
+ deleteBtn={ + showDelete && ( -
- )} + ) + } + >
= forwardRef((props, ref) => { }} > + + name="name" + rules={[ + { + required: true, + message: getRuleMessage('input', 'common.table.name') + } + ]} + > + + name="instance_type" rules={[ @@ -135,6 +154,7 @@ const PoolForm: React.FC = forwardRef((props, ref) => { id: 'clusters.workerpool.instanceType' })} required + disabled={action === PageAction.EDIT} > diff --git a/src/pages/cluster-management/components/pool-rows.tsx b/src/pages/cluster-management/components/pool-rows.tsx index b9906c71..6ba5fd46 100644 --- a/src/pages/cluster-management/components/pool-rows.tsx +++ b/src/pages/cluster-management/components/pool-rows.tsx @@ -1,5 +1,7 @@ import DeleteModal from '@/components/delete-modal'; +import CellContent from '@/components/seal-table/components/cell-content'; import RowChildren from '@/components/seal-table/components/row-children'; +import RowContext from '@/components/seal-table/row-context'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import { useIntl } from '@umijs/max'; @@ -11,7 +13,6 @@ import { ProviderType } from '../config'; import { NodePoolFormData, NodePoolListItem } from '../config/types'; import usePoolsColumns from '../hooks/use-pools-columns'; import AddPool from './add-pool'; - interface PoolRowsProps { dataList: NodePoolListItem[]; provider: ProviderType; @@ -58,6 +59,18 @@ const PoolRows: React.FC = ({ } }; + const handleOnCell = async (row: NodePoolListItem, dataIndex: string) => { + console.log('handleOncell===', row, dataIndex); + try { + await updateWorkerPool({ + data: row, + id: row.id + }); + message.success(intl.formatMessage({ id: 'common.message.success' })); + } catch (error) { + // error + } + }; const handleEdit = (action: string, record: NodePoolListItem) => { if (action === 'edit') { setAddPoolStatus({ @@ -97,6 +110,7 @@ const PoolRows: React.FC = ({ }); const columns = usePoolsColumns(onSelect); + return ( <> {dataList?.map((data: NodePoolListItem) => { @@ -105,23 +119,32 @@ const PoolRows: React.FC = ({ key={data.id} style={{ borderRadius: 'var(--ant-table-header-border-radius)' }} > - - - {columns.map((col: Record) => { - return ( - - {col.render + + + + {columns.map((col: Record) => { + return ( + + {/* {col.render ? col.render(data[col.dataIndex as string], data) - : data[col.dataIndex as string]} - - ); - })} - - + : data[col.dataIndex as string]} */} + + + ); + })} + + +
); })} diff --git a/src/pages/cluster-management/components/provider-catalog.tsx b/src/pages/cluster-management/components/provider-catalog.tsx index 980af83a..4556af26 100644 --- a/src/pages/cluster-management/components/provider-catalog.tsx +++ b/src/pages/cluster-management/components/provider-catalog.tsx @@ -21,7 +21,7 @@ const Title = styled.span` align-items: center; justify-content: space-between; font-weight: 700; - font-size: 18px; + font-size: 16px; margin-block: 16px 24px; `; diff --git a/src/pages/cluster-management/components/support-hardware.tsx b/src/pages/cluster-management/components/support-hardware.tsx index e3ae70a9..0d96d2db 100644 --- a/src/pages/cluster-management/components/support-hardware.tsx +++ b/src/pages/cluster-management/components/support-hardware.tsx @@ -13,10 +13,9 @@ const ProviderImage = ({ src, showBg }: { src: string; showBg?: boolean }) => { ); }; @@ -37,7 +36,7 @@ const supportedHardPlatforms = [ icon: ( ) }, diff --git a/src/pages/cluster-management/components/worker-pools.tsx b/src/pages/cluster-management/components/worker-pools.tsx index ecd960e6..82ab7e2f 100644 --- a/src/pages/cluster-management/components/worker-pools.tsx +++ b/src/pages/cluster-management/components/worker-pools.tsx @@ -14,7 +14,7 @@ import { updateWorkerPool, WORKER_POOLS_API } from '../apis'; -import { ProviderValueMap } from '../config'; +import { ProviderType, ProviderValueMap } from '../config'; import { NodePoolListItem as ListItem, NodePoolFormData @@ -60,7 +60,7 @@ const WorkerPools = () => { open: false, action: PageAction.CREATE, title: '', - provider: ProviderValueMap.DigitalOcean, + provider: ProviderValueMap.DigitalOcean as ProviderType, currentData: null as ListItem | null, clusterId: 0 as number | string }); @@ -75,7 +75,7 @@ const WorkerPools = () => { { id: 'common.button.edit.item' }, { name: record.instance_type } ), - provider: searchParams.get('provider') || '', + provider: searchParams.get('provider') as ProviderType, currentData: record, clusterId: searchParams.get('id') || 0 }); @@ -87,7 +87,7 @@ const WorkerPools = () => { open: true, action: PageAction.CREATE, title: intl.formatMessage({ id: 'clusters.button.addNodePool' }), - provider: searchParams.get('provider') || '', + provider: searchParams.get('provider') as ProviderType, clusterId: searchParams.get('id') || 0, currentData: null }); @@ -177,7 +177,7 @@ const WorkerPools = () => { open: false, action: PageAction.CREATE, title: '', - provider: ProviderValueMap.DigitalOcean, + provider: ProviderValueMap.DigitalOcean as ProviderType, currentData: null, clusterId: 0 }); diff --git a/src/pages/cluster-management/config/index.ts b/src/pages/cluster-management/config/index.ts index aeaee77f..dbd560c1 100644 --- a/src/pages/cluster-management/config/index.ts +++ b/src/pages/cluster-management/config/index.ts @@ -56,7 +56,7 @@ export const providerList = [ icon: React.cloneElement(icons.Docker, { style: { color: 'var(--ant-color-primary)' } }), - group: 'default' + group: 'Self-Managed' }, { label: 'Kubernetes', @@ -66,7 +66,7 @@ export const providerList = [ icon: React.cloneElement(icons.KubernetesOutlined, { style: { color: 'var(--ant-color-primary)' } }), - group: 'default' + group: 'Self-Managed' }, { label: 'DigitalOcean', diff --git a/src/pages/cluster-management/config/types.ts b/src/pages/cluster-management/config/types.ts index c77db981..4dadeb5a 100644 --- a/src/pages/cluster-management/config/types.ts +++ b/src/pages/cluster-management/config/types.ts @@ -22,10 +22,21 @@ export interface CredentialListItem { updated_at: string; } -export interface NodePoolListItem { +export interface NodePoolFormData { + name: string; + instance_type: string; + os_image: string; + replicas: number; + batch_size: number; + labels: Record; + cloud_options: Record; +} + +export interface NodePoolListItem extends NodePoolFormData { id: number; instance_type: string; replicas: number; + workers: number; batch_size: number; labels: Record; cloud_options: Record; @@ -35,20 +46,11 @@ export interface NodePoolListItem { cluster_id: number; } -export interface NodePoolFormData { - instance_type: string; - os_image: string; - replicas: number; - batch_size: number; - labels: Record; - cloud_options: Record; -} - export interface ClusterListItem { name: string; display_name: string; description: string; - provider: string; + provider: ProviderType; credential_id: number; zone: string; region: string; diff --git a/src/pages/cluster-management/hooks/use-cluster-columns.tsx b/src/pages/cluster-management/hooks/use-cluster-columns.tsx index f9ce56d9..1d66f5c9 100644 --- a/src/pages/cluster-management/hooks/use-cluster-columns.tsx +++ b/src/pages/cluster-management/hooks/use-cluster-columns.tsx @@ -48,7 +48,11 @@ const useClusterColumns = ( title: intl.formatMessage({ id: 'clusters.table.provider' }), dataIndex: 'provider', span: 3, - render: (value: string) => {ProviderLabelMap[value]} + render: (value: string) => ( + + {ProviderLabelMap[value]} + + ) }, { title: intl.formatMessage({ id: 'common.table.status' }), @@ -90,7 +94,9 @@ const useClusterColumns = ( dataIndex: 'created_at', span: 4, render: (value: string) => ( - {dayjs(value).format('YYYY-MM-DD HH:mm:ss')} + + {dayjs(value).format('YYYY-MM-DD HH:mm:ss')} + ) }, { diff --git a/src/pages/cluster-management/hooks/use-pools-columns.tsx b/src/pages/cluster-management/hooks/use-pools-columns.tsx index 4966da7f..c64dd387 100644 --- a/src/pages/cluster-management/hooks/use-pools-columns.tsx +++ b/src/pages/cluster-management/hooks/use-pools-columns.tsx @@ -1,6 +1,5 @@ import AutoTooltip from '@/components/auto-tooltip'; import DropdownButtons from '@/components/drop-down-buttons'; -import LabelsCell from '@/components/label-cell'; import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; import { ColumnsType } from 'antd/es/table'; @@ -34,9 +33,9 @@ const usePoolsColumns = ( return useMemo(() => { return [ { - title: intl.formatMessage({ id: 'clusters.workerpool.instanceType' }), - dataIndex: 'instance_type', - key: 'instance_type', + title: intl.formatMessage({ id: 'common.table.name' }), + dataIndex: 'name', + key: 'name', ellipsis: { showTitle: false }, @@ -51,16 +50,21 @@ const usePoolsColumns = ( ) }, { - title: intl.formatMessage({ id: 'clusters.workerpool.replicas' }), - dataIndex: 'replicas', + title: intl.formatMessage({ id: 'clusters.workerpool.instanceType' }), + dataIndex: 'instance_type', + key: 'instance_type', + ellipsis: { + showTitle: false + }, span: 3, - key: 'replicas' - }, - { - title: intl.formatMessage({ id: 'clusters.workerpool.batchSize' }), - dataIndex: 'batch_size', - key: 'batch_size', - span: 3 + style: { + paddingLeft: 12 + }, + render: (text: string) => ( + + {text} + + ) }, { title: intl.formatMessage({ id: 'clusters.workerpool.osImage' }), @@ -70,6 +74,9 @@ const usePoolsColumns = ( ellipsis: { showTitle: false }, + style: { + paddingLeft: 16 + }, render: (text: string) => ( {text} @@ -77,21 +84,46 @@ const usePoolsColumns = ( ) }, { - title: intl.formatMessage({ id: 'resources.table.labels' }), - dataIndex: 'labels', - key: 'labels', - width: 200, - span: 4, + title: 'Workers', + dataIndex: 'replicas', + span: 3, + key: 'replicas', + style: { + // textAlign: 'center' + paddingLeft: 4 + }, + editable: { + valueType: 'number', + title: intl.formatMessage({ id: 'models.table.replicas.edit' }) + }, render: (text: string, record: ListItem) => ( - + + {record.workers} / {record.replicas} + ) }, + { + title: intl.formatMessage({ id: 'clusters.workerpool.batchSize' }), + dataIndex: 'batch_size', + key: 'batch_size', + span: 4 + }, + // { + // title: intl.formatMessage({ id: 'resources.table.labels' }), + // dataIndex: 'labels', + // key: 'labels', + // width: 200, + // span: 4, + // render: (text: string, record: ListItem) => ( + // + // ) + // }, { title: intl.formatMessage({ id: 'common.table.createTime' }), dataIndex: 'create_at', key: 'created_at', span: 4, - showSorterTooltip: false, + showSorterTootip: false, defaultSortOrder: 'descend', sortOrder: sortOrder, sorter: false, @@ -101,7 +133,11 @@ const usePoolsColumns = ( style: { paddingLeft: 42 }, - render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm:ss') + render: (text: string) => ( + + {dayjs(text).format('YYYY-MM-DD HH:mm:ss')} + + ) }, { title: intl.formatMessage({ id: 'common.table.operation' }), diff --git a/src/pages/cluster-management/step-forms/use-step-list.tsx b/src/pages/cluster-management/step-forms/use-step-list.tsx index 10a57bc3..3d96774a 100644 --- a/src/pages/cluster-management/step-forms/use-step-list.tsx +++ b/src/pages/cluster-management/step-forms/use-step-list.tsx @@ -15,7 +15,7 @@ export default function useStepList() { () => [ { title: 'Select Cloud Provider', - content: 'Choose the cloud provider for your cluster.', + content: '', showButtons: (provider?: ProviderType) => { return { previous: false, @@ -31,7 +31,7 @@ export default function useStepList() { }, { title: 'Configure Cluster Settings', - content: 'Set up the basic configuration for your cluster.', + content: '', showButtons: (provider?: ProviderType) => { return { previous: true, @@ -47,7 +47,7 @@ export default function useStepList() { }, { title: 'Add Worker Pools', - content: 'Define the worker pools for your cluster.', + content: '', showButtons: (provider?: ProviderType) => { return { previous: true, @@ -64,7 +64,7 @@ export default function useStepList() { }, { title: 'Add Worker', - content: 'Add a worker node to your cluster.', + content: '', showButtons: (provider?: ProviderType) => { return { previous: false, @@ -80,7 +80,7 @@ export default function useStepList() { }, { title: 'Register Cluster', - content: 'Register your cluster with the chosen provider.', + content: '', showButtons: (provider?: ProviderType) => { return { previous: false, diff --git a/src/pages/cluster-management/step-forms/worker-pools-form.tsx b/src/pages/cluster-management/step-forms/worker-pools-form.tsx index 7b0b1817..28f8ed25 100644 --- a/src/pages/cluster-management/step-forms/worker-pools-form.tsx +++ b/src/pages/cluster-management/step-forms/worker-pools-form.tsx @@ -53,7 +53,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { [ 0, { - instance_type: 'Pool-1' + name: 'Pool-1' } ] ]) as Map @@ -70,7 +70,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { const newId = updateCount(); setWorkerPoolList((prev) => new Map(prev).set(newId, { - instance_type: `Pool-${newId + 1}` + name: `Pool-${newId + 1}` } as NodePoolFormData) ); setActiveKey((prev) => new Set([newId])); @@ -193,32 +193,30 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { > {Array.from(workerPoolList.keys()).map((key, index) => ( -
- - - { - if (el) { - formRefs.current[key] = el; - } - }} - collapseProps={{ - collapsible: true, - open: activeKey.has(key), - defaultOpen: activeKey.has(key), - onToggle: (open: boolean) => handleOnToggle(open, key) - }} - showDelete={workerPoolList.size > 1} - onFinish={handleOnFinish} - provider={provider} - currentData={workerPoolList.get(key)} - onDelete={() => handleRemovePool(key)} - > - - -
+ + + { + if (el) { + formRefs.current[key] = el; + } + }} + collapseProps={{ + collapsible: true, + open: activeKey.has(key), + defaultOpen: activeKey.has(key), + onToggle: (open: boolean) => handleOnToggle(open, key) + }} + showDelete={workerPoolList.size > 1} + onFinish={handleOnFinish} + provider={provider} + currentData={workerPoolList.get(key)} + onDelete={() => handleRemovePool(key)} + > + + ))} ); diff --git a/src/pages/login/components/login-form.tsx b/src/pages/login/components/login-form.tsx index fa2e5aab..50daf8df 100644 --- a/src/pages/login/components/login-form.tsx +++ b/src/pages/login/components/login-form.tsx @@ -106,7 +106,7 @@ const LoginForm = () => { logo diff --git a/src/pages/resources/apis/index.ts b/src/pages/resources/apis/index.ts index 948ff736..664d3050 100644 --- a/src/pages/resources/apis/index.ts +++ b/src/pages/resources/apis/index.ts @@ -7,6 +7,14 @@ export const WORKERS_API = '/workers'; export const GPU_DEVICES_API = '/gpu-devices'; export const MODEL_FILES_API = '/model-files'; +const matchFilename = (disposition: string | null): string | undefined => { + if (!disposition) return ''; + + const match = disposition.match(/filename="?([^"]+)"?/); + const filename = match ? match[1] : ''; + return filename; +}; + // download stream data and save as a csv file export async function downloadWorkerPrivateKey({ id, @@ -17,9 +25,13 @@ export async function downloadWorkerPrivateKey({ }) { try { const res = await fetch(`/v1${WORKERS_API}/${id}/privatekey`); + // header + const contentDispostion = res.headers.get('content-Disposition'); + const filename = + matchFilename(contentDispostion) || `${name}-privatekey.pem`; if (res.ok) { const blob = await res.blob(); - downloadFile(blob, `${name}-privatekey.csv`); + downloadFile(blob, filename); } } catch (error) { message.error('Download failed');