Files
gpustack-ui/src/pages/cluster-management/components/cluster-item.tsx
T
2025-09-16 11:26:17 +08:00

287 lines
6.7 KiB
TypeScript

import DropdownButtons from '@/components/drop-down-buttons';
import GaugeChart from '@/components/echarts/gauge';
import IconFont from '@/components/icon-font';
import StatusTag from '@/components/status-tag';
import Card from '@/components/templates/card';
import {
DeleteOutlined,
EditOutlined,
KubernetesOutlined
} from '@ant-design/icons';
import { Card as ACard, Button, Col, Row, Tag } from 'antd';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import {
ClusterStatus,
ClusterStatusLabelMap,
ProviderLabelMap,
ProviderValueMap
} from '../config';
import { ClusterListItem as ListItem, NodePoolListItem } from '../config/types';
import WorkerPools from './worker-pools';
const CollapseTitle = styled.div`
display: flex;
align-items: center;
gap: 8px;
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
height: 32px;
cursor: pointer;
`;
const Content = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
.chart-wrapper {
flex: 1;
}
`;
const CardWrapper = styled(ACard)`
text-align: center;
box-shadow: none;
flex: 1;
.ant-card {
box-shadow: none;
}
.ant-card-body {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 8px;
}
.label {
font-weight: 500;
font-size: var(--font-size-middle);
}
.value {
font-weight: 500;
color: var(--ant-color-text);
}
`;
const CardBox = styled.div`
display: flex;
gap: 16px;
flex: 0.5;
`;
const actionItems = [
{
key: 'edit',
label: 'common.button.edit',
icon: <EditOutlined />
},
{
key: 'add_worker',
label: 'Add Worker',
provider: ProviderValueMap.Custom,
locale: false,
icon: <IconFont type="icon-docker" />
},
{
key: 'register_cluster',
label: 'Register Cluster',
provider: ProviderValueMap.Kubernetes,
locale: false,
icon: <KubernetesOutlined />
},
{
key: 'addPool',
label: 'Add Node Pool',
provider: ProviderValueMap.DigitalOcean,
locale: false,
icon: <IconFont type="icon-catalog1" />
},
{
key: 'delete',
label: 'common.button.delete',
icon: <DeleteOutlined />,
props: {
danger: true
}
}
];
const Inner = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
cursor: default;
.title {
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
.text {
display: flex;
align-items: center;
font-size: var(--font-size-middle);
font-weight: 500;
color: var(--ant-color-text);
}
}
.content {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
}
`;
interface CardProps {
data: ListItem;
onSelect?: (key: string, row: ListItem) => void;
}
const gaugeConfig = {
radius: '100%',
progress: {
show: true,
roundCap: false,
width: 8
},
axisLine: {
roundCap: false,
lineStyle: {
width: 8,
color: [
[0.5, 'rgba(84, 204, 152, 80%)'],
[0.8, 'rgba(250, 173, 20, 80%)'],
[1, 'rgba(255, 77, 79, 80%)']
]
}
}
};
const CardItem: React.FC<CardProps> = (props) => {
const { data, onSelect } = props;
const [show, setShow] = React.useState(false);
const handleOnSelect = (key: string) => {
onSelect?.(key, data);
};
const handleOnAction = (action: string, record: NodePoolListItem) => {};
const actions = useMemo(() => {
return actionItems.filter((item) => {
if (item.provider) {
return item.provider === data.provider;
}
return true;
});
}, [data.provider]);
return (
<Card height={'auto'} clickable={false} ghost>
<Inner>
<div className="title">
<span className="flex-center gap-8">
<span className="text">{data.name}</span>
<Tag style={{ borderRadius: 4 }}>
{ProviderLabelMap[data.provider]}
</Tag>
<StatusTag
statusValue={{
status: ClusterStatus[data.status],
text: ClusterStatusLabelMap[data.status]
}}
/>
</span>
<span>
<DropdownButtons
items={actions}
onSelect={handleOnSelect}
></DropdownButtons>
</span>
</div>
<Content>
<CardBox>
<CardWrapper bordered={false}>
<div className="label">Workers</div>
<div className="value">1</div>
</CardWrapper>
<CardWrapper bordered={false}>
<div className="label">GPUs</div>
<div className="value">12</div>
</CardWrapper>
<CardWrapper bordered={false}>
<div className="label">Deployments</div>
<div className="value">2</div>
</CardWrapper>
</CardBox>
<div className="chart-wrapper">
<Row gutter={16} style={{ width: '100%' }}>
<Col span={6}>
<GaugeChart
title="GPU Utilization"
value={85}
height={160}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="CPU Utilization"
value={50}
height={160}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="RAM Utilization"
value={70}
height={160}
gaugeConfig={gaugeConfig}
/>
</Col>
<Col span={6}>
<GaugeChart
title="VRAM Utilization"
value={60}
height={160}
gaugeConfig={gaugeConfig}
/>
</Col>
</Row>
</div>
</Content>
{data.provider === ProviderValueMap.DigitalOcean && (
<>
<CollapseTitle>
<Button
type="text"
size="small"
icon={<IconFont type="icon-down" rotate={show ? 0 : -90} />}
onClick={() => setShow(!show)}
>
Worker Pools
</Button>
</CollapseTitle>
<WorkerPools
provider={data.provider}
workerPools={data.worker_pools}
height={show ? 'auto' : 0}
onAction={handleOnAction}
/>
</>
)}
</Inner>
</Card>
);
};
export default CardItem;