From f6f84b6d609645dfc1f83b10a3096db920e15363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=99=E9=BE=99=E9=BE=99?= Date: Sat, 11 Nov 2023 17:57:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(mis):=20=E7=AE=A1=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E6=9C=AA=E7=BB=93=E6=9D=9F=E4=BD=9C=E4=B8=9A=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=BB=93=E6=9D=9F=E6=93=8D=E4=BD=9C=20(#968)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 与门户系统类似,新增结束作业操作 ![image](https://github.com/PKUHPC/SCOW/assets/140392039/7ced3259-8d96-4cca-ad09-e47b2aa096fc) 与门户系统不同,增加了权限校验: ```typescript const { job, jobAccessible } = await checkJobAccessible(jobId, cluster, info); if (jobAccessible === "NotAllowed") { return { 403: null }; } else if (jobAccessible === "NotFound") { return { 404: { code: "JOB_NOT_FOUND" } as const }; } ``` --- .changeset/tame-crews-boil.md | 7 ++ apps/mis-server/src/services/job.ts | 16 ++++ apps/mis-web/src/apis/api.mock.ts | 1 + apps/mis-web/src/apis/api.ts | 2 + apps/mis-web/src/i18n/en.ts | 3 + apps/mis-web/src/i18n/zh_cn.ts | 3 + .../pageComponents/job/RunningJobTable.tsx | 20 ++++- apps/mis-web/src/pages/api/job/cancelJob.ts | 82 +++++++++++++++++++ protos/server/job.proto | 10 +++ 9 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 .changeset/tame-crews-boil.md create mode 100644 apps/mis-web/src/pages/api/job/cancelJob.ts diff --git a/.changeset/tame-crews-boil.md b/.changeset/tame-crews-boil.md new file mode 100644 index 0000000000..a925848e47 --- /dev/null +++ b/.changeset/tame-crews-boil.md @@ -0,0 +1,7 @@ +--- +"@scow/mis-server": minor +"@scow/mis-web": minor +"@scow/grpc-api": minor +--- + +管理系统未结束作业新增结束操作 diff --git a/apps/mis-server/src/services/job.ts b/apps/mis-server/src/services/job.ts index 1e9294fdfb..423e300a0d 100644 --- a/apps/mis-server/src/services/job.ts +++ b/apps/mis-server/src/services/job.ts @@ -375,5 +375,21 @@ export const jobServiceServer = plugin((server) => { } }, + + cancelJob: async ({ request, logger }) => { + const { cluster, userId, jobId } = request; + + await server.ext.clusters.callOnOne( + cluster, + logger, + async (client) => { + await asyncClientCall(client.job, "cancelJob", { + userId, jobId, + }); + }, + ); + + return [{}]; + }, }); }); diff --git a/apps/mis-web/src/apis/api.mock.ts b/apps/mis-web/src/apis/api.mock.ts index 39fc1f1151..0c7ad03f0d 100644 --- a/apps/mis-web/src/apis/api.mock.ts +++ b/apps/mis-web/src/apis/api.mock.ts @@ -404,6 +404,7 @@ export const mockApi: MockApi = { getAllAccounts: async () => ({ totalCount: mockAccounts.length, results: mockAccounts }), changeJobTimeLimit: async () => null, queryJobTimeLimit: async () => ({ result: 10 }), + cancelJob: async () => null, createAccount: async () => { return {}; }, dewhitelistAccount: async () => null, whitelistAccount: async () => null, diff --git a/apps/mis-web/src/apis/api.ts b/apps/mis-web/src/apis/api.ts index 04650bee36..357ead6746 100644 --- a/apps/mis-web/src/apis/api.ts +++ b/apps/mis-web/src/apis/api.ts @@ -53,6 +53,7 @@ import type { SetAsInitAdminSchema } from "src/pages/api/init/setAsInitAdmin"; 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 { CancelJobSchema } from "src/pages/api/job/cancelJob"; 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"; @@ -89,6 +90,7 @@ import type { UnsetAdminSchema } from "src/pages/api/users/unsetAdmin"; export const api = { + cancelJob: apiClient.fromTypeboxRoute("DELETE", "/api/job/cancelJob"), changeJobPrice: apiClient.fromTypeboxRoute("PATCH", "/api/admin/changeJobPrice"), changePasswordAsPlatformAdmin: apiClient.fromTypeboxRoute("PATCH", "/api/admin/changePassword"), changeStorageQuota: apiClient.fromTypeboxRoute("PUT", "/api/admin/changeStorage"), diff --git a/apps/mis-web/src/i18n/en.ts b/apps/mis-web/src/i18n/en.ts index f3a7057493..b55cc477c8 100644 --- a/apps/mis-web/src/i18n/en.ts +++ b/apps/mis-web/src/i18n/en.ts @@ -499,6 +499,9 @@ export default { limit: "Job Time Limit", changeLimit: "Modify Job Time Limit", gpus: "Number of GPU Cards", + finishJobButton: "Finish", + finishJobConfirm: "Are you sure you want to finish this task?", + finishJobSuccess: "Request to finish the task has been submitted!", }, }, profile: { diff --git a/apps/mis-web/src/i18n/zh_cn.ts b/apps/mis-web/src/i18n/zh_cn.ts index 052ba16809..6f80275005 100644 --- a/apps/mis-web/src/i18n/zh_cn.ts +++ b/apps/mis-web/src/i18n/zh_cn.ts @@ -499,6 +499,9 @@ export default { limit: "作业时间限制", changeLimit: "修改作业时限", gpus: "GPU卡数", + finishJobButton: "结束", + finishJobConfirm: "确定结束这个任务吗?", + finishJobSuccess: "任务结束请求已经提交!", }, }, profile:{ diff --git a/apps/mis-web/src/pageComponents/job/RunningJobTable.tsx b/apps/mis-web/src/pageComponents/job/RunningJobTable.tsx index 0a333f7f02..3b030f5c6a 100644 --- a/apps/mis-web/src/pageComponents/job/RunningJobTable.tsx +++ b/apps/mis-web/src/pageComponents/job/RunningJobTable.tsx @@ -12,7 +12,7 @@ import { useDidUpdateEffect } from "@scow/lib-web/build/utils/hooks"; import { getI18nConfigCurrentText } from "@scow/lib-web/build/utils/i18n"; -import { Button, Form, Input, InputNumber, Select, Space, Table } from "antd"; +import { Button, Form, Input, InputNumber, message, Popconfirm, Select, Space, Table } from "antd"; import React, { useCallback, useMemo, useRef, useState } from "react"; import { useAsync } from "react-async"; import { useStore } from "simstate"; @@ -298,11 +298,27 @@ export const RunningJobInfoTable: React.FC = ({ title={t(pCommon("more"))} - width="9%" + width="12%" fixed="right" render={(_, r) => ( setPreviewItem(r)}>{t(pCommon("detail"))} + + api.cancelJob({ + query: { + cluster: r.cluster.id, + jobId: r.jobId, + }, + }).then(() => { + message.success(t(p("finishJobSuccess"))); + reload(); + }) + } + > + {t(p("finishJobButton"))} + true); + +export default /* #__PURE__*/route(CancelJobSchema, async (req, res) => { + + const info = await auth(req, res); + + if (!info) { return; } + + const { cluster, jobId } = req.query; + + const { job, jobAccessible } = await checkJobAccessible(jobId, cluster, info); + + if (jobAccessible === "NotAllowed") { + return { 403: null }; + } else if (jobAccessible === "NotFound") { + return { 404: { code: "JOB_NOT_FOUND" } as const }; + } + + const client = getClient(JobServiceClient); + + const logInfo = { + operatorUserId: info.identityId, + operatorIp: parseIp(req) ?? "", + operationTypeName: OperationType.endJob, + operationTypePayload: { + jobId: +jobId, accountName: job.account, + }, + }; + + return asyncUnaryCall(client, "cancelJob", { + jobId: +jobId, userId: info.identityId, cluster, + }).then(async () => { + await callLog(logInfo, OperationResult.SUCCESS); + return { 204: null }; + }, handlegRPCError({ + [status.NOT_FOUND]: () => ({ 404: { code: "JOB_NOT_FOUND" } } as const), + }, + async () => await callLog(logInfo, OperationResult.FAIL), + )); +}); diff --git a/protos/server/job.proto b/protos/server/job.proto index e7cc4899d2..b6f52d44c7 100644 --- a/protos/server/job.proto +++ b/protos/server/job.proto @@ -97,6 +97,14 @@ message QueryJobTimeLimitRequest { string job_id = 2; } +message CancelJobRequest { + string cluster = 1; + string user_id = 2; + uint32 job_id = 3; +} + +message CancelJobResponse {} + message GetBillingItemsRequest { // if not specified, return default price items optional string tenant_name = 1; @@ -152,6 +160,8 @@ service JobService { rpc ChangeJobTimeLimit(ChangeJobTimeLimitRequest) returns (ChangeJobTimeLimitResponse); rpc QueryJobTimeLimit(QueryJobTimeLimitRequest) returns (QueryJobTimeLimitResponse); + rpc CancelJob(CancelJobRequest) returns (CancelJobResponse); + rpc GetBillingItems(GetBillingItemsRequest) returns (GetBillingItemsResponse); // ALREADY_EXISTS: item_id already exists