diff --git a/.changeset/serious-scissors-thank.md b/.changeset/serious-scissors-thank.md new file mode 100644 index 0000000000..90f63a0e78 --- /dev/null +++ b/.changeset/serious-scissors-thank.md @@ -0,0 +1,5 @@ +--- +"@scow/portal-web": patch +--- + +修改了门户系统下仪表盘的样式和交互逻辑 diff --git a/apps/portal-web/src/i18n/en.ts b/apps/portal-web/src/i18n/en.ts index 03b74ef39b..92c3385562 100644 --- a/apps/portal-web/src/i18n/en.ts +++ b/apps/portal-web/src/i18n/en.ts @@ -470,6 +470,22 @@ export default { card:"Card", job:"Job", pending:"Pending", + platformOverview:"Platform Overview", + }, + nodeRange:{ + jobs:"Jobs", + running:"Running", + pending:"Pending", + }, + infoPane:{ + nodeUtilization:"Node Utilization", + }, + doubleInfoPane:{ + CPUCoreUsage:"CPU Core Usage", + GPUCoreUsage:"GPU Core Usage", + }, + titleContainer:{ + available:"Available", }, }, }, diff --git a/apps/portal-web/src/i18n/zh_cn.ts b/apps/portal-web/src/i18n/zh_cn.ts index 4e3dd94251..047d8e2d65 100644 --- a/apps/portal-web/src/i18n/zh_cn.ts +++ b/apps/portal-web/src/i18n/zh_cn.ts @@ -449,6 +449,12 @@ export default { card:"卡", job:"作业", pending:"排队中", + platformOverview:"平台概览", + }, + nodeRange:{ + jobs:"作业", + running:"运行中", + pending:"排队中", }, addEntryModal:{ addQuickEntry:"添加快捷方式", @@ -471,6 +477,16 @@ export default { saveFailed:"保存失败", saveSuccessfully:"保存成功", }, + infoPane:{ + nodeUtilization:"节点使用率", + }, + doubleInfoPane:{ + CPUCoreUsage:"CPU核心使用率", + GPUCoreUsage:"GPU卡使用率", + }, + titleContainer:{ + available:"可用", + }, }, }, component:{ diff --git a/apps/portal-web/src/models/cluster.ts b/apps/portal-web/src/models/cluster.ts new file mode 100644 index 0000000000..66cada372c --- /dev/null +++ b/apps/portal-web/src/models/cluster.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export interface ClusterOverview { + clusterName: string, + nodeCount: number, + runningNodeCount: number, + idleNodeCount: number, + notAvailableNodeCount: number, + cpuCoreCount: number, + runningCpuCount: number, + idleCpuCount: number, + notAvailableCpuCount: number, + gpuCoreCount: number, + runningGpuCount: number, + idleGpuCount: number, + notAvailableGpuCount: number, + jobCount: number, + runningJobCount: number, + pendingJobCount: number, + usageRatePercentage: number, + partitionStatus: number, +} + +export interface PlatformOverview { + nodeCount: number, + runningNodeCount: number, + idleNodeCount: number, + notAvailableNodeCount: number, + cpuCoreCount: number, + runningCpuCount: number, + idleCpuCount: number, + notAvailableCpuCount: number, + gpuCoreCount: number, + runningGpuCount: number, + idleGpuCount: number, + notAvailableGpuCount: number, + jobCount: number, + runningJobCount: number, + pendingJobCount: number, + usageRatePercentage: number, + partitionStatus: number, +} diff --git a/apps/portal-web/src/pageComponents/dashboard/DoubleInfoPane.tsx b/apps/portal-web/src/pageComponents/dashboard/DoubleInfoPane.tsx index eb99f1aad1..d24a5bafbe 100644 --- a/apps/portal-web/src/pageComponents/dashboard/DoubleInfoPane.tsx +++ b/apps/portal-web/src/pageComponents/dashboard/DoubleInfoPane.tsx @@ -10,13 +10,14 @@ * See the Mulan PSL v2 for more details. */ -import { Card, Tag } from "antd"; +import { Card } from "antd"; import React, { useMemo } from "react"; import { PieChartCom } from "src/pageComponents/dashboard/PieChartCom"; import { styled } from "styled-components"; ; -import { gray } from "@ant-design/colors"; +import { prefix, useI18nTranslateToString } from "src/i18n"; -import { Line, PaneData, PieChartContainer, Tag as TagProps, Title, TitleContainer } from "./InfoPane"; +import { PaneData, PieChartContainer, Tag as TagProps, Title } from "./InfoPane"; +import { TitleContainer } from "./TitleContainer"; interface InfoProps { title?: Title; @@ -28,30 +29,27 @@ interface Props { cpuInfo: InfoProps; gpuInfo: InfoProps; loading: boolean; + strokeColor: [string, string] } const Container = styled.div` -margin: 20px 0; -`; - -const UpperContainer = styled.div` - display: flex; - flex-direction: column; +margin: 0px 0; `; const LowerContainer = styled.div` display: flex; - /* CPU和GPU两个信息面板的间隔距离 */ - & > :first-child { - margin-right: 40px; - } + justify-content:space-between `; const ResourceContainer = styled.div` flex: 1; `; -export const DoubleInfoPane: React.FC = ({ cpuInfo, gpuInfo, loading }) => { +const p = prefix("pageComp.dashboard.doubleInfoPane."); + +export const DoubleInfoPane: React.FC = ({ cpuInfo, gpuInfo, loading, strokeColor }) => { + + const t = useI18nTranslateToString(); const cpuNotEmptyData = useMemo(() => { return cpuInfo.paneData.some((x) => x.num > 0); @@ -61,73 +59,51 @@ export const DoubleInfoPane: React.FC = ({ cpuInfo, gpuInfo, loading }) = return gpuInfo.paneData.some((x) => x.num > 0); }, [gpuInfo.paneData]); + return ( - - - {cpuInfo.title ? ( - - {cpuInfo.title.title} - {cpuInfo.title.subTitle ? `[${cpuInfo.title.subTitle}]` : " "} - - ) - : undefined} - + + a + b.num, 0)} + available={cpuInfo.paneData[1].num} + display={cpuNotEmptyData} + > + a + b.num, 0)} + available={gpuInfo.paneData[1].num} + display={gpuNotEmptyData} + > + + )} + > -
- - {cpuInfo.tag.itemName} -  {cpuInfo.tag.num} - {cpuInfo.tag.unit} - -
-
- { - cpuInfo.paneData.map((item, idx) => - ) - } -
- {/* 数据全为空时,饼图置灰 */} - {cpuNotEmptyData ? ( - ({ value:item.num, color:item.color }))} - > - ) : - } + ({ value:item.num, color:item.color }))} + strokeColor={strokeColor[0]} + range={Math.round((cpuInfo.paneData[0].num / cpuInfo.paneData.reduce((a, b) => a + b.num, 0)) * 100) } + display={cpuNotEmptyData} + >
-
- - {gpuInfo.tag.itemName} -  {gpuInfo.tag.num} - {gpuInfo.tag.unit} - -
-
- { - gpuInfo.paneData.map((item, idx) => - ) - } -
- {/* 数据全为空时,饼图置灰 */} - {gpuNotEmptyData ? ( - ({ value:item.num, color:item.color }))} - > - ) : - } + ({ value:item.num, color:item.color }))} + strokeColor={strokeColor[1]} + range={Math.round((gpuInfo.paneData[0].num / gpuInfo.paneData.reduce((a, b) => a + b.num, 0)) * 100) } + display={gpuNotEmptyData} + >
diff --git a/apps/portal-web/src/pageComponents/dashboard/InfoPane.tsx b/apps/portal-web/src/pageComponents/dashboard/InfoPane.tsx index 5588e9b535..2b22b38121 100644 --- a/apps/portal-web/src/pageComponents/dashboard/InfoPane.tsx +++ b/apps/portal-web/src/pageComponents/dashboard/InfoPane.tsx @@ -12,9 +12,11 @@ import { Card, Tag } from "antd"; import React, { useMemo } from "react"; +import { prefix, useI18nTranslateToString } from "src/i18n"; import { PieChartCom } from "src/pageComponents/dashboard/PieChartCom"; -import { styled } from "styled-components"; ; -import { gray } from "@ant-design/colors"; +import { styled } from "styled-components"; + +import { TitleContainer } from "./TitleContainer"; interface LineProps { itemName: string; @@ -57,28 +59,14 @@ export interface PaneData { color: string; } interface Props { - title?: Title; tag: Tag; paneData: PaneData[]; loading: boolean; + strokeColor: string; } const Container = styled.div` -margin: 20px 0; -`; - -export const TitleContainer = styled.div` - height: 45px; - font-size: 16px; - font-weight: 600; - padding-bottom: 20px; - display: flex; - align-items: center; - justify-content: start; - - span:nth-child(2) { - color: ${gray[5]}; - } +margin: 0px 0; `; export const PieChartContainer = styled.div` @@ -86,45 +74,39 @@ export const PieChartContainer = styled.div` justify-content: center; `; -export const InfoPane: React.FC = ({ title, tag, paneData, loading }) => { +const p = prefix("pageComp.dashboard.infoPane."); + +export const InfoPane: React.FC = ({ paneData, loading, strokeColor }) => { + + const t = useI18nTranslateToString(); const notEmptyData = useMemo(() => { return paneData.some((x) => x.num > 0); }, [paneData]); + return ( - - {title ? ( - - {title.title} - {title.subTitle ? `[${title.subTitle}]` : " "} - - ) - : undefined} -
- - {tag.itemName} -  {tag.num} - {tag.unit} - -
-
- { - paneData.map((item, idx) => - ) - } -
+ a + b.num, 0)} + available={paneData[1].num} + display={notEmptyData} + > + )} + style={{ maxHeight:"310px", boxShadow: "0px 2px 10px 0px #1C01011A" }} + > - {/* 数据全为空时,饼图置灰 */} - {notEmptyData ? - ({ value:item.num, color:item.color }))}> : - } - + ({ value:item.num, color:item.color }))} + strokeColor={strokeColor} + range={Math.round((paneData[0].num / paneData.reduce((a, b) => a + b.num, 0)) * 100) } + display={notEmptyData} + > diff --git a/apps/portal-web/src/pageComponents/dashboard/InfoPanes.tsx b/apps/portal-web/src/pageComponents/dashboard/InfoPanes.tsx index 783754bd40..3018c78a30 100644 --- a/apps/portal-web/src/pageComponents/dashboard/InfoPanes.tsx +++ b/apps/portal-web/src/pageComponents/dashboard/InfoPanes.tsx @@ -14,21 +14,20 @@ import React from "react"; import { DoubleInfoPane } from "src/pageComponents/dashboard/DoubleInfoPane"; import { InfoPane } from "src/pageComponents/dashboard/InfoPane"; import { styled } from "styled-components"; ; -import { cyan, geekblue, red } from "@ant-design/colors"; -import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; -import { Col, Row } from "antd"; +import { Card, Col, Row } from "antd"; +import { useStore } from "simstate"; import { prefix, useI18n, useI18nTranslateToString } from "src/i18n"; -import { ClusterInfo } from "src/pageComponents/dashboard/OverviewTable"; +import { ClusterOverview, PlatformOverview } from "src/models/cluster"; +import JobInfo from "src/pageComponents/dashboard/NodeRange"; +import { ClusterInfoStore } from "src/stores/ClusterInfoStore"; interface Props { - selectItem: ClusterInfo; + selectItem: ClusterOverview | PlatformOverview | undefined; loading: boolean; + activeTabKey: string; + onTabChange: (key: string) => void; } -const Container = styled.div` - -`; - const InfoPaneContainer = styled.div` min-width: 350px; `; @@ -43,82 +42,131 @@ const DoubleInfoPaneContainer = styled.div` const colors = { - running:cyan[5], - idle:geekblue[5], - notAvailable:red[8], + // 节点使用率颜色 + nodeUtilizationAvailable:"#6959CA", + nodeUtilizationNotavailable:"#CFCAEE", + nodeUtilizationStroke:"#E9E6F7", + + // CPU核心使用率颜色 + cpuAvailable:"#4DA2AE", + cpunotAvailable:"#B0D6DC", + cpuStroke:"#DBECEF", + + // GPU核心使用率颜色 + gpuAvailable:"#CDE044", + gpunotAvailable:"#E1EC8F", + gpuStroke:"#F5F9DA", + + // 作业字体颜色 + runningJob:"#D1CB5B", + queuing:"#A58E74", }; const p = prefix("pageComp.dashboard.infoPanes."); -export const InfoPanes: React.FC = ({ selectItem, loading }) => { +export const InfoPanes: React.FC = ({ selectItem, loading, activeTabKey, onTabChange }) => { const t = useI18nTranslateToString(); const languageId = useI18n().currentLanguage.id; - const { clusterName, partitionName, nodeCount, runningNodeCount, idleNodeCount, notAvailableNodeCount, + const { currentClusters } = useStore(ClusterInfoStore); + + // card的每一项 + const clusterCardsList = [ + { + key:"platformOverview", + tab: +
+ {t(p("platformOverview"))} +
, + }, ...currentClusters.map((x) => ({ + key:x.id, + tab:typeof (x.name) == "string" ? x.name : x.name.i18n[languageId], + })), + ]; + + + const { nodeCount, runningNodeCount, idleNodeCount, notAvailableNodeCount, cpuCoreCount, runningCpuCount, idleCpuCount, notAvailableCpuCount, - gpuCoreCount, runningGpuCount, idleGpuCount, notAvailableGpuCount, - jobCount, runningJobCount, pendingJobCount, - } = selectItem; + gpuCoreCount, runningGpuCount, idleGpuCount, notAvailableGpuCount, runningJobCount, pendingJobCount, + } = selectItem ?? { + nodeCount: 0, + runningNodeCount: 0, + idleNodeCount: 0, + notAvailableNodeCount: 0, + cpuCoreCount: 0, + runningCpuCount: 0, + idleCpuCount: 0, + notAvailableCpuCount: 0, + gpuCoreCount: 0, + runningGpuCount: 0, + idleGpuCount: 0, + notAvailableGpuCount: 0, + jobCount: 0, + runningJobCount: 0, + pendingJobCount: 0, + }; + return ( - + - - + display={!(runningJobCount == 0 && pendingJobCount == 0)} + /> - - +
); }; diff --git a/apps/portal-web/src/pageComponents/dashboard/NodeRange.tsx b/apps/portal-web/src/pageComponents/dashboard/NodeRange.tsx new file mode 100644 index 0000000000..b87fbfb8c4 --- /dev/null +++ b/apps/portal-web/src/pageComponents/dashboard/NodeRange.tsx @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { Card, Typography } from "antd"; +import { prefix, useI18nTranslateToString } from "src/i18n"; +import { styled } from "styled-components"; + +const { Text } = Typography; + +interface Props { + runningJobs: string; + pendingJobs: string; + loading: boolean; + display: boolean; +} + +const JobInfoContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-left:15px; + padding-right:15px; +`; + +const JobNumber = styled(Text)` + margin: 0; + font-weight:700; + font-size:64px; +`; + +const JobLabel = styled(Text)` + margin: 0; + font-weight:500; +`; + +const JobInfoRow = styled.div` + display: flex; + width:100%; + align-items:center; + border-bottom:2px solid #DEDEDE; + justify-content: space-between; +`; + +const Container = styled.div` +margin: 0px 0; +`; + +const p = prefix("pageComp.dashboard.nodeRange."); + +const JobInfo: React.FC = ({ runningJobs, pendingJobs, loading, display }) => { + const t = useI18nTranslateToString(); + + // 没有数据时不显示 + if (!display) { + return null; + } + + return ( + + {t(p("jobs"))}} + style={{ maxHeight:"310px", boxShadow: "0px 2px 10px 0px #1C01011A" }} + loading={loading} + > + + + {runningJobs} + {t(p("running"))} + + + {pendingJobs} + {t(p("pending"))} + + + + + + ); +}; + +export default JobInfo; diff --git a/apps/portal-web/src/pageComponents/dashboard/OverviewTable.tsx b/apps/portal-web/src/pageComponents/dashboard/OverviewTable.tsx index cac5fd580c..364b3b3bf6 100644 --- a/apps/portal-web/src/pageComponents/dashboard/OverviewTable.tsx +++ b/apps/portal-web/src/pageComponents/dashboard/OverviewTable.tsx @@ -13,9 +13,10 @@ import { I18nStringType } from "@scow/config/build/i18n"; import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/systemLanguage"; import { PartitionInfo, PartitionInfo_PartitionStatus } from "@scow/protos/build/portal/config"; -import { Table, Tag } from "antd"; -import React, { useMemo, useState } from "react"; +import { Progress, Table, Tag } from "antd"; +import React, { useEffect, useMemo, useState } from "react"; import { Localized, prefix, useI18n, useI18nTranslateToString } from "src/i18n"; +import { ClusterOverview, PlatformOverview } from "src/models/cluster"; import { InfoPanes } from "src/pageComponents/dashboard/InfoPanes"; import { Cluster } from "src/utils/cluster"; import { compareWithUndefined } from "src/utils/dashboard"; @@ -35,6 +36,8 @@ interface Props { failedClusters: ({clusterName: I18nStringType})[]; currentClusters: Cluster[]; isLoading: boolean; + clustersOverview: ClusterOverview[]; + platformOverview?: PlatformOverview | undefined; } interface InfoProps { @@ -90,29 +93,67 @@ const Container = styled.div` const p = prefix("pageComp.dashboard.overviewTable."); -export const OverviewTable: React.FC = ({ clusterInfo, failedClusters, currentClusters, isLoading }) => { +export const OverviewTable: React.FC = ({ clusterInfo, failedClusters, + currentClusters, isLoading, clustersOverview, platformOverview }) => { const t = useI18nTranslateToString(); const languageId = useI18n().currentLanguage.id; - const [selectId, setSelectId] = useState(0); + const [selectId, setSelectId] = useState(undefined); - const selectItem = useMemo(() => clusterInfo[selectId], [clusterInfo, selectId]); + const selectItem = useMemo(() => clusterInfo[selectId ?? 0], [clusterInfo, selectId]); + + // 控制Tab切换 + const [activeTabKey, setActiveTabKey] = useState("platformOverview"); + + // 找到对应平台概览 + const selectedClusterOverview = useMemo(() => { + if (!selectItem?.clusterName) { + return undefined; + }; + return clustersOverview.find( + (overview) => + overview.clusterName === selectItem.clusterName, + ); + }, [selectItem, clustersOverview, languageId]); + + + // 当activekey改变时表格数据显示的逻辑 + const filteredClusterInfo = useMemo(() => { + if (activeTabKey === "platformOverview") { + setSelectId(undefined); + return clusterInfo; + } + return clusterInfo.filter((info) => info.clusterName === activeTabKey); + }, [activeTabKey, clusterInfo, languageId]); + + useEffect(() => { + if (activeTabKey !== "platformOverview") { + const selectedInfo = clusterInfo.find((info) => info.clusterName === activeTabKey); + if (selectedInfo) { + setSelectId(selectedInfo.id); + } + } + }, [activeTabKey, clusterInfo]); - // 定义一个函数来获取颜色,根据给定的使用率 - const getColorByUsage = (usage: number) => { - if (usage >= 90) return "red"; - if (usage >= 70) return "orange"; - return "green"; - }; return ( (isLoading || currentClusters.length > 0) ? ( + t(p("title"))} + style={{ + marginTop:"15px", + }} tableLayout="fixed" - dataSource={(clusterInfo.map((x) => ({ clusterName:x.clusterName, info:{ ...x } })) as Array) - .concat(failedClusters)} + dataSource={(filteredClusterInfo.map((x) => + ({ clusterName:x.clusterName, info:{ ...x } })) as Array) + .concat(failedClusters) + } loading={isLoading} pagination={false} scroll={{ y:275 }} @@ -122,6 +163,7 @@ export const OverviewTable: React.FC = ({ clusterInfo, failedClusters, cu onClick() { if (r.info?.id !== undefined) { setSelectId(r.info?.id); + setActiveTabKey(getI18nConfigCurrentText(r.clusterName, languageId)); } }, }; @@ -131,10 +173,15 @@ export const OverviewTable: React.FC = ({ clusterInfo, failedClusters, cu dataIndex="clusterName" width="15%" title={t(p("clusterName"))} + hidden={activeTabKey !== "platformOverview"} sorter={(a, b, sortOrder) => compareWithUndefined(getI18nConfigCurrentText(a.clusterName, languageId), getI18nConfigCurrentText(b.clusterName, languageId), sortOrder)} - render={(clusterName) => getI18nConfigCurrentText(clusterName, languageId)} + render={(clusterName) => ( + + {getI18nConfigCurrentText(clusterName, languageId)} + + )} /> dataIndex="partitionName" @@ -153,12 +200,19 @@ export const OverviewTable: React.FC = ({ clusterInfo, failedClusters, cu title={t(p("usageRatePercentage"))} sorter={(a, b, sortOrder) => compareWithUndefined(a.info?.usageRatePercentage, b.info?.usageRatePercentage, sortOrder)} + hidden={clusterInfo.every((item) => item.usageRatePercentage === undefined)} render={(_, r) => ( - - {r.info?.usageRatePercentage ? `${r.info.usageRatePercentage}%` : "-"} - + r.info?.usageRatePercentage ? ( +
+ +
+ ) : "-" )} /> @@ -166,11 +220,17 @@ export const OverviewTable: React.FC = ({ clusterInfo, failedClusters, cu title={t(p("cpuUsage"))} sorter={(a, b, sortOrder) => compareWithUndefined(a.info?.cpuUsage, b.info?.cpuUsage, sortOrder)} render={(_, r) => ( - - {r.info?.cpuUsage !== undefined ? Number(r.info?.cpuUsage).toFixed(2) + "%" : "-"} - + r.info?.cpuUsage ? ( +
+ +
+ ) : "-" )} /> @@ -178,11 +238,17 @@ export const OverviewTable: React.FC = ({ clusterInfo, failedClusters, cu title={t(p("gpuUsage"))} sorter={(a, b, sortOrder) => compareWithUndefined(a.info?.gpuUsage, b.info?.gpuUsage, sortOrder) } render={(_, r) => ( - - {r.info?.gpuUsage !== undefined ? Number(r.info?.gpuUsage).toFixed(2) + "%" : "-"} - + r.info?.gpuUsage ? ( +
+ +
+ ) : "-" )} /> @@ -202,7 +268,6 @@ export const OverviewTable: React.FC = ({ clusterInfo, failedClusters, cu } />
-
) : ( diff --git a/apps/portal-web/src/pageComponents/dashboard/PieChartCom.tsx b/apps/portal-web/src/pageComponents/dashboard/PieChartCom.tsx index 7b8329bfa4..7074e865fc 100644 --- a/apps/portal-web/src/pageComponents/dashboard/PieChartCom.tsx +++ b/apps/portal-web/src/pageComponents/dashboard/PieChartCom.tsx @@ -16,6 +16,9 @@ import { Cell, Pie, PieChart } from "recharts"; interface Props { pieData: PieData[]; + strokeColor: string; + range: number; + display: boolean; } interface PieData { @@ -24,24 +27,66 @@ interface PieData { } const Container = styled.div` + position:relative; + bottom:5.4em; `; -export const PieChartCom: React.FC = ({ pieData }) => { +const JobRange = styled.div` + font-weight:700; + position:relative; + width:max-content; + left:50%; + top:50%; + transform: translate(-52%,0); +`; + +export const PieChartCom: React.FC = ({ pieData, range, strokeColor, display }) => { + + // 没有值的时候不显示 + if (!display) { + return null; + } + return ( - + + {Math.min(range, 100) + "%"} + + + + + {pieData.map((entry, index) => ( - + ))} + + + ); diff --git a/apps/portal-web/src/pageComponents/dashboard/TitleContainer.tsx b/apps/portal-web/src/pageComponents/dashboard/TitleContainer.tsx new file mode 100644 index 0000000000..dfc9256997 --- /dev/null +++ b/apps/portal-web/src/pageComponents/dashboard/TitleContainer.tsx @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy + * SCOW is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import React from "react"; +import { prefix, useI18nTranslateToString } from "src/i18n"; +import { styled } from "styled-components"; + +// 标题容器 +const Container = styled.div` + width:100%; + display:flex; + justify-content: space-between; +`; +// 标题 +const Title = styled.div` + font-weight:700; + font-size:1.14em; + width:max-content; +`; +// 副标题容器 +const SubContainer = styled.div` + display:flex; + font-weight:700; + font-size:1.14em; + width:max-content; +`; + +interface Props { + // 总共节点 + total: Number, + // 可用节点 + available: Number, + // 标题名称 + name: String, + // 是否显示 + display: Boolean, +} + +const p = prefix("pageComp.dashboard.titleContainer."); + +export const TitleContainer: React.FC = ({ total, available, name, display }) => { + const t = useI18nTranslateToString(); + + // 没有数据的时候不显示 + if (!display) { + return null; + } + + return ( + + {name} + + + {t(p("available"))} + {available.toFixed(0)} + /{total.toFixed(0)} + + + + ); + +}; diff --git a/apps/portal-web/src/pages/dashboard.tsx b/apps/portal-web/src/pages/dashboard.tsx index 264ab50331..8eed1a436e 100644 --- a/apps/portal-web/src/pages/dashboard.tsx +++ b/apps/portal-web/src/pages/dashboard.tsx @@ -19,6 +19,7 @@ import { useStore } from "simstate"; import { api } from "src/apis"; import { requireAuth } from "src/auth/requireAuth"; import { useI18nTranslateToString } from "src/i18n"; +import { ClusterOverview, PlatformOverview } from "src/models/cluster"; import { OverviewTable } from "src/pageComponents/dashboard/OverviewTable"; import { QuickEntry } from "src/pageComponents/dashboard/QuickEntry"; import { ClusterInfoStore } from "src/stores/ClusterInfoStore"; @@ -33,6 +34,7 @@ interface FulfilledResult { clusterInfo: {clusterName: string, partitions: PartitionInfo[]} } + export const DashboardPage: NextPage = requireAuth(() => true)(() => { const userStore = useStore(UserStore); @@ -86,24 +88,123 @@ export const DashboardPage: NextPage = requireAuth(() => true)(() => { ); const clustersInfo = successfulResults - .map((cluster) => ({ clusterInfo: { ...cluster.clusterInfo, - clusterName: currentClusters.find((x) => x.id === cluster.clusterInfo.clusterName)?.name } })) + .map((cluster) => ({ + clusterInfo: { + ...cluster.clusterInfo, + clusterName: cluster.clusterInfo.clusterName, + }, + })) .flatMap((cluster) => cluster.clusterInfo.partitions.map((x) => ({ clusterName: cluster.clusterInfo.clusterName, ...x, - cpuUsage:((x.runningCpuCount / x.cpuCoreCount) * 100).toFixed(2), - // 有些分区没有gpu就为空,前端显示'-' - ...x.gpuCoreCount ? { gpuUsage:((x.runningGpuCount / x.gpuCoreCount) * 100).toFixed(2) } : {}, + cpuUsage: ((x.runningCpuCount / x.cpuCoreCount) * 100).toFixed(2), + gpuUsage: x.gpuCoreCount ? ((x.runningGpuCount / x.gpuCoreCount) * 100).toFixed(2) : undefined, })), ); + // 平台概览信息 + const platformOverview: PlatformOverview = { + nodeCount:0, + runningNodeCount:0, + idleNodeCount:0, + notAvailableNodeCount:0, + cpuCoreCount:0, + runningCpuCount:0, + idleCpuCount:0, + notAvailableCpuCount:0, + gpuCoreCount:0, + runningGpuCount:0, + idleGpuCount:0, + notAvailableGpuCount:0, + jobCount:0, + runningJobCount:0, + pendingJobCount:0, + usageRatePercentage:0, + partitionStatus:0, + }; + + // 各个集群概览信息 + const clustersOverview: ClusterOverview[] = []; + successfulResults.forEach((result) => { + const { clusterName, partitions } = result.clusterInfo; + + const aggregatedData = partitions.reduce( + (acc, partition) => { + acc.nodeCount += partition.nodeCount; + acc.runningNodeCount += partition.runningNodeCount; + acc.idleNodeCount += partition.idleNodeCount; + acc.notAvailableNodeCount += partition.notAvailableNodeCount; + acc.cpuCoreCount += partition.cpuCoreCount; + acc.runningCpuCount += partition.runningCpuCount; + acc.idleCpuCount += partition.idleCpuCount; + acc.notAvailableCpuCount += partition.notAvailableCpuCount; + acc.gpuCoreCount += partition.gpuCoreCount; + acc.runningGpuCount += partition.runningGpuCount; + acc.idleGpuCount += partition.idleGpuCount; + acc.notAvailableGpuCount += partition.notAvailableGpuCount; + acc.jobCount += partition.jobCount; + acc.runningJobCount += partition.runningJobCount; + acc.pendingJobCount += partition.pendingJobCount; + acc.partitionStatus += partition.partitionStatus; + return acc; + }, + { + clusterName, + nodeCount: 0, + runningNodeCount: 0, + idleNodeCount: 0, + notAvailableNodeCount: 0, + cpuCoreCount: 0, + runningCpuCount: 0, + idleCpuCount: 0, + notAvailableCpuCount: 0, + gpuCoreCount: 0, + runningGpuCount: 0, + idleGpuCount: 0, + notAvailableGpuCount: 0, + jobCount: 0, + runningJobCount: 0, + pendingJobCount: 0, + usageRatePercentage: 0, + partitionStatus: 0, + }, + ); + + // 累加平台概览信息 + platformOverview.nodeCount += aggregatedData.nodeCount; + platformOverview.runningNodeCount += aggregatedData.runningNodeCount; + platformOverview.idleNodeCount += aggregatedData.idleNodeCount; + platformOverview.notAvailableNodeCount += aggregatedData.notAvailableNodeCount; + platformOverview.cpuCoreCount += aggregatedData.cpuCoreCount; + platformOverview.runningCpuCount += aggregatedData.runningCpuCount; + platformOverview.idleCpuCount += aggregatedData.idleCpuCount; + platformOverview.notAvailableCpuCount += aggregatedData.notAvailableCpuCount; + platformOverview.gpuCoreCount += aggregatedData.gpuCoreCount; + platformOverview.runningGpuCount += aggregatedData.runningGpuCount; + platformOverview.idleGpuCount += aggregatedData.idleGpuCount; + platformOverview.notAvailableGpuCount += aggregatedData.notAvailableGpuCount; + platformOverview.jobCount += aggregatedData.jobCount; + platformOverview.runningJobCount += aggregatedData.runningJobCount; + platformOverview.pendingJobCount += aggregatedData.pendingJobCount; + platformOverview.partitionStatus += aggregatedData.partitionStatus; + + aggregatedData.usageRatePercentage = + Number(((aggregatedData.runningNodeCount / aggregatedData.nodeCount) * 100).toFixed(2)); + + clustersOverview.push(aggregatedData); + }); + + platformOverview.usageRatePercentage = + Number(((platformOverview.runningNodeCount / platformOverview.nodeCount) * 100).toFixed(2)); + return { clustersInfo, - failedClusters:failedClusters.map((x) => ({ clusterName:x.name })), + failedClusters: failedClusters.map((x) => ({ clusterName: x.name })), + clustersOverview, + platformOverview, }; - - }, []), + }, [currentClusters]), }); return ( @@ -115,6 +216,8 @@ export const DashboardPage: NextPage = requireAuth(() => true)(() => { clusterInfo={data?.clustersInfo ? data.clustersInfo.map((item, idx) => ({ ...item, id:idx })) : []} failedClusters={data?.failedClusters ?? []} currentClusters={currentClusters} + clustersOverview={data?.clustersOverview ?? []} + platformOverview={data?.platformOverview } /> ); diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 879943dac7..3f271e8100 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -36,7 +36,7 @@ } html[data-theme='dark'] .docusaurus-highlight-code-line { - background-color: rgba(0, 0, 0, 0.3); + background-color: rgba(29, 29, 29, 0.3); } html[data-theme='dark'] .alert--success { diff --git a/libs/web/src/layouts/base/header/index.tsx b/libs/web/src/layouts/base/header/index.tsx index 105affddf8..864c1668a2 100644 --- a/libs/web/src/layouts/base/header/index.tsx +++ b/libs/web/src/layouts/base/header/index.tsx @@ -32,6 +32,8 @@ const Container = styled.header` z-index: 50; align-items: center; background-color: ${({ theme }) => theme.token.colorBgContainer}; + font-weight:700; + font-size:18px; `;