Skip to content

Commit

Permalink
feat(mis-web): 管理系统用户可见分区追加优化 (#882)
Browse files Browse the repository at this point in the history
### 做了什么
根据优化需求进行优化,将集群返回的错误信息返回并展示

**优化需求**
每个集群一个表,显示可用分区以及价格
分区信息的显示顺序,按照配置文件中的集群的优先级顺序显示
只显示一个spinner, 如果某个集群分区获取出现错误则不显示整个集群的可见分区信息

**优化后效果**

- 多集群分区获取失败时


![多集群失败](https://github.com/PKUHPC/SCOW/assets/43978285/3165bd42-fb56-4864-8032-d14bf01ee093)

- 多集群分区获取正常时


![多集群](https://github.com/PKUHPC/SCOW/assets/43978285/6a7c4880-4d10-4c87-8ae8-cd5a1d45a757)

- 多集群部分集群分区信息获取失败时


![某一个集群出现了错误](https://github.com/PKUHPC/SCOW/assets/43978285/39894768-c7cb-4b00-a1c2-8b1f64cb1256)
  • Loading branch information
piccaSun authored Oct 20, 2023
1 parent 29e4b18 commit 914f6c8
Show file tree
Hide file tree
Showing 13 changed files with 195 additions and 101 deletions.
6 changes: 6 additions & 0 deletions .changeset/odd-chairs-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@scow/mis-server": patch
"@scow/mis-web": patch
---

修改管理系统用户可见分区为按不同集群响应分开展示,页面展示顺序为按集群优先级顺序
5 changes: 5 additions & 0 deletions .changeset/ten-gorillas-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@scow/grpc-api": minor
---

新增getAvailablePartitionsForPartitions接口获取某个集群下可见分区信息,getAvailablePartitions变更为deprecated将在下一个大版本中被删除
2 changes: 1 addition & 1 deletion apps/mis-server/src/entities/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* See the Mulan PSL v2 for more details.
*/

import { BooleanType, Collection, Entity,
import { Collection, Entity,
ManyToOne, OneToMany, OneToOne, PrimaryKey, Property,
Ref } from "@mikro-orm/core";
import { Decimal } from "@scow/lib-decimal";
Expand Down
22 changes: 22 additions & 0 deletions apps/mis-server/src/services/misConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import { ConfigServiceServer, ConfigServiceService } from "@scow/protos/build/se

export const misConfigServiceServer = plugin((server) => {
server.addService<ConfigServiceServer>(ConfigServiceService, {

/**
* Deprecated Notice
* This API function GetAvailablePartitions has been deprecated.
* Use the new API function GetAvailablePartitionsForCluster instead.
* @deprecated
*/
getAvailablePartitions: async ({ request, logger }) => {

const { accountName, userId } = request;
Expand All @@ -32,5 +39,20 @@ export const misConfigServiceServer = plugin((server) => {

return [{ clusterPartitions: wrappedResult } ];
},


getAvailablePartitionsForCluster: async ({ request, logger }) => {

const { cluster, accountName, userId } = request;
const reply = await server.ext.clusters.callOnOne(
cluster,
logger,
async (client) => await asyncClientCall(client.config, "getAvailablePartitions", {
accountName, userId,
}),
);

return [reply];
},
});
});
6 changes: 3 additions & 3 deletions apps/mis-web/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

const { envConfig, str, bool } = require("@scow/lib-config");
const { getClusterConfigs } = require("@scow/config/build/cluster");
const { getClusterConfigs, getSortedClusters } = require("@scow/config/build/cluster");
const { getMisConfig } = require("@scow/config/build/mis");
const { getCommonConfig } = require("@scow/config/build/common");
const { getClusterTextsConfig } = require("@scow/config/build/clusterTexts");
Expand Down Expand Up @@ -123,8 +123,8 @@ const buildRuntimeConfig = async (phase, basePath) => {

PUBLIC_PATH: config.PUBLIC_PATH,

CLUSTERS: Object.keys(clusters).reduce((prev, curr) => {
prev[curr] = { id: curr, name: clusters[curr].displayName };
CLUSTERS: getSortedClusters(clusters).reduce((prev, curr) => {
prev[curr.id] = { id: curr.id, name: curr.displayName };
return prev;
}, {}),

Expand Down
1 change: 1 addition & 0 deletions apps/mis-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@scow/lib-web": "workspace:*",
"@scow/utils": "workspace:*",
"@scow/lib-operation-log": "workspace:*",
"@scow/rich-error-model": "workspace:*",
"@sinclair/typebox": "0.31.1",
"antd": "5.8.4",
"dayjs": "1.11.9",
Expand Down
2 changes: 1 addition & 1 deletion apps/mis-web/src/apis/api.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ export const mockApi: MockApi<typeof api> = {

initGetUsers: async () => ({ users: mockUsers }),

getBillingTable: null,
getAvailableBillingTable: null,

createInitAdmin: async () => ({ createdInAuth: false }),

Expand Down
4 changes: 2 additions & 2 deletions apps/mis-web/src/apis/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ import type { UnsetInitAdminSchema } from "src/pages/api/init/unsetInitAdmin";
import type { UserExistsSchema } from "src/pages/api/init/userExists";
import type { AddBillingItemSchema } from "src/pages/api/job/addBillingItem";
import type { ChangeJobTimeLimitSchema } from "src/pages/api/job/changeJobTimeLimit";
import type { GetAvailableBillingTableSchema } from "src/pages/api/job/getAvailableBillingTable";
import type { GetBillingItemsSchema } from "src/pages/api/job/getBillingItems";
import type { GetBillingTableSchema } from "src/pages/api/job/getBillingTable";
import type { GetJobByBiJobIndexSchema } from "src/pages/api/job/getJobByBiJobIndex";
import type { GetMissingDefaultPriceItemsSchema } from "src/pages/api/job/getMissingDefaultPriceItems";
import type { GetJobInfoSchema } from "src/pages/api/job/jobInfo";
Expand Down Expand Up @@ -125,7 +125,7 @@ export const api = {
addBillingItem: apiClient.fromTypeboxRoute<typeof AddBillingItemSchema>("POST", "/api/job/addBillingItem"),
changeJobTimeLimit: apiClient.fromTypeboxRoute<typeof ChangeJobTimeLimitSchema>("PATCH", "/api/job/changeJobTimeLimit"),
getBillingItems: apiClient.fromTypeboxRoute<typeof GetBillingItemsSchema>("GET", "/api/job/getBillingItems"),
getBillingTable: apiClient.fromTypeboxRoute<typeof GetBillingTableSchema>("GET", "/api/job/getBillingTable"),
getAvailableBillingTable: apiClient.fromTypeboxRoute<typeof GetAvailableBillingTableSchema>("GET", "/api/job/getAvailableBillingTable"),
getJobByBiJobIndex: apiClient.fromTypeboxRoute<typeof GetJobByBiJobIndexSchema>("GET", "/api/job/getJobByBiJobIndex"),
getMissingDefaultPriceItems: apiClient.fromTypeboxRoute<typeof GetMissingDefaultPriceItemsSchema>("GET", "/api/job/getMissingDefaultPriceItems"),
getJobInfo: apiClient.fromTypeboxRoute<typeof GetJobInfoSchema>("GET", "/api/job/jobInfo"),
Expand Down
16 changes: 10 additions & 6 deletions apps/mis-web/src/components/JobBillingTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ export interface JobBillingTableItem {
interface Props {
data: JobBillingTableItem[] | undefined;
loading?: boolean;
isUserPartitionsPage?: boolean;
}

const p = prefix("component.others.");
const pCommon = prefix("common.");

export const JobBillingTable: React.FC<Props> = ({ data, loading }) => {
export const JobBillingTable: React.FC<Props> = ({ data, loading, isUserPartitionsPage }) => {

const t = useI18nTranslateToString();
const languageId = useI18n().currentLanguage.id;
Expand All @@ -71,10 +72,13 @@ export const JobBillingTable: React.FC<Props> = ({ data, loading }) => {
}, {}) : {};

const columns: ColumnsType<JobBillingTableItem> = [
{ dataIndex: "cluster", title: t(pCommon("cluster")), key: "index", render: (_, r) => ({
children: getI18nConfigCurrentText(publicConfig.CLUSTERS[r.cluster]?.name, languageId) ?? r.cluster,
props: { rowSpan: r.clusterItemIndex === 0 && clusterTotalQosCounts ? clusterTotalQosCounts[r.cluster] : 0 },
}) },
...(isUserPartitionsPage ? [] : [
{ dataIndex: "cluster", title: t(pCommon("cluster")), key: "index", render: (_, r) => ({
children: getI18nConfigCurrentText(publicConfig.CLUSTERS[r.cluster]?.name, languageId) ?? r.cluster,
props: { rowSpan: r.clusterItemIndex === 0 && clusterTotalQosCounts ? clusterTotalQosCounts[r.cluster] : 0 },
}) },
])
,
{ dataIndex: "partition", title: t(p("partitionFullName")), key: "index", render: (_, r) => ({
children: r.partition,
props: { rowSpan: r.partitionItemIndex === 0 ? r.qosCount : 0 },
Expand Down Expand Up @@ -125,7 +129,7 @@ export const JobBillingTable: React.FC<Props> = ({ data, loading }) => {
<Table
dataSource={data}
columns={columns}
scroll={{ x: 800 }}
scroll={{ x: 800, y: 500 }}
size="middle"
bordered
pagination={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@

import { typeboxRoute, typeboxRouteSchema } from "@ddadaal/next-typed-api-routes-runtime";
import { asyncClientCall } from "@ddadaal/tsgrpc-client";
import { ConfigServiceClient } from "@scow/protos/build/common/config";
import { ConfigServiceClient as MisConfigServerClient } from "@scow/protos/build/server/config";
import { JobBillingItem } from "@scow/protos/build/server/job";
import { UserStatus } from "@scow/protos/build/server/user";
import { Static, Type } from "@sinclair/typebox";
import { authenticate } from "src/auth/server";
import { getBillingItems } from "src/pages/api/job/getBillingItems";
import { getClient } from "src/utils/client";
import { runtimeConfig } from "src/utils/config";
import { moneyToString } from "src/utils/money";

import { getUserStatus } from "../dashboard/status";
Expand Down Expand Up @@ -71,10 +69,11 @@ export const ClusterPartitions = Type.Object({
});
export type ClusterPartitions = Static<typeof ClusterPartitions>;

export const GetBillingTableSchema = typeboxRouteSchema({
export const GetAvailableBillingTableSchema = typeboxRouteSchema({
method: "GET",

query: Type.Object({
cluster: Type.String(),
tenant: Type.Optional(Type.String()),
userId: Type.Optional(Type.String()),
}),
Expand All @@ -83,11 +82,12 @@ export const GetBillingTableSchema = typeboxRouteSchema({
200: Type.Object({
items: Type.Array(JobBillingTableItem),
}),

},
});

export async function getAvailablePartitionForItems(
userId: string, tenantName: string): Promise<{[cluster: string]: Partition[]}> {
cluster: string, userId: string, tenantName: string): Promise<Partition[]> {

const client = getClient(MisConfigServerClient);

Expand All @@ -97,33 +97,26 @@ export async function getAvailablePartitionForItems(
(key) => (!statuses.accountStatuses[key].accountBlocked
&& statuses.accountStatuses[key].userStatus !== UserStatus.BLOCKED));

if (!accountNames) { return {}; }
if (!accountNames) { return []; }

const clusterPartitionsMap: { [cluster: string]: Partition[] } = {};
const partitions: Partition[] = [];

await Promise.all(accountNames
await Promise.allSettled(accountNames
.map(async (accountName) => {
const availableCPs = await asyncClientCall(client, "getAvailablePartitions",
{ accountName: accountName, userId: userId }).then((resp) => {
return resp.clusterPartitions;
});
availableCPs.forEach((cp) => {

const cluster = cp.cluster;
if (!(cluster in clusterPartitionsMap)) {
clusterPartitionsMap[cluster] = [];
}
if (cp.partitions) {
clusterPartitionsMap[cluster] = clusterPartitionsMap[cluster].concat(cp.partitions);
}
});
}));
return await asyncClientCall(client, "getAvailablePartitionsForCluster",
{ cluster, accountName, userId });
}),
).then((results) => {
results.forEach((result) => {
if (result.status === "fulfilled") {
partitions.push(...result.value.partitions);
}
});
});

for (const cluster of Object.keys(clusterPartitionsMap)) {
clusterPartitionsMap[cluster] = removeDuplicatesByPName(clusterPartitionsMap[cluster]);
}
const result = removeDuplicatesByPName(partitions);

return clusterPartitionsMap;
return result;
}

const removeDuplicatesByPName = (partitions: Partition[]): Partition[] => {
Expand All @@ -138,8 +131,10 @@ const removeDuplicatesByPName = (partitions: Partition[]): Partition[] => {
return uniquePartitions;
};

export async function getBillingTableItems(
tenantName: string | undefined, userId?: string | undefined): Promise<JobBillingTableItem[]> {
export async function getAvailableBillingTableItems(
cluster: string,
tenantName: string | undefined,
userId: string | undefined): Promise<JobBillingTableItem[]> {
const items = (await getBillingItems(tenantName, true)).activeItems;

const pathItemMap = items.reduce((prev, curr) => {
Expand All @@ -149,64 +144,58 @@ export async function getBillingTableItems(

let count = 0;
const tableItems: JobBillingTableItem[] = [];
const clusters = runtimeConfig.CLUSTERS_CONFIG;

const client = getClient(ConfigServiceClient);

const clusterPartitions = userId && tenantName ? await getAvailablePartitionForItems(userId, tenantName) : {};

for (const [cluster] of Object.entries(clusters)) {

const partitions = userId ? (clusterPartitions[cluster] ?? [])
: await asyncClientCall(client, "getClusterConfig", { cluster }).then((resp) => resp.partitions);

const partitionCount = partitions.length;
let clusterItemIndex = 0;
for (const partition of partitions) {
const qosCount = partition.qos?.length ?? 1;
let partitionItemIndex = 0;
for (const qos of partition.qos ?? [""]) {

const path = [cluster, partition.name, qos].filter((x) => x).join(".");

const item = pathItemMap[path];

tableItems.push({
index: count++,
clusterItemIndex: clusterItemIndex++,
partitionItemIndex: partitionItemIndex++,
cluster: cluster,
cores: partition.cores,
gpus: partition.gpus,
mem: partition.memMb,
nodes: partition.nodes,
partition: partition.name,
partitionCount,
qosCount,
qos,
priceItem: item ? {
amount: item.amountStrategy,
itemId: item.id,
price: moneyToString(item.price!),
} : undefined,
path,
comment: partition.comment,
});
}

const partitions = tenantName && userId ?
await getAvailablePartitionForItems(cluster, userId, tenantName) : [];

const partitionCount = partitions.length;
let clusterItemIndex = 0;
for (const partition of partitions) {
const qosCount = partition.qos?.length ?? 1;
let partitionItemIndex = 0;
for (const qos of partition.qos ?? [""]) {

const path = [cluster, partition.name, qos].filter((x) => x).join(".");

const item = pathItemMap[path];

tableItems.push({
index: count++,
clusterItemIndex: clusterItemIndex++,
partitionItemIndex: partitionItemIndex++,
cluster: cluster,
cores: partition.cores,
gpus: partition.gpus,
mem: partition.memMb,
nodes: partition.nodes,
partition: partition.name,
partitionCount,
qosCount,
qos,
priceItem: item ? {
amount: item.amountStrategy,
itemId: item.id,
price: moneyToString(item.price!),
} : undefined,
path,
comment: partition.comment,
});
}
}

return tableItems;

}

export default /* #__PURE__*/typeboxRoute(GetBillingTableSchema, async (req, res) => {
const { tenant, userId } = req.query;
export default /* #__PURE__*/typeboxRoute(GetAvailableBillingTableSchema, async (req, res) => {
const { cluster, tenant, userId } = req.query;
const auth = authenticate(() => true);
const info = await auth(req, res);
if (!info) { return; }

const items = await getBillingTableItems(tenant, userId);
return await getAvailableBillingTableItems(cluster, tenant, userId)
.then((items) => {
return { 200: { items } };
});

return { 200: { items } };
});
Loading

0 comments on commit 914f6c8

Please sign in to comment.