diff --git a/back/api/env.ts b/back/api/env.ts index 845b5df5..834bdd51 100644 --- a/back/api/env.ts +++ b/back/api/env.ts @@ -1,12 +1,12 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import { Container } from 'typedi'; -import EnvService from '../services/env'; -import { Logger } from 'winston'; -import { celebrate, Joi } from 'celebrate'; -import multer from 'multer'; -import config from '../config'; +import { Joi, celebrate } from 'celebrate'; +import { NextFunction, Request, Response, Router } from 'express'; import fs from 'fs'; +import multer from 'multer'; +import { Container } from 'typedi'; +import { Logger } from 'winston'; +import config from '../config'; import { safeJSONParse } from '../config/util'; +import EnvService from '../services/env'; const route = Router(); const storage = multer.diskStorage({ @@ -196,6 +196,40 @@ export default (app: Router) => { }, ); + route.put( + '/pin', + celebrate({ + body: Joi.array().items(Joi.number().required()), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const envService = Container.get(EnvService); + const data = await envService.pin(req.body); + return res.send({ code: 200, data }); + } catch (e) { + return next(e); + } + }, + ); + + route.put( + '/unpin', + celebrate({ + body: Joi.array().items(Joi.number().required()), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const envService = Container.get(EnvService); + const data = await envService.unPin(req.body); + return res.send({ code: 200, data }); + } catch (e) { + return next(e); + } + }, + ); + route.post( '/upload', upload.single('env'), diff --git a/back/data/env.ts b/back/data/env.ts index f6a9094e..d08d2ec7 100644 --- a/back/data/env.ts +++ b/back/data/env.ts @@ -1,5 +1,5 @@ +import { DataTypes, Model } from 'sequelize'; import { sequelize } from '.'; -import { DataTypes, Model, ModelDefined } from 'sequelize'; export class Env { value?: string; @@ -9,6 +9,7 @@ export class Env { position?: number; name?: string; remarks?: string; + isPinned?: 1 | 0; constructor(options: Env) { this.value = options.value; @@ -21,6 +22,7 @@ export class Env { this.position = options.position; this.name = options.name; this.remarks = options.remarks || ''; + this.isPinned = options.isPinned || 0; } } @@ -42,4 +44,5 @@ export const EnvModel = sequelize.define('Env', { position: DataTypes.NUMBER, name: { type: DataTypes.STRING, unique: 'compositeIndex' }, remarks: DataTypes.STRING, + isPinned: { type: DataTypes.NUMBER, field: 'is_pinned' }, }); diff --git a/back/loaders/db.ts b/back/loaders/db.ts index 8dd00ca8..a0af7c6d 100644 --- a/back/loaders/db.ts +++ b/back/loaders/db.ts @@ -61,6 +61,9 @@ export default async () => { 'alter table Crontabs add column log_name VARCHAR(255)', ); } catch (error) {} + try { + await sequelize.query('alter table Envs add column is_pinned NUMBER'); + } catch (error) {} Logger.info('✌️ DB loaded'); } catch (error) { diff --git a/back/services/env.ts b/back/services/env.ts index be89cc86..0759710e 100644 --- a/back/services/env.ts +++ b/back/services/env.ts @@ -1,7 +1,8 @@ -import { Service, Inject } from 'typedi'; +import groupBy from 'lodash/groupBy'; +import { FindOptions, Op } from 'sequelize'; +import { Inject, Service } from 'typedi'; import winston from 'winston'; import config from '../config'; -import * as fs from 'fs/promises'; import { Env, EnvModel, @@ -11,8 +12,6 @@ import { minPosition, stepPosition, } from '../data/env'; -import groupBy from 'lodash/groupBy'; -import { FindOptions, Op } from 'sequelize'; import { writeFileWithLock } from '../shared/utils'; @Service() @@ -147,6 +146,7 @@ export default class EnvService { } try { const result = await this.find(condition, [ + ['isPinned', 'DESC'], ['position', 'DESC'], ['createdAt', 'ASC'], ]); @@ -190,6 +190,14 @@ export default class EnvService { await this.set_envs(); } + public async pin(ids: number[]) { + await EnvModel.update({ isPinned: 1 }, { where: { id: ids } }); + } + + public async unPin(ids: number[]) { + await EnvModel.update({ isPinned: 0 }, { where: { id: ids } }); + } + public async set_envs() { const envs = await this.envs('', { name: { [Op.not]: null }, diff --git a/src/pages/env/index.tsx b/src/pages/env/index.tsx index 3600e0a8..fde76fda 100644 --- a/src/pages/env/index.tsx +++ b/src/pages/env/index.tsx @@ -1,47 +1,42 @@ -import intl from 'react-intl-universal'; -import React, { - useCallback, - useRef, - useState, - useEffect, - useMemo, -} from 'react'; +import useTableScrollHeight from '@/hooks/useTableScrollHeight'; +import { SharedContext } from '@/layouts'; +import config from '@/utils/config'; +import { request } from '@/utils/http'; +import { exportJson } from '@/utils/index'; import { - Button, - message, - Modal, - Table, - Tag, - Space, - Typography, - Tooltip, - Input, - UploadProps, - Upload, -} from 'antd'; -import { - EditOutlined, - DeleteOutlined, - SyncOutlined, CheckCircleOutlined, + DeleteOutlined, + EditOutlined, + PushpinFilled, + PushpinOutlined, StopOutlined, UploadOutlined, } from '@ant-design/icons'; -import config from '@/utils/config'; import { PageContainer } from '@ant-design/pro-layout'; -import { request } from '@/utils/http'; -import EnvModal from './modal'; -import EditNameModal from './editNameModal'; +import { useOutletContext } from '@umijs/max'; +import { + Button, + Input, + Modal, + Space, + Table, + Tag, + Tooltip, + Typography, + Upload, + UploadProps, + message, +} from 'antd'; +import dayjs from 'dayjs'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import './index.less'; -import { exportJson } from '@/utils/index'; -import { useOutletContext } from '@umijs/max'; -import { SharedContext } from '@/layouts'; -import useTableScrollHeight from '@/hooks/useTableScrollHeight'; -import Copy from '../../components/copy'; +import intl from 'react-intl-universal'; import { useVT } from 'virtualizedtableforantd4'; -import dayjs from 'dayjs'; +import Copy from '../../components/copy'; +import EditNameModal from './editNameModal'; +import './index.less'; +import EnvModal from './modal'; const { Paragraph } = Typography; const { Search } = Input; @@ -59,11 +54,15 @@ enum StatusColor { enum OperationName { '启用', '禁用', + '置顶', + '取消置顶', } enum OperationPath { 'enable', 'disable', + 'pin', + 'unpin', } const type = 'DragableBodyRow'; @@ -181,7 +180,7 @@ const Env = () => { { title: intl.get('操作'), key: 'action', - width: 120, + width: 160, render: (text: string, record: any, index: number) => { const isPc = !isPhone; return ( @@ -208,6 +207,23 @@ const Env = () => { )} + + pinOrUnpinEnv(record, index)}> + {record.isPinned === 1 ? ( + + ) : ( + + )} + + deleteEnv(record, index)}> @@ -305,6 +321,51 @@ const Env = () => { setIsModalVisible(true); }; + const pinOrUnpinEnv = (record: any, index: number) => { + Modal.confirm({ + title: `确认${ + record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶') + }`, + content: ( + <> + {intl.get('确认')} + {record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')} + Env{' '} + + {record.name}: {record.value} + {' '} + {intl.get('吗')} + + ), + onOk() { + request + .put( + `${config.apiPrefix}envs/${ + record.isPinned === 1 ? 'unpin' : 'pin' + }`, + [record.id], + ) + .then(({ code, data }) => { + if (code === 200) { + message.success( + `${ + record.isPinned === 1 + ? intl.get('取消置顶') + : intl.get('置顶') + }${intl.get('成功')}`, + ); + getEnvs(); + } + }); + }, + }); + }; + const deleteEnv = (record: any, index: number) => { Modal.confirm({ title: intl.get('确认删除'), @@ -589,6 +650,20 @@ const Env = () => { > {intl.get('批量禁用')} + + {intl.get('已选择')} {selectedRowIds?.length}