refactor: cluster list
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
// models expand keys: create, update , delete,
|
||||||
|
export const expandKeysAtom = atom<string[]>([]);
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import IconFont from '@/components/icon-font';
|
||||||
|
import { Card } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const CardStyled = styled(Card)`
|
||||||
|
box-shadow: none !important;
|
||||||
|
&.isOpen {
|
||||||
|
.ant-card-head {
|
||||||
|
border-bottom: 1px solid var(--ant-color-border-secondary);
|
||||||
|
border-radius: var(--ant-border-radius) var(--ant-border-radius) 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-card-head {
|
||||||
|
background-color: var(--ant-color-fill-quaternary);
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: var(--ant-border-radius);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--ant-color-fill-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token }) => {
|
||||||
|
return {
|
||||||
|
title: css`
|
||||||
|
font-weight: 400;
|
||||||
|
height: 56px;
|
||||||
|
font-size: ${token.fontSizeLG};
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
`,
|
||||||
|
subtitle: css`
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${token.colorTextSecondary};
|
||||||
|
`,
|
||||||
|
content: css`
|
||||||
|
padding-top: 8px;
|
||||||
|
`,
|
||||||
|
left: css`
|
||||||
|
flex: 1;
|
||||||
|
`,
|
||||||
|
right: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface CollapsibleContainerProps {
|
||||||
|
title?: React.ReactNode;
|
||||||
|
subtitle?: React.ReactNode;
|
||||||
|
right?: React.ReactNode;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
collapsible?: boolean;
|
||||||
|
onToggle?: (open: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
variant?: 'outlined' | 'borderless' | undefined;
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CollapsibleContainer({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
right,
|
||||||
|
defaultOpen = true,
|
||||||
|
open,
|
||||||
|
onToggle,
|
||||||
|
disabled = false,
|
||||||
|
variant = 'borderless',
|
||||||
|
className = '',
|
||||||
|
collapsible,
|
||||||
|
children
|
||||||
|
}: CollapsibleContainerProps) {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const isControlled = typeof open === 'boolean';
|
||||||
|
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
||||||
|
const isOpen = collapsible
|
||||||
|
? isControlled
|
||||||
|
? (open as boolean)
|
||||||
|
: internalOpen
|
||||||
|
: true;
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (disabled || !collapsible) return;
|
||||||
|
const next = !isOpen;
|
||||||
|
if (!isControlled) setInternalOpen(next);
|
||||||
|
onToggle?.(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [height, setHeight] = useState(isOpen ? 'auto' : '0px');
|
||||||
|
|
||||||
|
const renderTitle = () => {
|
||||||
|
if (!collapsible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.title} onClick={toggle}>
|
||||||
|
<div className={styles.left}>
|
||||||
|
{title && <div>{title}</div>}
|
||||||
|
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
|
||||||
|
</div>
|
||||||
|
<div className={styles.right}>
|
||||||
|
{right}
|
||||||
|
<IconFont
|
||||||
|
rotate={isOpen ? 180 : 0}
|
||||||
|
type="icon-down"
|
||||||
|
style={{
|
||||||
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
fontSize: 12
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!collapsible) {
|
||||||
|
setHeight('auto');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isOpen) {
|
||||||
|
const scrollHeight = contentRef.current?.scrollHeight || 0;
|
||||||
|
setHeight(scrollHeight + 'px');
|
||||||
|
const timer = setTimeout(() => setHeight('auto'), 200);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
} else {
|
||||||
|
const scrollHeight = contentRef.current?.scrollHeight || 0;
|
||||||
|
setHeight(scrollHeight + 'px');
|
||||||
|
requestAnimationFrame(() => setHeight('0px'));
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
}, [isOpen, collapsible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardStyled
|
||||||
|
className={classNames(className, { collapsible, disabled, isOpen })}
|
||||||
|
variant={variant}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={renderTitle()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={contentRef}
|
||||||
|
style={{
|
||||||
|
maxHeight: height,
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: collapsible ? 'max-height 0.2s ease' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ paddingTop: 8 }}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</CardStyled>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -102,13 +102,15 @@ export async function queryClusterToken(params: { id: number }) {
|
|||||||
// ===================== Worker Pools =====================
|
// ===================== Worker Pools =====================
|
||||||
|
|
||||||
export async function queryWorkerPools(
|
export async function queryWorkerPools(
|
||||||
params?: Global.SearchParams & { cluster_id: string | number }
|
params?: Global.SearchParams & { cluster_id: string | number },
|
||||||
|
options?: any
|
||||||
) {
|
) {
|
||||||
return request<Global.PageResponse<NodePoolListItem>>(
|
return request<Global.PageResponse<NodePoolListItem>>(
|
||||||
`${WORKER_POOLS_API}?`,
|
`${WORKER_POOLS_API}?`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params
|
params,
|
||||||
|
cancelToken: options?.token
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { PageActionType } from '@/config/types';
|
|||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
|
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { queryClusterList } from './apis';
|
import { createCluster, queryCredentialList } from './apis';
|
||||||
import ClusterSteps from './components/cluster-steps';
|
import ClusterSteps from './components/cluster-steps';
|
||||||
import FooterButtons from './components/footer-buttons';
|
import FooterButtons from './components/footer-buttons';
|
||||||
import ProviderCatalog from './components/provider-catalog';
|
import ProviderCatalog from './components/provider-catalog';
|
||||||
@@ -45,6 +45,7 @@ const HeaderContainer = styled.div`
|
|||||||
|
|
||||||
const ClusterCreate = () => {
|
const ClusterCreate = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const startStep = 0;
|
||||||
const stepList = useStepList();
|
const stepList = useStepList();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const action =
|
const action =
|
||||||
@@ -53,7 +54,7 @@ const ClusterCreate = () => {
|
|||||||
const [credentialList, setCredentialList] = useState<
|
const [credentialList, setCredentialList] = useState<
|
||||||
Global.BaseOption<number>[]
|
Global.BaseOption<number>[]
|
||||||
>([]);
|
>([]);
|
||||||
const [currentStep, setCurrentStep] = useState<number>(0);
|
const [currentStep, setCurrentStep] = useState<number>(startStep);
|
||||||
const [registrationInfo, setRegistrationInfo] = useState<{
|
const [registrationInfo, setRegistrationInfo] = useState<{
|
||||||
token: string;
|
token: string;
|
||||||
image: string;
|
image: string;
|
||||||
@@ -96,11 +97,7 @@ const ClusterCreate = () => {
|
|||||||
setCurrentStep(newStep);
|
setCurrentStep(newStep);
|
||||||
console.log('step========', newStep);
|
console.log('step========', newStep);
|
||||||
};
|
};
|
||||||
const customizer = (objValue: any, srcValue: any) => {
|
|
||||||
if (Array.isArray(objValue)) {
|
|
||||||
return srcValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const getFormFieldsValue = () => {
|
const getFormFieldsValue = () => {
|
||||||
setFormValues((prev) => {
|
setFormValues((prev) => {
|
||||||
const newFormValues = _.cloneDeep(prev);
|
const newFormValues = _.cloneDeep(prev);
|
||||||
@@ -166,16 +163,24 @@ const ClusterCreate = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormValues({});
|
||||||
|
for (const [formKey, formRef] of Object.entries(formRefs)) {
|
||||||
|
formRefs[formKey] = React.createRef();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectProvider = (value: ProviderType) => {
|
const handleSelectProvider = (value: ProviderType) => {
|
||||||
setExtraData({
|
setExtraData({
|
||||||
provider: value
|
provider: value
|
||||||
} as ClusterFormData);
|
} as ClusterFormData);
|
||||||
|
resetForm();
|
||||||
onNext();
|
onNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const res = await queryClusterList({ page: 1, perPage: 100 });
|
const res = await queryCredentialList({ page: 1, perPage: 100 });
|
||||||
const list = res.items?.map((item) => ({
|
const list = res.items?.map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
value: item.id
|
value: item.id
|
||||||
@@ -232,7 +237,7 @@ const ClusterCreate = () => {
|
|||||||
...extraData,
|
...extraData,
|
||||||
...(typeof values === 'object' ? values : {})
|
...(typeof values === 'object' ? values : {})
|
||||||
};
|
};
|
||||||
console.log('submit values====:', data);
|
await createCluster({ data });
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -274,7 +279,7 @@ const ClusterCreate = () => {
|
|||||||
</HeaderContainer>
|
</HeaderContainer>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{currentStep === 0 && (
|
{currentStep === startStep && (
|
||||||
<ProviderCatalog
|
<ProviderCatalog
|
||||||
dataList={providerList}
|
dataList={providerList}
|
||||||
onSelect={handleSelectProvider}
|
onSelect={handleSelectProvider}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
import { expandKeysAtom } from '@/atoms/clusters';
|
||||||
import DeleteModal from '@/components/delete-modal';
|
import DeleteModal from '@/components/delete-modal';
|
||||||
import { FilterBar } from '@/components/page-tools';
|
import { FilterBar } from '@/components/page-tools';
|
||||||
|
import SealTable from '@/components/seal-table';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import type { PageActionType } from '@/config/types';
|
import type { PageActionType } from '@/config/types';
|
||||||
|
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
|
||||||
import useTableFetch from '@/hooks/use-table-fetch';
|
import useTableFetch from '@/hooks/use-table-fetch';
|
||||||
import AddWorker from '@/pages/resources/components/add-worker';
|
import AddWorker from '@/pages/resources/components/add-worker';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
|
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { Table, message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
createCluster,
|
createCluster,
|
||||||
@@ -16,13 +20,16 @@ import {
|
|||||||
queryClusterList,
|
queryClusterList,
|
||||||
queryClusterToken,
|
queryClusterToken,
|
||||||
queryCredentialList,
|
queryCredentialList,
|
||||||
|
queryWorkerPools,
|
||||||
updateCluster
|
updateCluster
|
||||||
} from './apis';
|
} from './apis';
|
||||||
import AddCluster from './components/add-cluster';
|
import AddCluster from './components/add-cluster';
|
||||||
import AddPool from './components/add-pool';
|
import AddPool from './components/add-pool';
|
||||||
|
import PoolRows from './components/pool-rows';
|
||||||
import RegisterCluster from './components/register-cluster';
|
import RegisterCluster from './components/register-cluster';
|
||||||
import { ProviderLabelMap, ProviderValueMap } from './config';
|
import { ProviderLabelMap, ProviderType, ProviderValueMap } from './config';
|
||||||
import {
|
import {
|
||||||
|
ClusterListItem,
|
||||||
ClusterFormData as FormData,
|
ClusterFormData as FormData,
|
||||||
ClusterListItem as ListItem,
|
ClusterListItem as ListItem,
|
||||||
NodePoolFormData
|
NodePoolFormData
|
||||||
@@ -47,7 +54,14 @@ const Credentials: React.FC = () => {
|
|||||||
deleteAPI: deleteCluster,
|
deleteAPI: deleteCluster,
|
||||||
contentForDelete: 'menu.clusterManagement.clusters'
|
contentForDelete: 'menu.clusterManagement.clusters'
|
||||||
});
|
});
|
||||||
|
const [expandAtom, setExpandAtom] = useAtom(expandKeysAtom);
|
||||||
|
const {
|
||||||
|
handleExpandChange,
|
||||||
|
handleExpandAll,
|
||||||
|
updateExpandedRowKeys,
|
||||||
|
removeExpandedRowKey,
|
||||||
|
expandedRowKeys
|
||||||
|
} = useExpandedRowKeys(expandAtom);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -106,13 +120,13 @@ const Credentials: React.FC = () => {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
title: string;
|
title: string;
|
||||||
provider: string;
|
provider: ProviderType;
|
||||||
clusterId: number;
|
clusterId: number;
|
||||||
}>({
|
}>({
|
||||||
open: false,
|
open: false,
|
||||||
action: PageAction.CREATE,
|
action: PageAction.CREATE,
|
||||||
title: '',
|
title: '',
|
||||||
provider: ProviderValueMap.DigitalOcean,
|
provider: ProviderValueMap.DigitalOcean as ProviderType,
|
||||||
clusterId: 0
|
clusterId: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -246,6 +260,16 @@ const Credentials: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleOnToggleExpandAll = () => {};
|
||||||
|
|
||||||
|
const handleToggleExpandAll = useMemoizedFn((expanded: boolean) => {
|
||||||
|
const keys = dataSource.dataList?.map((item) => item.id);
|
||||||
|
handleExpandAll(expanded, keys);
|
||||||
|
if (expanded) {
|
||||||
|
handleOnToggleExpandAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleSubmitWorkerPool = async (formdata: NodePoolFormData) => {
|
const handleSubmitWorkerPool = async (formdata: NodePoolFormData) => {
|
||||||
try {
|
try {
|
||||||
await createWorkerPool({
|
await createWorkerPool({
|
||||||
@@ -262,6 +286,20 @@ const Credentials: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getWorkerPoolList = useMemoizedFn(
|
||||||
|
async (row: ClusterListItem, options?: any) => {
|
||||||
|
const params = {
|
||||||
|
cluster_id: row.id,
|
||||||
|
page: 1,
|
||||||
|
perPage: 100
|
||||||
|
};
|
||||||
|
const data = await queryWorkerPools(params, {
|
||||||
|
token: options?.token
|
||||||
|
});
|
||||||
|
return data?.items || [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCredentialList = async () => {
|
const fetchCredentialList = async () => {
|
||||||
const data = await queryCredentialList({ page: 1, perPage: 100 });
|
const data = await queryCredentialList({ page: 1, perPage: 100 });
|
||||||
@@ -274,6 +312,19 @@ const Credentials: React.FC = () => {
|
|||||||
fetchCredentialList();
|
fetchCredentialList();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const renderChildren = (
|
||||||
|
list: any,
|
||||||
|
options: { parent?: any; [key: string]: any }
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<PoolRows
|
||||||
|
dataList={list}
|
||||||
|
provider={options.parent?.provider}
|
||||||
|
clusterId={options.parent?.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const columns = useClusterColumns(handleSelect);
|
const columns = useClusterColumns(handleSelect);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -305,15 +356,23 @@ const Credentials: React.FC = () => {
|
|||||||
handleDeleteByBatch={handleDeleteBatch}
|
handleDeleteByBatch={handleDeleteBatch}
|
||||||
handleClickPrimary={handleClickDropdown}
|
handleClickPrimary={handleClickDropdown}
|
||||||
></FilterBar>
|
></FilterBar>
|
||||||
<Table
|
<SealTable
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
|
loadChildren={getWorkerPoolList}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
expandedRowKeys={expandedRowKeys}
|
||||||
|
onExpand={handleExpandChange}
|
||||||
|
onExpandAll={handleToggleExpandAll}
|
||||||
|
renderChildren={renderChildren}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
|
loadend={dataSource.loadend}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
childParentKey="cluster_id"
|
||||||
|
expandable={true}
|
||||||
pagination={{
|
pagination={{
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
pageSize: queryParams.perPage,
|
pageSize: queryParams.perPage,
|
||||||
@@ -322,7 +381,35 @@ const Credentials: React.FC = () => {
|
|||||||
hideOnSinglePage: queryParams.perPage === 10,
|
hideOnSinglePage: queryParams.perPage === 10,
|
||||||
onChange: handlePageChange
|
onChange: handlePageChange
|
||||||
}}
|
}}
|
||||||
></Table>
|
></SealTable>
|
||||||
|
{/* <SealTable
|
||||||
|
columns={columns}
|
||||||
|
dataSource={dataSource}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
expandedRowKeys={expandedRowKeys}
|
||||||
|
onExpand={handleExpandChange}
|
||||||
|
onExpandAll={handleToggleExpandAll}
|
||||||
|
loading={loading}
|
||||||
|
loadend={loadend}
|
||||||
|
rowKey="id"
|
||||||
|
childParentKey="model_id"
|
||||||
|
expandable={true}
|
||||||
|
onSort={handleOnSort}
|
||||||
|
onCell={handleOnCell}
|
||||||
|
pollingChildren={false}
|
||||||
|
watchChildren={true}
|
||||||
|
loadChildren={getModelInstances}
|
||||||
|
loadChildrenAPI={generateChildrenRequestAPI}
|
||||||
|
renderChildren={renderChildren}
|
||||||
|
pagination={{
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSize: queryParams.perPage,
|
||||||
|
current: queryParams.page,
|
||||||
|
total: total,
|
||||||
|
hideOnSinglePage: queryParams.perPage === 10,
|
||||||
|
onChange: handlePageChange
|
||||||
|
}}
|
||||||
|
></SealTable> */}
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
<AddCluster
|
<AddCluster
|
||||||
provider={openAddModal.provider}
|
provider={openAddModal.provider}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import ModalFooter from '@/components/modal-footer';
|
|||||||
import ScrollerModal from '@/components/scroller-modal';
|
import ScrollerModal from '@/components/scroller-modal';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
|
import { ProviderType } from '../config';
|
||||||
import {
|
import {
|
||||||
NodePoolFormData as FormData,
|
NodePoolFormData as FormData,
|
||||||
NodePoolListItem as ListItem
|
NodePoolListItem as ListItem
|
||||||
@@ -12,7 +13,7 @@ type AddModalProps = {
|
|||||||
title: string;
|
title: string;
|
||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
provider: string; // 'kubernetes' | 'custom' | 'digitalocean';
|
provider: ProviderType; // 'kubernetes' | 'custom' | 'digitalocean';
|
||||||
currentData?: ListItem | null;
|
currentData?: ListItem | null;
|
||||||
onOk: (values: FormData) => void;
|
onOk: (values: FormData) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
|
import CollapsibleContainer, {
|
||||||
|
CollapsibleContainerProps
|
||||||
|
} from '@/components/collapse-container';
|
||||||
import LabelSelector from '@/components/label-selector';
|
import LabelSelector from '@/components/label-selector';
|
||||||
import SealInputNumber from '@/components/seal-form/input-number';
|
import SealInputNumber from '@/components/seal-form/input-number';
|
||||||
import SealInput from '@/components/seal-form/seal-input';
|
import SealInput from '@/components/seal-form/seal-input';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import useAppUtils from '@/hooks/use-app-utils';
|
import useAppUtils from '@/hooks/use-app-utils';
|
||||||
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Button, Form } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, {
|
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||||
forwardRef,
|
import styled from 'styled-components';
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef
|
|
||||||
} from 'react';
|
|
||||||
import { ProviderType } from '../config';
|
import { ProviderType } from '../config';
|
||||||
import { NodePoolFormData as FormData } from '../config/types';
|
import { NodePoolFormData as FormData } from '../config/types';
|
||||||
import VolumesConfig from './volumes-config';
|
import VolumesConfig from './volumes-config';
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
pointer-events: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 0 16px;
|
||||||
|
.ant-form-item:nth-child(5) {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-form-item:nth-child(6) {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
type AddModalProps = {
|
type AddModalProps = {
|
||||||
ref: any;
|
ref: any;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -24,42 +38,74 @@ type AddModalProps = {
|
|||||||
provider: ProviderType; // 'kubernetes' | 'custom' | 'digitalocean';
|
provider: ProviderType; // 'kubernetes' | 'custom' | 'digitalocean';
|
||||||
currentData?: FormData | null;
|
currentData?: FormData | null;
|
||||||
onFinish: (values: FormData) => void;
|
onFinish: (values: FormData) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
showDelete?: boolean;
|
||||||
|
collapseProps?: CollapsibleContainerProps;
|
||||||
};
|
};
|
||||||
const PoolForm: React.FC<AddModalProps> = forwardRef(
|
const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
|
||||||
({ action, name = 'workerPoolForm', onFinish, currentData }, ref) => {
|
const {
|
||||||
const cloudOptionsRef = useRef<any>(null);
|
action,
|
||||||
const [form] = Form.useForm();
|
name = 'workerPoolForm',
|
||||||
const intl = useIntl();
|
onFinish,
|
||||||
const { getRuleMessage } = useAppUtils();
|
onDelete,
|
||||||
|
showDelete,
|
||||||
|
currentData,
|
||||||
|
collapseProps
|
||||||
|
} = props;
|
||||||
|
const { collapsible, onToggle, ...restCollapseProps } = collapseProps || {};
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const intl = useIntl();
|
||||||
|
const { getRuleMessage } = useAppUtils();
|
||||||
|
const labels = Form.useWatch('labels', form);
|
||||||
|
const instance_type = Form.useWatch('instance_type', form);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentData) {
|
if (currentData) {
|
||||||
console.log('currentData===========1=', currentData);
|
console.log('currentData===========1=', currentData);
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...currentData
|
...currentData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [currentData]);
|
}, [currentData]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
resetFields: () => {
|
resetFields: () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
},
|
},
|
||||||
submit: () => {
|
submit: () => {
|
||||||
form.submit();
|
form.submit();
|
||||||
},
|
},
|
||||||
setFieldsValue: (values: any) => {
|
setFieldsValue: (values: any) => {
|
||||||
form.setFieldsValue(values);
|
form.setFieldsValue(values);
|
||||||
},
|
},
|
||||||
getFieldsValue: () => {
|
getFieldsValue: () => {
|
||||||
return form.getFieldsValue();
|
return form.getFieldsValue();
|
||||||
},
|
},
|
||||||
validateFields: async () => {
|
validateFields: async () => {
|
||||||
return await form.validateFields();
|
return await form.validateFields();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<CollapsibleContainer
|
||||||
|
title={instance_type}
|
||||||
|
collapsible={collapsible}
|
||||||
|
onToggle={onToggle}
|
||||||
|
{...restCollapseProps}
|
||||||
|
>
|
||||||
|
{showDelete && (
|
||||||
|
<div className="flex-end" style={{ marginBlock: '8px 16px' }}>
|
||||||
|
<Button
|
||||||
|
onClick={onDelete}
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
danger
|
||||||
|
type="text"
|
||||||
|
variant="filled"
|
||||||
|
color="danger"
|
||||||
|
size="small"
|
||||||
|
></Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Form
|
<Form
|
||||||
name={name}
|
name={name}
|
||||||
form={form}
|
form={form}
|
||||||
@@ -71,103 +117,114 @@ const PoolForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
batch_size: 1
|
batch_size: 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item<FormData>
|
<Container>
|
||||||
name="instance_type"
|
<Form.Item<FormData>
|
||||||
rules={[
|
name="instance_type"
|
||||||
{
|
rules={[
|
||||||
required: true,
|
{
|
||||||
message: getRuleMessage(
|
required: true,
|
||||||
'input',
|
message: getRuleMessage(
|
||||||
'clusters.workerpool.instanceType'
|
'input',
|
||||||
)
|
'clusters.workerpool.instanceType'
|
||||||
}
|
)
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SealInput.Input
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: 'clusters.workerpool.instanceType'
|
|
||||||
})}
|
|
||||||
required
|
|
||||||
></SealInput.Input>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="replicas"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: getRuleMessage('input', 'clusters.workerpool.replicas')
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SealInputNumber
|
|
||||||
label={intl.formatMessage({ id: 'clusters.workerpool.replicas' })}
|
|
||||||
required
|
|
||||||
></SealInputNumber>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="batch_size"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: getRuleMessage('input', 'clusters.workerpool.batchSize')
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SealInputNumber
|
|
||||||
label={intl.formatMessage({ id: 'clusters.workerpool.batchSize' })}
|
|
||||||
required
|
|
||||||
></SealInputNumber>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="os_image"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: getRuleMessage('input', 'clusters.workerpool.osImage')
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SealInput.Input
|
|
||||||
disabled={action === PageAction.EDIT}
|
|
||||||
label={intl.formatMessage({ id: 'clusters.workerpool.osImage' })}
|
|
||||||
required
|
|
||||||
></SealInput.Input>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item<FormData>
|
|
||||||
name="labels"
|
|
||||||
rules={[
|
|
||||||
({ getFieldValue }) => ({
|
|
||||||
validator(rule, value) {
|
|
||||||
if (_.keys(value).length > 0) {
|
|
||||||
if (_.some(_.keys(value), (k: string) => !value[k])) {
|
|
||||||
return Promise.reject(
|
|
||||||
intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: 'common.validate.value'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'labels'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
})
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<SealInput.Input
|
||||||
<LabelSelector
|
label={intl.formatMessage({
|
||||||
label={intl.formatMessage({ id: 'resources.table.labels' })}
|
id: 'clusters.workerpool.instanceType'
|
||||||
labels={form.getFieldValue('labels') || {}}
|
})}
|
||||||
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
|
required
|
||||||
></LabelSelector>
|
></SealInput.Input>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<VolumesConfig ref={cloudOptionsRef}></VolumesConfig>
|
<Form.Item<FormData>
|
||||||
|
name="replicas"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: getRuleMessage('input', 'clusters.workerpool.replicas')
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInputNumber
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.workerpool.replicas'
|
||||||
|
})}
|
||||||
|
required
|
||||||
|
></SealInputNumber>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="batch_size"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: getRuleMessage(
|
||||||
|
'input',
|
||||||
|
'clusters.workerpool.batchSize'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInputNumber
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.workerpool.batchSize'
|
||||||
|
})}
|
||||||
|
required
|
||||||
|
></SealInputNumber>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="os_image"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: getRuleMessage('input', 'clusters.workerpool.osImage')
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SealInput.Input
|
||||||
|
disabled={action === PageAction.EDIT}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'clusters.workerpool.osImage'
|
||||||
|
})}
|
||||||
|
required
|
||||||
|
></SealInput.Input>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name="labels"
|
||||||
|
rules={[
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(rule, value) {
|
||||||
|
if (_.keys(value).length > 0) {
|
||||||
|
if (_.some(_.keys(value), (k: string) => !value[k])) {
|
||||||
|
return Promise.reject(
|
||||||
|
intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'common.validate.value'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'labels'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<LabelSelector
|
||||||
|
label={intl.formatMessage({ id: 'resources.table.labels' })}
|
||||||
|
labels={labels || {}}
|
||||||
|
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
|
||||||
|
></LabelSelector>
|
||||||
|
</Form.Item>
|
||||||
|
<VolumesConfig></VolumesConfig>
|
||||||
|
</Container>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
</CollapsibleContainer>
|
||||||
}
|
);
|
||||||
);
|
});
|
||||||
|
|
||||||
export default PoolForm;
|
export default PoolForm;
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import DeleteModal from '@/components/delete-modal';
|
||||||
|
import RowChildren from '@/components/seal-table/components/row-children';
|
||||||
|
import { PageAction } from '@/config';
|
||||||
|
import { PageActionType } from '@/config/types';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { Col, message, Row } from 'antd';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { deleteWorkerPool, updateWorkerPool } from '../apis';
|
||||||
|
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;
|
||||||
|
clusterId: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PoolRows: React.FC<PoolRowsProps> = ({
|
||||||
|
dataList,
|
||||||
|
provider,
|
||||||
|
clusterId
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const modalRef = useRef<any>(null);
|
||||||
|
const [addPoolStatus, setAddPoolStatus] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
action: PageActionType;
|
||||||
|
title: string;
|
||||||
|
provider: ProviderType;
|
||||||
|
currentData: NodePoolListItem | null;
|
||||||
|
clusterId: number | string;
|
||||||
|
}>({
|
||||||
|
open: false,
|
||||||
|
action: PageAction.EDIT,
|
||||||
|
title: '',
|
||||||
|
provider: provider,
|
||||||
|
currentData: null as NodePoolListItem | null,
|
||||||
|
clusterId: clusterId
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmitWorkerPool = async (formdata: NodePoolFormData) => {
|
||||||
|
try {
|
||||||
|
await updateWorkerPool({
|
||||||
|
data: formdata,
|
||||||
|
id: addPoolStatus.currentData!.id
|
||||||
|
});
|
||||||
|
setAddPoolStatus({
|
||||||
|
...addPoolStatus,
|
||||||
|
open: false
|
||||||
|
});
|
||||||
|
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||||
|
// handleSearch();
|
||||||
|
} catch (error) {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (action: string, record: NodePoolListItem) => {
|
||||||
|
if (action === 'edit') {
|
||||||
|
setAddPoolStatus({
|
||||||
|
open: true,
|
||||||
|
action: PageAction.EDIT,
|
||||||
|
title: intl.formatMessage(
|
||||||
|
{ id: 'common.button.edit.item' },
|
||||||
|
{ name: record.instance_type }
|
||||||
|
),
|
||||||
|
provider: provider,
|
||||||
|
currentData: record,
|
||||||
|
clusterId: record.cluster_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (row: { name: string; id: number }, options?: any) => {
|
||||||
|
modalRef.current?.show({
|
||||||
|
content: 'worker pool',
|
||||||
|
operation: 'common.delete.single.confirm',
|
||||||
|
name: row.name,
|
||||||
|
...options,
|
||||||
|
async onOk() {
|
||||||
|
console.log('OK');
|
||||||
|
await deleteWorkerPool(row.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = useMemoizedFn((key: string, record: NodePoolListItem) => {
|
||||||
|
if (key === 'delete') {
|
||||||
|
handleDelete({ ...record, name: record.instance_type });
|
||||||
|
}
|
||||||
|
if (key === 'edit') {
|
||||||
|
handleEdit(key, record);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = usePoolsColumns(onSelect);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{dataList?.map((data: NodePoolListItem) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={data.id}
|
||||||
|
style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}
|
||||||
|
>
|
||||||
|
<RowChildren>
|
||||||
|
<Row style={{ width: '100%' }} align="middle">
|
||||||
|
{columns.map((col: Record<string, any>) => {
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
key={col.dataIndex as string}
|
||||||
|
span={col.span}
|
||||||
|
style={col.style}
|
||||||
|
>
|
||||||
|
{col.render
|
||||||
|
? col.render(data[col.dataIndex as string], data)
|
||||||
|
: data[col.dataIndex as string]}
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
</RowChildren>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<AddPool
|
||||||
|
provider={addPoolStatus.provider}
|
||||||
|
open={addPoolStatus.open}
|
||||||
|
action={addPoolStatus.action}
|
||||||
|
title={addPoolStatus.title}
|
||||||
|
currentData={addPoolStatus.currentData}
|
||||||
|
onCancel={() => {
|
||||||
|
setAddPoolStatus({
|
||||||
|
open: false,
|
||||||
|
action: PageAction.CREATE,
|
||||||
|
title: '',
|
||||||
|
provider: provider,
|
||||||
|
currentData: null,
|
||||||
|
clusterId: 0
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onOk={handleSubmitWorkerPool}
|
||||||
|
></AddPool>
|
||||||
|
<DeleteModal ref={modalRef}></DeleteModal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PoolRows;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Card from '@/components/templates/card';
|
import Card from '@/components/templates/card';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { ProviderType } from '../config';
|
import { ProviderType } from '../config';
|
||||||
|
|
||||||
@@ -9,6 +9,22 @@ const Wrapper = styled.div`
|
|||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
gap: 22px;
|
gap: 22px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled.span`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-block: 16px 24px;
|
||||||
|
`;
|
||||||
|
|
||||||
interface ProviderCatalogProps {
|
interface ProviderCatalogProps {
|
||||||
onSelect?: (provider: ProviderType) => void;
|
onSelect?: (provider: ProviderType) => void;
|
||||||
currentProvider?: ProviderType;
|
currentProvider?: ProviderType;
|
||||||
@@ -20,6 +36,7 @@ interface ProviderCatalogProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
group?: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,27 +47,46 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
|
|||||||
currentProvider
|
currentProvider
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const groupList = useMemo(() => {
|
||||||
|
if (!dataList) return {};
|
||||||
|
return dataList?.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
(acc[item.group || 'default'] ??= []).push(item);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, typeof dataList>
|
||||||
|
);
|
||||||
|
}, [dataList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Container>
|
||||||
{dataList?.map((action) => (
|
{Object.entries(groupList).map(([groupName, items]) => (
|
||||||
<Card
|
<div key={groupName}>
|
||||||
height="auto"
|
{groupName !== 'default' && <Title>{groupName}</Title>}
|
||||||
key={action.key}
|
<Wrapper>
|
||||||
onClick={() => onSelect?.(action.key as ProviderType)}
|
{items?.map((action) => (
|
||||||
active={currentProvider === action.key}
|
<Card
|
||||||
disabled={action.disabled}
|
height="auto"
|
||||||
clickable={clickable}
|
key={action.key}
|
||||||
header={
|
onClick={() => onSelect?.(action.key as ProviderType)}
|
||||||
action.locale
|
active={currentProvider === action.key}
|
||||||
? intl.formatMessage({ id: action.label })
|
disabled={action.disabled}
|
||||||
: action.label
|
clickable={clickable}
|
||||||
}
|
header={
|
||||||
icon={action.icon}
|
action.locale
|
||||||
>
|
? intl.formatMessage({ id: action.label })
|
||||||
{action.description || 'This is a description'}
|
: action.label
|
||||||
</Card>
|
}
|
||||||
|
icon={action.icon}
|
||||||
|
>
|
||||||
|
{action.description || 'This is a description'}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Wrapper>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</Wrapper>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ const WorkerPools = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = usePoolsColumns(sortOrder, onSelect);
|
const columns = usePoolsColumns(onSelect, sortOrder);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ export const providerList = [
|
|||||||
key: ProviderValueMap.Custom,
|
key: ProviderValueMap.Custom,
|
||||||
icon: React.cloneElement(icons.Docker, {
|
icon: React.cloneElement(icons.Docker, {
|
||||||
style: { color: 'var(--ant-color-primary)' }
|
style: { color: 'var(--ant-color-primary)' }
|
||||||
})
|
}),
|
||||||
|
group: 'default'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Kubernetes',
|
label: 'Kubernetes',
|
||||||
@@ -64,7 +65,8 @@ export const providerList = [
|
|||||||
key: ProviderValueMap.Kubernetes,
|
key: ProviderValueMap.Kubernetes,
|
||||||
icon: React.cloneElement(icons.KubernetesOutlined, {
|
icon: React.cloneElement(icons.KubernetesOutlined, {
|
||||||
style: { color: 'var(--ant-color-primary)' }
|
style: { color: 'var(--ant-color-primary)' }
|
||||||
})
|
}),
|
||||||
|
group: 'default'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'DigitalOcean',
|
label: 'DigitalOcean',
|
||||||
@@ -75,7 +77,8 @@ export const providerList = [
|
|||||||
style: {
|
style: {
|
||||||
color: 'var(--ant-color-primary)'
|
color: 'var(--ant-color-primary)'
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
group: 'Cloud Provider'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Huawei Cloud',
|
label: 'Huawei Cloud',
|
||||||
@@ -84,7 +87,8 @@ export const providerList = [
|
|||||||
value: ProviderValueMap.HuaweiCloud,
|
value: ProviderValueMap.HuaweiCloud,
|
||||||
key: ProviderValueMap.HuaweiCloud,
|
key: ProviderValueMap.HuaweiCloud,
|
||||||
icon: icons.HuaweiCloud,
|
icon: icons.HuaweiCloud,
|
||||||
description: 'Currently Not Supported'
|
description: 'Comming soon',
|
||||||
|
group: 'Cloud Provider'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Ali Cloud',
|
label: 'Ali Cloud',
|
||||||
@@ -93,7 +97,8 @@ export const providerList = [
|
|||||||
value: ProviderValueMap.AliCloud,
|
value: ProviderValueMap.AliCloud,
|
||||||
key: ProviderValueMap.AliCloud,
|
key: ProviderValueMap.AliCloud,
|
||||||
icon: icons.AliCloud,
|
icon: icons.AliCloud,
|
||||||
description: 'Currently Not Supported'
|
description: 'Comming soon',
|
||||||
|
group: 'Cloud Provider'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Tencent Cloud',
|
label: 'Tencent Cloud',
|
||||||
@@ -102,7 +107,8 @@ export const providerList = [
|
|||||||
value: ProviderValueMap.TencentCloud,
|
value: ProviderValueMap.TencentCloud,
|
||||||
key: ProviderValueMap.TencentCloud,
|
key: ProviderValueMap.TencentCloud,
|
||||||
icon: icons.TencentCloud,
|
icon: icons.TencentCloud,
|
||||||
description: 'Currently Not Supported'
|
description: 'Comming soon',
|
||||||
|
group: 'Cloud Provider'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -142,13 +148,13 @@ export const clusterActionList = [
|
|||||||
locale: true,
|
locale: true,
|
||||||
icon: icons.KubernetesOutlined
|
icon: icons.KubernetesOutlined
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// key: 'addPool',
|
key: 'addPool',
|
||||||
// label: 'clusters.button.addNodePool',
|
label: 'clusters.button.addNodePool',
|
||||||
// provider: ProviderValueMap.DigitalOcean,
|
provider: ProviderValueMap.DigitalOcean,
|
||||||
// locale: true,
|
locale: true,
|
||||||
// icon: icons.Catalog
|
icon: icons.Catalog
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: 'common.button.delete',
|
label: 'common.button.delete',
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ export interface NodePoolListItem {
|
|||||||
batch_size: number;
|
batch_size: number;
|
||||||
labels: Record<string, string>;
|
labels: Record<string, string>;
|
||||||
cloud_options: Record<string, any>;
|
cloud_options: Record<string, any>;
|
||||||
|
os_image: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
cluster_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodePoolFormData {
|
export interface NodePoolFormData {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const useClusterColumns = (
|
|||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
|
span: 3,
|
||||||
render: (text: string, record: ClusterListItem) => (
|
render: (text: string, record: ClusterListItem) => (
|
||||||
<AutoTooltip ghost>
|
<AutoTooltip ghost>
|
||||||
<Link
|
<Link
|
||||||
@@ -46,11 +47,13 @@ const useClusterColumns = (
|
|||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'clusters.table.provider' }),
|
title: intl.formatMessage({ id: 'clusters.table.provider' }),
|
||||||
dataIndex: 'provider',
|
dataIndex: 'provider',
|
||||||
|
span: 3,
|
||||||
render: (value: string) => <span>{ProviderLabelMap[value]}</span>
|
render: (value: string) => <span>{ProviderLabelMap[value]}</span>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.status' }),
|
title: intl.formatMessage({ id: 'common.table.status' }),
|
||||||
dataIndex: 'state',
|
dataIndex: 'state',
|
||||||
|
span: 3,
|
||||||
render: (value: number) => (
|
render: (value: number) => (
|
||||||
<StatusTag
|
<StatusTag
|
||||||
statusValue={{
|
statusValue={{
|
||||||
@@ -63,6 +66,7 @@ const useClusterColumns = (
|
|||||||
{
|
{
|
||||||
title: 'Workers',
|
title: 'Workers',
|
||||||
dataIndex: 'workers',
|
dataIndex: 'workers',
|
||||||
|
span: 3,
|
||||||
render: (value: number, record: ClusterListItem) => (
|
render: (value: number, record: ClusterListItem) => (
|
||||||
<span>
|
<span>
|
||||||
{record.ready_workers} / {record.workers}
|
{record.ready_workers} / {record.workers}
|
||||||
@@ -72,17 +76,19 @@ const useClusterColumns = (
|
|||||||
{
|
{
|
||||||
title: 'GPUs',
|
title: 'GPUs',
|
||||||
dataIndex: 'gpus',
|
dataIndex: 'gpus',
|
||||||
|
span: 3,
|
||||||
render: (value: number) => <span>{value}</span>
|
render: (value: number) => <span>{value}</span>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'clusters.table.deployments' }),
|
title: intl.formatMessage({ id: 'clusters.table.deployments' }),
|
||||||
dataIndex: 'models',
|
dataIndex: 'models',
|
||||||
|
span: 2,
|
||||||
render: (value: number) => <span>{value}</span>
|
render: (value: number) => <span>{value}</span>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
width: 180,
|
span: 4,
|
||||||
render: (value: string) => (
|
render: (value: string) => (
|
||||||
<span>{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}</span>
|
<span>{dayjs(value).format('YYYY-MM-DD HH:mm:ss')}</span>
|
||||||
)
|
)
|
||||||
@@ -90,6 +96,7 @@ const useClusterColumns = (
|
|||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.operation' }),
|
title: intl.formatMessage({ id: 'common.table.operation' }),
|
||||||
dataIndex: 'operations',
|
dataIndex: 'operations',
|
||||||
|
span: 3,
|
||||||
render: (value: string, record: ClusterListItem) => (
|
render: (value: string, record: ClusterListItem) => (
|
||||||
<DropdownButtons
|
<DropdownButtons
|
||||||
items={setActionsItems(record)}
|
items={setActionsItems(record)}
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ const actionItems = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
const usePoolsColumns = (
|
const usePoolsColumns = (
|
||||||
sortOrder: SortOrder,
|
handleSelect: (val: string, record: ListItem) => void,
|
||||||
handleSelect: (val: string, record: ListItem) => void
|
sortOrder?: SortOrder
|
||||||
): ColumnsType<ListItem> => {
|
): ColumnsType<ListItem> => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
@@ -40,6 +40,10 @@ const usePoolsColumns = (
|
|||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
|
span: 4,
|
||||||
|
style: {
|
||||||
|
paddingInline: 'var(--ant-table-cell-padding-inline)'
|
||||||
|
},
|
||||||
render: (text: string) => (
|
render: (text: string) => (
|
||||||
<AutoTooltip title={text} ghost minWidth={20}>
|
<AutoTooltip title={text} ghost minWidth={20}>
|
||||||
{text}
|
{text}
|
||||||
@@ -49,17 +53,20 @@ const usePoolsColumns = (
|
|||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'clusters.workerpool.replicas' }),
|
title: intl.formatMessage({ id: 'clusters.workerpool.replicas' }),
|
||||||
dataIndex: 'replicas',
|
dataIndex: 'replicas',
|
||||||
|
span: 3,
|
||||||
key: 'replicas'
|
key: 'replicas'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'clusters.workerpool.batchSize' }),
|
title: intl.formatMessage({ id: 'clusters.workerpool.batchSize' }),
|
||||||
dataIndex: 'batch_size',
|
dataIndex: 'batch_size',
|
||||||
key: 'batch_size'
|
key: 'batch_size',
|
||||||
|
span: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'clusters.workerpool.osImage' }),
|
title: intl.formatMessage({ id: 'clusters.workerpool.osImage' }),
|
||||||
dataIndex: 'os_image',
|
dataIndex: 'os_image',
|
||||||
key: 'os_image',
|
key: 'os_image',
|
||||||
|
span: 3,
|
||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
@@ -74,6 +81,7 @@ const usePoolsColumns = (
|
|||||||
dataIndex: 'labels',
|
dataIndex: 'labels',
|
||||||
key: 'labels',
|
key: 'labels',
|
||||||
width: 200,
|
width: 200,
|
||||||
|
span: 4,
|
||||||
render: (text: string, record: ListItem) => (
|
render: (text: string, record: ListItem) => (
|
||||||
<LabelsCell labels={record.labels}></LabelsCell>
|
<LabelsCell labels={record.labels}></LabelsCell>
|
||||||
)
|
)
|
||||||
@@ -82,6 +90,7 @@ const usePoolsColumns = (
|
|||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: 'create_at',
|
dataIndex: 'create_at',
|
||||||
key: 'created_at',
|
key: 'created_at',
|
||||||
|
span: 4,
|
||||||
showSorterTooltip: false,
|
showSorterTooltip: false,
|
||||||
defaultSortOrder: 'descend',
|
defaultSortOrder: 'descend',
|
||||||
sortOrder: sortOrder,
|
sortOrder: sortOrder,
|
||||||
@@ -89,11 +98,18 @@ const usePoolsColumns = (
|
|||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
|
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' }),
|
title: intl.formatMessage({ id: 'common.table.operation' }),
|
||||||
key: 'operations',
|
key: 'operations',
|
||||||
|
span: 3,
|
||||||
|
style: {
|
||||||
|
paddingLeft: 36
|
||||||
|
},
|
||||||
render: (text: string, record: ListItem) => (
|
render: (text: string, record: ListItem) => (
|
||||||
<DropdownButtons
|
<DropdownButtons
|
||||||
items={actionItems}
|
items={actionItems}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import PageTools from '@/components/page-tools';
|
import PageTools from '@/components/page-tools';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Button, Divider, FormInstance } from 'antd';
|
import { Button, FormInstance } from 'antd';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -22,6 +22,7 @@ const PoolContainer = styled.div`
|
|||||||
|
|
||||||
const PoolFormWrapper = styled.div`
|
const PoolFormWrapper = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
margin-bottom: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Title = styled.span`
|
const Title = styled.span`
|
||||||
@@ -42,14 +43,15 @@ interface WorkerPoolsFormProps {
|
|||||||
|
|
||||||
const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||||
const { provider, action, currentData } = props;
|
const { provider, action, currentData } = props;
|
||||||
const countRef = useRef(1);
|
const countRef = useRef(0);
|
||||||
const formRefs = useRef<Record<number, FormInstance<any> | null>>({});
|
const formRefs = useRef<Record<number, FormInstance<any> | null>>({});
|
||||||
|
const [activeKey, setActiveKey] = useState<Set<number>>(new Set([0]));
|
||||||
const [workerPoolList, setWorkerPoolList] = useState<
|
const [workerPoolList, setWorkerPoolList] = useState<
|
||||||
Map<number, NodePoolFormData>
|
Map<number, NodePoolFormData>
|
||||||
>(
|
>(
|
||||||
new Map([
|
new Map([
|
||||||
[
|
[
|
||||||
1,
|
0,
|
||||||
{
|
{
|
||||||
instance_type: 'Pool-1'
|
instance_type: 'Pool-1'
|
||||||
}
|
}
|
||||||
@@ -68,9 +70,14 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
|||||||
const newId = updateCount();
|
const newId = updateCount();
|
||||||
setWorkerPoolList((prev) =>
|
setWorkerPoolList((prev) =>
|
||||||
new Map(prev).set(newId, {
|
new Map(prev).set(newId, {
|
||||||
instance_type: `Pool-${newId}`
|
instance_type: `Pool-${newId + 1}`
|
||||||
} as NodePoolFormData)
|
} as NodePoolFormData)
|
||||||
);
|
);
|
||||||
|
setActiveKey((prev) => new Set([newId]));
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemovePool = (id: number) => {
|
const handleRemovePool = (id: number) => {
|
||||||
@@ -137,6 +144,19 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnToggle = (open: boolean, key: number) => {
|
||||||
|
console.log('Active keys changed:', key);
|
||||||
|
if (open) {
|
||||||
|
setActiveKey((prev) => new Set([key]));
|
||||||
|
} else {
|
||||||
|
setActiveKey((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(key);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
submit: () => {},
|
submit: () => {},
|
||||||
validateFields: validateFields,
|
validateFields: validateFields,
|
||||||
@@ -157,7 +177,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
|||||||
marginTop={0}
|
marginTop={0}
|
||||||
left={
|
left={
|
||||||
<span className="flex-center gap-16">
|
<span className="flex-center gap-16">
|
||||||
<Title>Worker Pools Configuration</Title>
|
<Title>Worker Pools</Title>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
@@ -171,6 +191,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
></PageTools>
|
></PageTools>
|
||||||
|
|
||||||
{Array.from(workerPoolList.keys()).map((key, index) => (
|
{Array.from(workerPoolList.keys()).map((key, index) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<PoolContainer>
|
<PoolContainer>
|
||||||
@@ -183,34 +204,22 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
|||||||
formRefs.current[key] = 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}
|
onFinish={handleOnFinish}
|
||||||
provider={provider}
|
provider={provider}
|
||||||
currentData={workerPoolList.get(key)}
|
currentData={workerPoolList.get(key)}
|
||||||
|
onDelete={() => handleRemovePool(key)}
|
||||||
></WorkerPoolForm>
|
></WorkerPoolForm>
|
||||||
</PoolFormWrapper>
|
</PoolFormWrapper>
|
||||||
</PoolContainer>
|
</PoolContainer>
|
||||||
{index !== Array.from(workerPoolList.keys()).length - 1 && (
|
|
||||||
<Divider orientation="right">
|
|
||||||
<Button
|
|
||||||
variant="filled"
|
|
||||||
color="default"
|
|
||||||
onClick={() => handleRemovePool(key)}
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
></Button>
|
|
||||||
</Divider>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="flex-end">
|
|
||||||
<Button
|
|
||||||
onClick={handleAddPool}
|
|
||||||
variant="filled"
|
|
||||||
color="default"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
>
|
|
||||||
Add Pool
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user