From 795d1b938d17d62c23b9a8ac41d8d4c75cc98469 Mon Sep 17 00:00:00 2001 From: whyour Date: Sat, 23 Oct 2021 18:23:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BE=9D=E8=B5=96=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/api/dependence.ts | 127 +++++++++ back/api/index.ts | 2 + back/config/index.ts | 2 + back/data/dependence.ts | 45 ++++ back/data/sock.ts | 16 ++ back/loaders/sock.ts | 2 +- back/services/dependence.ts | 245 +++++++++++++++++ back/services/sock.ts | 5 +- back/services/user.ts | 19 +- src/layouts/defaultProps.tsx | 6 + src/layouts/index.tsx | 11 +- src/pages/crontab/logModal.tsx | 8 +- src/pages/dependence/index.less | 0 src/pages/dependence/index.tsx | 424 ++++++++++++++++++++++++++++++ src/pages/dependence/logModal.tsx | 122 +++++++++ src/pages/dependence/modal.tsx | 139 ++++++++++ src/pages/env/index.tsx | 4 +- src/pages/env/modal.tsx | 9 +- src/utils/config.ts | 1 + 19 files changed, 1170 insertions(+), 17 deletions(-) create mode 100644 back/api/dependence.ts create mode 100644 back/data/dependence.ts create mode 100644 back/data/sock.ts create mode 100644 back/services/dependence.ts create mode 100644 src/pages/dependence/index.less create mode 100644 src/pages/dependence/index.tsx create mode 100644 src/pages/dependence/logModal.tsx create mode 100644 src/pages/dependence/modal.tsx diff --git a/back/api/dependence.ts b/back/api/dependence.ts new file mode 100644 index 00000000..c8cf6a01 --- /dev/null +++ b/back/api/dependence.ts @@ -0,0 +1,127 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { Container } from 'typedi'; +import DependenceService from '../services/dependence'; +import { Logger } from 'winston'; +import { celebrate, Joi } from 'celebrate'; +const route = Router(); + +export default (app: Router) => { + app.use('/', route); + route.get( + '/dependencies', + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const dependenceService = Container.get(DependenceService); + const data = await dependenceService.dependencies(req.query as any); + return res.send({ code: 200, data }); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); + + route.post( + '/dependencies', + celebrate({ + body: Joi.array().items( + Joi.object({ + name: Joi.string().required(), + type: Joi.number().required(), + remark: Joi.number().optional().allow(''), + }), + ), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const dependenceService = Container.get(DependenceService); + const data = await dependenceService.create(req.body); + return res.send({ code: 200, data }); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); + + route.put( + '/dependencies', + celebrate({ + body: Joi.object({ + name: Joi.string().required(), + _id: Joi.string().required(), + type: Joi.number().required(), + remark: Joi.number().optional().allow(''), + }), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const dependenceService = Container.get(DependenceService); + const data = await dependenceService.update(req.body); + return res.send({ code: 200, data }); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); + + route.delete( + '/dependencies', + celebrate({ + body: Joi.array().items(Joi.string().required()), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const dependenceService = Container.get(DependenceService); + const data = await dependenceService.remove(req.body); + return res.send({ code: 200, data }); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); + + route.get( + '/dependencies/:id', + celebrate({ + params: Joi.object({ + id: Joi.string().required(), + }), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const dependenceService = Container.get(DependenceService); + const data = await dependenceService.get(req.params.id); + return res.send({ code: 200, data }); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); + + route.put( + '/dependencies/reinstall', + celebrate({ + body: Joi.array().items(Joi.string().required()), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const dependenceService = Container.get(DependenceService); + const data = await dependenceService.reInstall(req.body); + return res.send({ code: 200, data }); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); +}; diff --git a/back/api/index.ts b/back/api/index.ts index fec78580..0921a955 100644 --- a/back/api/index.ts +++ b/back/api/index.ts @@ -6,6 +6,7 @@ import log from './log'; import cron from './cron'; import script from './script'; import open from './open'; +import dependence from './dependence'; export default () => { const app = Router(); @@ -16,6 +17,7 @@ export default () => { cron(app); script(app); open(app); + dependence(app); return app; }; diff --git a/back/config/index.ts b/back/config/index.ts index c98ad210..02021b2f 100644 --- a/back/config/index.ts +++ b/back/config/index.ts @@ -29,6 +29,7 @@ const cronDbFile = path.join(rootPath, 'db/crontab.db'); const envDbFile = path.join(rootPath, 'db/env.db'); const appDbFile = path.join(rootPath, 'db/app.db'); const authDbFile = path.join(rootPath, 'db/auth.db'); +const dependenceDbFile = path.join(rootPath, 'db/dependence.db'); const versionFile = path.join(rootPath, 'src/version.ts'); const configFound = dotenv.config({ path: confFile }); @@ -68,6 +69,7 @@ export default { envDbFile, appDbFile, authDbFile, + dependenceDbFile, configPath, scriptPath, samplePath, diff --git a/back/data/dependence.ts b/back/data/dependence.ts new file mode 100644 index 00000000..21e7f0ad --- /dev/null +++ b/back/data/dependence.ts @@ -0,0 +1,45 @@ +export class Dependence { + timestamp?: string; + created?: number; + _id?: string; + status?: DependenceStatus; + type?: DependenceTypes; + name?: number; + log?: string[]; + remark?: string; + + constructor(options: Dependence) { + this._id = options._id; + this.created = options.created || new Date().valueOf(); + this.status = options.status || DependenceStatus.installing; + this.type = options.type || DependenceTypes.nodejs; + this.timestamp = new Date().toString(); + this.name = options.name; + this.log = options.log || []; + this.remark = options.remark || ''; + } +} + +export enum DependenceStatus { + 'installing', + 'installed', + 'installFailed', +} + +export enum DependenceTypes { + 'nodejs', + 'python3', + 'linux', +} + +export enum InstallDependenceCommandTypes { + 'pnpm install -g', + 'pip3 install', + 'apk add --no-cache', +} + +export enum unInstallDependenceCommandTypes { + 'pnpm uninstall -g', + 'pip3 uninstall', + 'apk del', +} diff --git a/back/data/sock.ts b/back/data/sock.ts new file mode 100644 index 00000000..b65c1e31 --- /dev/null +++ b/back/data/sock.ts @@ -0,0 +1,16 @@ +export class SockMessage { + message?: string; + type?: SockMessageType; + references?: string[]; + + constructor(options: SockMessage) { + this.type = options.type; + this.message = options.message; + this.references = options.references; + } +} + +export type SockMessageType = + | 'ping' + | 'installDependence' + | 'updateSystemVersion'; diff --git a/back/loaders/sock.ts b/back/loaders/sock.ts index 15fae712..11bb1c83 100644 --- a/back/loaders/sock.ts +++ b/back/loaders/sock.ts @@ -18,7 +18,7 @@ export default async ({ server }: { server: Server }) => { if (data) { const { token = '', tokens = {} } = JSON.parse(data); if (headerToken === token || tokens[platform] === headerToken) { - conn.write('hanhh'); + conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' })); sockService.addClient(conn); conn.on('data', (message) => { diff --git a/back/services/dependence.ts b/back/services/dependence.ts new file mode 100644 index 00000000..6be5716b --- /dev/null +++ b/back/services/dependence.ts @@ -0,0 +1,245 @@ +import { Service, Inject } from 'typedi'; +import winston from 'winston'; +import config from '../config'; +import DataStore from 'nedb'; +import { + Dependence, + InstallDependenceCommandTypes, + DependenceStatus, + DependenceTypes, + unInstallDependenceCommandTypes, +} from '../data/dependence'; +import _ from 'lodash'; +import { spawn } from 'child_process'; +import SockService from './sock'; + +@Service() +export default class DependenceService { + private dependenceDb = new DataStore({ filename: config.dependenceDbFile }); + constructor( + @Inject('logger') private logger: winston.Logger, + private sockService: SockService, + ) { + this.dependenceDb.loadDatabase((err) => { + if (err) throw err; + }); + } + + public getDb(): DataStore { + return this.dependenceDb; + } + + public async create(payloads: Dependence[]): Promise { + const tabs = payloads.map((x) => { + const tab = new Dependence({ ...x, status: DependenceStatus.installing }); + return tab; + }); + const docs = await this.insert(tabs); + this.installOrUninstallDependencies(docs); + return docs; + } + + public async insert(payloads: Dependence[]): Promise { + return new Promise((resolve) => { + this.dependenceDb.insert(payloads, (err, docs) => { + if (err) { + this.logger.error(err); + } else { + resolve(docs); + } + }); + }); + } + + public async update( + payload: Dependence & { _id: string }, + ): Promise { + const { _id, ...other } = payload; + const doc = await this.get(_id); + const tab = new Dependence({ + ...doc, + ...other, + status: DependenceStatus.installing, + }); + const newDoc = await this.updateDb(tab); + this.installOrUninstallDependencies([newDoc]); + return newDoc; + } + + private async updateDb(payload: Dependence): Promise { + return new Promise((resolve) => { + this.dependenceDb.update( + { _id: payload._id }, + payload, + { returnUpdatedDocs: true }, + (err, num, doc) => { + if (err) { + this.logger.error(err); + } else { + resolve(doc as Dependence); + } + }, + ); + }); + } + + public async remove(ids: string[]) { + return new Promise((resolve: any) => { + this.dependenceDb.find({ _id: { $in: ids } }).exec((err, docs) => { + this.installOrUninstallDependencies(docs, false); + this.removeDb(ids); + resolve(); + }); + }); + } + + public async removeDb(ids: string[]) { + return new Promise((resolve: any) => { + this.dependenceDb.remove( + { _id: { $in: ids } }, + { multi: true }, + async (err) => { + resolve(); + }, + ); + }); + } + + public async dependencies( + { searchValue, type }: { searchValue: string; type: string }, + sort: any = { position: -1 }, + query: any = {}, + ): Promise { + let condition = { ...query, type: DependenceTypes[type as any] }; + if (searchValue) { + const reg = new RegExp(searchValue); + condition = { + ...condition, + $or: [ + { + name: reg, + }, + ], + }; + } + const newDocs = await this.find(condition, sort); + return newDocs; + } + + public async reInstall(ids: string[]): Promise { + return new Promise((resolve: any) => { + this.dependenceDb.update( + { _id: { $in: ids } }, + { $set: { status: DependenceStatus.installing, log: [] } }, + { multi: true, returnUpdatedDocs: true }, + async (err, num, docs: Dependence[]) => { + this.installOrUninstallDependencies(docs); + resolve(docs); + }, + ); + }); + } + + private async find(query: any, sort: any): Promise { + return new Promise((resolve) => { + this.dependenceDb + .find(query) + .sort({ ...sort }) + .exec((err, docs) => { + resolve(docs); + }); + }); + } + + public async get(_id: string): Promise { + return new Promise((resolve) => { + this.dependenceDb.find({ _id }).exec((err, docs) => { + resolve(docs[0]); + }); + }); + } + + private async updateLog(ids: string[], log: string): Promise { + return new Promise((resolve) => { + this.dependenceDb.update( + { _id: { $in: ids } }, + { $push: { log } }, + { multi: true }, + (err, num, doc) => { + if (err) { + this.logger.error(err); + } else { + resolve(); + } + }, + ); + }); + } + + public installOrUninstallDependencies( + dependencies: Dependence[], + isInstall: boolean = true, + ) { + if (dependencies.length === 0) { + return; + } + const depNames = dependencies.map((x) => x.name).join(' '); + const depRunCommand = ( + isInstall + ? InstallDependenceCommandTypes + : unInstallDependenceCommandTypes + )[dependencies[0].type as any]; + const depIds = dependencies.map((x) => x._id) as string[]; + const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' }); + this.sockService.sendMessage({ + type: 'installDependence', + message: `开始安装依赖 ${depNames}`, + references: depIds, + }); + this.updateLog(depIds, `开始安装依赖 ${depNames}\n`); + cp.stdout.on('data', (data) => { + this.sockService.sendMessage({ + type: 'installDependence', + message: data.toString(), + references: depIds, + }); + isInstall && this.updateLog(depIds, data.toString()); + }); + + cp.stderr.on('data', (data) => { + this.sockService.sendMessage({ + type: 'installDependence', + message: data.toString(), + references: depIds, + }); + isInstall && this.updateLog(depIds, data.toString()); + }); + + cp.on('error', (err) => { + this.sockService.sendMessage({ + type: 'installDependence', + message: JSON.stringify(err), + references: depIds, + }); + isInstall && this.updateLog(depIds, JSON.stringify(err)); + }); + + cp.on('close', (code) => { + this.sockService.sendMessage({ + type: 'installDependence', + message: '安装结束', + references: depIds, + }); + isInstall && this.updateLog(depIds, '安装结束'); + isInstall && + this.dependenceDb.update( + { _id: { $in: depIds } }, + { + $set: { status: DependenceStatus.installed }, + $unset: { pid: true }, + }, + { multi: true }, + ); + }); + } +} diff --git a/back/services/sock.ts b/back/services/sock.ts index c39dd9f7..60e78929 100644 --- a/back/services/sock.ts +++ b/back/services/sock.ts @@ -1,6 +1,7 @@ import { Service, Inject } from 'typedi'; import winston from 'winston'; import { Connection } from 'sockjs'; +import { SockMessage } from '../data/sock'; @Service() export default class SockService { @@ -25,9 +26,9 @@ export default class SockService { } } - public sendMessage(msg: string) { + public sendMessage(msg: SockMessage) { this.clients.forEach((x) => { - x.write(msg); + x.write(JSON.stringify(msg)); }); } } diff --git a/back/services/user.ts b/back/services/user.ts index 2b608f60..22b55e33 100644 --- a/back/services/user.ts +++ b/back/services/user.ts @@ -433,16 +433,29 @@ export default class UserService { public async updateSystem() { const cp = spawn('ql -l update', { shell: '/bin/bash' }); + this.sockService.sendMessage({ + type: 'updateSystemVersion', + message: `开始更新系统`, + }); cp.stdout.on('data', (data) => { - this.sockService.sendMessage(data.toString()); + this.sockService.sendMessage({ + type: 'updateSystemVersion', + message: data.toString(), + }); }); cp.stderr.on('data', (data) => { - this.sockService.sendMessage(data.toString()); + this.sockService.sendMessage({ + type: 'updateSystemVersion', + message: data.toString(), + }); }); cp.on('error', (err) => { - this.sockService.sendMessage(JSON.stringify(err)); + this.sockService.sendMessage({ + type: 'updateSystemVersion', + message: JSON.stringify(err), + }); }); return { code: 200 }; diff --git a/src/layouts/defaultProps.tsx b/src/layouts/defaultProps.tsx index 20a103fe..579e738a 100644 --- a/src/layouts/defaultProps.tsx +++ b/src/layouts/defaultProps.tsx @@ -48,6 +48,12 @@ export default { icon: , component: '@/pages/script/index', }, + { + path: '/dependence', + name: '依赖管理', + icon: , + component: '@/pages/dependence/index', + }, { path: '/diff', name: '对比工具', diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index 2fa81d5b..0bbb7233 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -129,9 +129,14 @@ export default function (props: any) { ); ws.current.onmessage = (e: any) => { - if (e.data === 'hanhh') { - console.log('websocket连接成功', e); - } else { + try { + const data = JSON.parse(e.data); + if (data && data.message === 'hanhh') { + console.log('websocket连接成功', e); + } else { + console.log('websocket连接失败', e); + } + } catch (error) { console.log('websocket连接失败', e); } }; diff --git a/src/pages/crontab/logModal.tsx b/src/pages/crontab/logModal.tsx index 345bb39c..9889c36e 100644 --- a/src/pages/crontab/logModal.tsx +++ b/src/pages/crontab/logModal.tsx @@ -27,7 +27,7 @@ const CronLogModal = ({ }) => { const [value, setValue] = useState('启动中...'); const [loading, setLoading] = useState(true); - const [excuting, setExcuting] = useState(true); + const [executing, setExecuting] = useState(true); const [isPhone, setIsPhone] = useState(false); const [theme, setTheme] = useState(''); @@ -41,7 +41,7 @@ const CronLogModal = ({ if (localStorage.getItem('logCron') === cron._id) { const log = data.data as string; setValue(log || '暂无日志'); - setExcuting( + setExecuting( log && !log.includes('执行结束') && !log.includes('重启面板'), ); if (log && !log.includes('执行结束') && !log.includes('重启面板')) { @@ -89,8 +89,8 @@ const CronLogModal = ({ const titleElement = () => { return ( <> - {(excuting || loading) && } - {!excuting && } + {(executing || loading) && } + {!executing && } 日志-{cron && cron.name}{' '} ); diff --git a/src/pages/dependence/index.less b/src/pages/dependence/index.less new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/dependence/index.tsx b/src/pages/dependence/index.tsx new file mode 100644 index 00000000..92f5b299 --- /dev/null +++ b/src/pages/dependence/index.tsx @@ -0,0 +1,424 @@ +import React, { useCallback, useRef, useState, useEffect } from 'react'; +import { + Button, + message, + Modal, + Table, + Tag, + Space, + Typography, + Tooltip, + Input, + Tabs, +} from 'antd'; +import { + EditOutlined, + DeleteOutlined, + SyncOutlined, + CheckCircleOutlined, + StopOutlined, + BugOutlined, + FileTextOutlined, +} from '@ant-design/icons'; +import config from '@/utils/config'; +import { PageContainer } from '@ant-design/pro-layout'; +import { request } from '@/utils/http'; +import DependenceModal from './modal'; +import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import './index.less'; +import { getTableScroll } from '@/utils/index'; +import DependenceLogModal from './logModal'; + +const { Text } = Typography; +const { Search } = Input; + +enum Status { + '安装中', + '已安装', + '安装失败', +} + +enum StatusColor { + 'processing', + 'success', + 'error', +} + +const Dependence = ({ headerStyle, isPhone, ws }: any) => { + const columns: any = [ + { + title: '序号', + align: 'center' as const, + width: 50, + render: (text: string, record: any, index: number) => { + return {index + 1} ; + }, + }, + { + title: '名称', + dataIndex: 'name', + key: 'name', + align: 'center' as const, + }, + { + title: '状态', + key: 'status', + dataIndex: 'status', + align: 'center' as const, + render: (text: string, record: any, index: number) => { + return ( + + + {Status[record.status]} + + + ); + }, + }, + { + title: '创建时间', + key: 'created', + dataIndex: 'created', + align: 'center' as const, + render: (text: string, record: any) => { + return {new Date(record.created).toLocaleString()}; + }, + }, + { + title: '操作', + key: 'action', + align: 'center' as const, + render: (text: string, record: any, index: number) => { + const isPc = !isPhone; + return ( + + {record.status !== Status.安装中 && ( + <> + + reInstallDependence(record, index)}> + + + + + deleteDependence(record, index)}> + + + + + )} + + { + setLogDependence({ ...record, timestamp: Date.now() }); + }} + > + + + + + ); + }, + }, + ]; + const [value, setValue] = useState([]); + const [loading, setLoading] = useState(true); + const [isModalVisible, setIsModalVisible] = useState(false); + const [editedDependence, setEditedDependence] = useState(); + const [selectedRowIds, setSelectedRowIds] = useState([]); + const [searchText, setSearchText] = useState(''); + const [tableScrollHeight, setTableScrollHeight] = useState(); + const [logDependence, setLogDependence] = useState(); + const [isLogModalVisible, setIsLogModalVisible] = useState(false); + const [type, setType] = useState('nodejs'); + + const getDependencies = () => { + setLoading(true); + request + .get( + `${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`, + ) + .then((data: any) => { + setValue(data.data); + }) + .finally(() => setLoading(false)); + }; + + const addDependence = () => { + setEditedDependence(null as any); + setIsModalVisible(true); + }; + + const editDependence = (record: any, index: number) => { + setEditedDependence(record); + setIsModalVisible(true); + }; + + const deleteDependence = (record: any, index: number) => { + Modal.confirm({ + title: '确认删除', + content: ( + <> + 确认删除依赖{' '} + + {record.name} + {' '} + 吗 + + ), + onOk() { + request + .delete(`${config.apiPrefix}dependencies`, { data: [record._id] }) + .then((data: any) => { + if (data.code === 200) { + message.success('删除成功'); + const result = [...value]; + result.splice(index, 1); + setValue(result); + } else { + message.error(data); + } + }); + }, + onCancel() { + console.log('Cancel'); + }, + }); + }; + + const reInstallDependence = (record: any, index: number) => { + Modal.confirm({ + title: '确认重新安装', + content: ( + <> + 确认重新安装{' '} + + {record.name} + {' '} + 吗 + + ), + onOk() { + request + .put(`${config.apiPrefix}dependencies/reinstall`, { + data: [record._id], + }) + .then((data: any) => { + if (data.code === 200) { + handleDependence(data.data[0]); + } else { + message.error(data); + } + }); + }, + onCancel() { + console.log('Cancel'); + }, + }); + }; + + const handleCancel = (dependence?: any[]) => { + setIsModalVisible(false); + dependence && handleDependence(dependence); + }; + + const handleDependence = (dependence: any) => { + const result = [...value]; + if (Array.isArray(dependence)) { + result.push(...dependence); + } else { + const index = value.findIndex((x) => x._id === dependence._id); + result.splice(index, 1, { + ...dependence, + }); + } + setValue(result); + }; + + const onSelectChange = (selectedIds: any[]) => { + setSelectedRowIds(selectedIds); + + setTimeout(() => { + if (selectedRowIds.length === 0 || selectedIds.length === 0) { + const offset = isPhone ? 40 : 0; + setTableScrollHeight(getTableScroll({ extraHeight: 87 }) - offset); + } + }); + }; + + const rowSelection = { + selectedRowIds, + onChange: onSelectChange, + }; + + const delDependencies = () => { + Modal.confirm({ + title: '确认删除', + content: <>确认删除选中的变量吗, + onOk() { + request + .delete(`${config.apiPrefix}dependencies`, { data: selectedRowIds }) + .then((data: any) => { + if (data.code === 200) { + message.success('批量删除成功'); + setSelectedRowIds([]); + getDependencies(); + } else { + message.error(data); + } + }); + }, + onCancel() { + console.log('Cancel'); + }, + }); + }; + + const getDependenceDetail = (dependence: any) => { + request + .get(`${config.apiPrefix}dependencies/${dependence._id}`) + .then((data: any) => { + const index = value.findIndex((x) => x._id === dependence._id); + const result = [...value]; + result.splice(index, 1, { + ...dependence, + ...data.data, + }); + setValue(result); + }) + .finally(() => setLoading(false)); + }; + + const onSearch = (value: string) => { + setSearchText(value.trim()); + }; + + useEffect(() => { + getDependencies(); + }, [searchText, type]); + + useEffect(() => { + const offset = isPhone ? 40 : 0; + setTableScrollHeight(getTableScroll({ extraHeight: 87 }) - offset); + }, []); + + useEffect(() => { + if (logDependence) { + localStorage.setItem('logDependence', logDependence._id); + setIsLogModalVisible(true); + } + }, [logDependence]); + + useEffect(() => { + ws.onmessage = (e: any) => { + const { type, message, references } = JSON.parse(e.data); + if ( + type === 'installDependence' && + message === '安装结束' && + references.length > 0 + ) { + const result = [...value]; + for (let i = 0; i < references.length; i++) { + const index = value.findIndex((x) => x._id === references[i]); + result.splice(index, 1, { + ...result[index], + status: Status.已安装, + }); + } + setValue(result); + } + }; + }, [value]); + + const panelContent = () => ( + <> + {selectedRowIds.length > 0 && ( +
+ + + 已选择 + {selectedRowIds?.length}项 + +
+ )} + + + + + ); + + const onTabChange = (activeKey: string) => { + setType(activeKey); + }; + + return ( + , + , + ]} + header={{ + style: headerStyle, + }} + > + + + {panelContent()} + + + {panelContent()} + + + {panelContent()} + + + + { + setIsLogModalVisible(false); + getDependenceDetail(logDependence); + }} + ws={ws} + dependence={logDependence} + /> + + ); +}; + +export default Dependence; diff --git a/src/pages/dependence/logModal.tsx b/src/pages/dependence/logModal.tsx new file mode 100644 index 00000000..856bcda3 --- /dev/null +++ b/src/pages/dependence/logModal.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useState } from 'react'; +import { Modal, message, Input, Form, Statistic, Button } from 'antd'; +import { request } from '@/utils/http'; +import config from '@/utils/config'; +import { + Loading3QuartersOutlined, + CheckCircleOutlined, +} from '@ant-design/icons'; +import { PageLoading } from '@ant-design/pro-layout'; + +const DependenceLogModal = ({ + dependence, + handleCancel, + visible, + ws, +}: { + dependence?: any; + visible: boolean; + handleCancel: () => void; + ws: any; +}) => { + const [value, setValue] = useState(''); + const [executing, setExecuting] = useState(true); + const [isPhone, setIsPhone] = useState(false); + const [loading, setLoading] = useState(true); + + const cancel = () => { + localStorage.removeItem('logDependence'); + handleCancel(); + }; + + const titleElement = () => { + return ( + <> + {executing && } + {!executing && } + + 日志 - {dependence && dependence.name} + {' '} + + ); + }; + + const getDependenceLog = () => { + setLoading(true); + request + .get(`${config.apiPrefix}dependencies/${dependence._id}`) + .then((data: any) => { + if (localStorage.getItem('logDependence') === dependence._id) { + const log = (data.data.log || []).join('\n') as string; + setValue(log); + setExecuting(!log.includes('安装结束')); + } + }) + .finally(() => { + setLoading(false); + }); + }; + + useEffect(() => { + if (dependence) { + getDependenceLog(); + ws.onmessage = (e: any) => { + const { type, message, references } = JSON.parse(e.data); + if ( + type === 'installDependence' && + message === '安装结束' && + references.length > 0 + ) { + setExecuting(false); + } + setValue(`${value} \n ${message}`); + }; + } + }, [dependence]); + + useEffect(() => { + setIsPhone(document.body.clientWidth < 768); + }, []); + + return ( + cancel()} + onCancel={() => cancel()} + footer={[ + , + ]} + > + {loading ? ( + + ) : ( +
+          {value}
+        
+ )} +
+ ); +}; + +export default DependenceLogModal; diff --git a/src/pages/dependence/modal.tsx b/src/pages/dependence/modal.tsx new file mode 100644 index 00000000..c49a2999 --- /dev/null +++ b/src/pages/dependence/modal.tsx @@ -0,0 +1,139 @@ +import React, { useEffect, useState } from 'react'; +import { Modal, message, Input, Form, Radio, Select } from 'antd'; +import { request } from '@/utils/http'; +import config from '@/utils/config'; + +const { Option } = Select; +enum DependenceTypes { + 'nodejs', + 'python3', + 'linux', +} + +const DependenceModal = ({ + dependence, + handleCancel, + visible, + defaultType, +}: { + dependence?: any; + visible: boolean; + handleCancel: (cks?: any[]) => void; + defaultType: string; +}) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + + const handleOk = async (values: any) => { + setLoading(true); + const { name, split, type } = values; + const method = dependence ? 'put' : 'post'; + let payload; + if (!dependence) { + if (split === '1') { + const symbol = name.includes('&') ? '&' : '\n'; + payload = name.split(symbol).map((x: any) => { + return { + name: x, + type, + }; + }); + } else { + payload = [{ name, type }]; + } + } else { + payload = { ...values, _id: dependence._id }; + } + try { + const { code, data } = await request[method]( + `${config.apiPrefix}dependencies`, + { + data: payload, + }, + ); + if (code === 200) { + message.success(dependence ? '更新依赖成功' : '添加依赖成功'); + } else { + message.error(data); + } + setLoading(false); + handleCancel(data); + } catch (error) { + setLoading(false); + } + }; + + useEffect(() => { + form.resetFields(); + }, [dependence, visible]); + + return ( + { + form + .validateFields() + .then((values) => { + handleOk(values); + }) + .catch((info) => { + console.log('Validate Failed:', info); + }); + }} + onCancel={() => handleCancel()} + confirmLoading={loading} + > +
+ + + + {!dependence && ( + + + + + + + )} + + + + + + + +
+ ); +}; + +export default DependenceModal; diff --git a/src/pages/env/index.tsx b/src/pages/env/index.tsx index eb76b75b..41ad7546 100644 --- a/src/pages/env/index.tsx +++ b/src/pages/env/index.tsx @@ -368,7 +368,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => { setTimeout(() => { if (selectedRowIds.length === 0 || selectedIds.length === 0) { const offset = isPhone ? 40 : 0; - setTableScrollHeight(getTableScroll({ extraHeight: 127 }) - offset); + setTableScrollHeight(getTableScroll({ extraHeight: 87 }) - offset); } }); }; @@ -438,7 +438,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => { useEffect(() => { const offset = isPhone ? 40 : 0; - setTableScrollHeight(getTableScroll({ extraHeight: 127 }) - offset); + setTableScrollHeight(getTableScroll({ extraHeight: 87 }) - offset); }, []); return ( diff --git a/src/pages/env/modal.tsx b/src/pages/env/modal.tsx index 25a04a51..504d55da 100644 --- a/src/pages/env/modal.tsx +++ b/src/pages/env/modal.tsx @@ -85,7 +85,12 @@ const EnvModal = ({ {!env && ( - + @@ -106,7 +111,7 @@ const EnvModal = ({ /> - + diff --git a/src/utils/config.ts b/src/utils/config.ts index dde3905f..a1b1e083 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -188,4 +188,5 @@ export default { '/log': '任务日志', '/setting': '系统设置', }, + dependenceTypes: ['nodejs', 'python3', 'linux'], };