Skip to content

Commit 4cb9f57

Browse files
Copilotwhyour
andauthored
环境变量支持置顶 (#2822)
* Initial plan * Add pin to top feature for environment variables Co-authored-by: whyour <[email protected]> * Format code with prettier Co-authored-by: whyour <[email protected]> * Add database migration for isPinned column in Envs table Co-authored-by: whyour <[email protected]> * Use snake_case naming (is_pinned) for database column Co-authored-by: whyour <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: whyour <[email protected]> Co-authored-by: whyour <[email protected]>
1 parent c369514 commit 4cb9f57

File tree

5 files changed

+167
-44
lines changed

5 files changed

+167
-44
lines changed

back/api/env.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { Router, Request, Response, NextFunction } from 'express';
1+
import { Joi, celebrate } from 'celebrate';
2+
import { NextFunction, Request, Response, Router } from 'express';
3+
import fs from 'fs';
4+
import multer from 'multer';
25
import { Container } from 'typedi';
3-
import EnvService from '../services/env';
46
import { Logger } from 'winston';
5-
import { celebrate, Joi } from 'celebrate';
6-
import multer from 'multer';
77
import config from '../config';
8-
import fs from 'fs';
98
import { safeJSONParse } from '../config/util';
9+
import EnvService from '../services/env';
1010
const route = Router();
1111

1212
const storage = multer.diskStorage({
@@ -196,6 +196,40 @@ export default (app: Router) => {
196196
},
197197
);
198198

199+
route.put(
200+
'/pin',
201+
celebrate({
202+
body: Joi.array().items(Joi.number().required()),
203+
}),
204+
async (req: Request, res: Response, next: NextFunction) => {
205+
const logger: Logger = Container.get('logger');
206+
try {
207+
const envService = Container.get(EnvService);
208+
const data = await envService.pin(req.body);
209+
return res.send({ code: 200, data });
210+
} catch (e) {
211+
return next(e);
212+
}
213+
},
214+
);
215+
216+
route.put(
217+
'/unpin',
218+
celebrate({
219+
body: Joi.array().items(Joi.number().required()),
220+
}),
221+
async (req: Request, res: Response, next: NextFunction) => {
222+
const logger: Logger = Container.get('logger');
223+
try {
224+
const envService = Container.get(EnvService);
225+
const data = await envService.unPin(req.body);
226+
return res.send({ code: 200, data });
227+
} catch (e) {
228+
return next(e);
229+
}
230+
},
231+
);
232+
199233
route.post(
200234
'/upload',
201235
upload.single('env'),

back/data/env.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { DataTypes, Model } from 'sequelize';
12
import { sequelize } from '.';
2-
import { DataTypes, Model, ModelDefined } from 'sequelize';
33

44
export class Env {
55
value?: string;
@@ -9,6 +9,7 @@ export class Env {
99
position?: number;
1010
name?: string;
1111
remarks?: string;
12+
isPinned?: 1 | 0;
1213

1314
constructor(options: Env) {
1415
this.value = options.value;
@@ -21,6 +22,7 @@ export class Env {
2122
this.position = options.position;
2223
this.name = options.name;
2324
this.remarks = options.remarks || '';
25+
this.isPinned = options.isPinned || 0;
2426
}
2527
}
2628

@@ -42,4 +44,5 @@ export const EnvModel = sequelize.define<EnvInstance>('Env', {
4244
position: DataTypes.NUMBER,
4345
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
4446
remarks: DataTypes.STRING,
47+
isPinned: { type: DataTypes.NUMBER, field: 'is_pinned' },
4548
});

back/loaders/db.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ export default async () => {
6161
'alter table Crontabs add column log_name VARCHAR(255)',
6262
);
6363
} catch (error) {}
64+
try {
65+
await sequelize.query('alter table Envs add column is_pinned NUMBER');
66+
} catch (error) {}
6467

6568
Logger.info('✌️ DB loaded');
6669
} catch (error) {

back/services/env.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Service, Inject } from 'typedi';
1+
import groupBy from 'lodash/groupBy';
2+
import { FindOptions, Op } from 'sequelize';
3+
import { Inject, Service } from 'typedi';
24
import winston from 'winston';
35
import config from '../config';
4-
import * as fs from 'fs/promises';
56
import {
67
Env,
78
EnvModel,
@@ -11,8 +12,6 @@ import {
1112
minPosition,
1213
stepPosition,
1314
} from '../data/env';
14-
import groupBy from 'lodash/groupBy';
15-
import { FindOptions, Op } from 'sequelize';
1615
import { writeFileWithLock } from '../shared/utils';
1716

1817
@Service()
@@ -147,6 +146,7 @@ export default class EnvService {
147146
}
148147
try {
149148
const result = await this.find(condition, [
149+
['isPinned', 'DESC'],
150150
['position', 'DESC'],
151151
['createdAt', 'ASC'],
152152
]);
@@ -190,6 +190,14 @@ export default class EnvService {
190190
await this.set_envs();
191191
}
192192

193+
public async pin(ids: number[]) {
194+
await EnvModel.update({ isPinned: 1 }, { where: { id: ids } });
195+
}
196+
197+
public async unPin(ids: number[]) {
198+
await EnvModel.update({ isPinned: 0 }, { where: { id: ids } });
199+
}
200+
193201
public async set_envs() {
194202
const envs = await this.envs('', {
195203
name: { [Op.not]: null },

src/pages/env/index.tsx

Lines changed: 109 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,42 @@
1-
import intl from 'react-intl-universal';
2-
import React, {
3-
useCallback,
4-
useRef,
5-
useState,
6-
useEffect,
7-
useMemo,
8-
} from 'react';
1+
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
2+
import { SharedContext } from '@/layouts';
3+
import config from '@/utils/config';
4+
import { request } from '@/utils/http';
5+
import { exportJson } from '@/utils/index';
6+
import {
7+
CheckCircleOutlined,
8+
DeleteOutlined,
9+
EditOutlined,
10+
PushpinFilled,
11+
PushpinOutlined,
12+
StopOutlined,
13+
UploadOutlined,
14+
} from '@ant-design/icons';
15+
import { PageContainer } from '@ant-design/pro-layout';
16+
import { useOutletContext } from '@umijs/max';
917
import {
1018
Button,
11-
message,
19+
Input,
1220
Modal,
21+
Space,
1322
Table,
1423
Tag,
15-
Space,
16-
Typography,
1724
Tooltip,
18-
Input,
19-
UploadProps,
25+
Typography,
2026
Upload,
27+
UploadProps,
28+
message,
2129
} from 'antd';
22-
import {
23-
EditOutlined,
24-
DeleteOutlined,
25-
SyncOutlined,
26-
CheckCircleOutlined,
27-
StopOutlined,
28-
UploadOutlined,
29-
} from '@ant-design/icons';
30-
import config from '@/utils/config';
31-
import { PageContainer } from '@ant-design/pro-layout';
32-
import { request } from '@/utils/http';
33-
import EnvModal from './modal';
34-
import EditNameModal from './editNameModal';
30+
import dayjs from 'dayjs';
31+
import React, { useCallback, useEffect, useRef, useState } from 'react';
3532
import { DndProvider, useDrag, useDrop } from 'react-dnd';
3633
import { HTML5Backend } from 'react-dnd-html5-backend';
37-
import './index.less';
38-
import { exportJson } from '@/utils/index';
39-
import { useOutletContext } from '@umijs/max';
40-
import { SharedContext } from '@/layouts';
41-
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
42-
import Copy from '../../components/copy';
34+
import intl from 'react-intl-universal';
4335
import { useVT } from 'virtualizedtableforantd4';
44-
import dayjs from 'dayjs';
36+
import Copy from '../../components/copy';
37+
import EditNameModal from './editNameModal';
38+
import './index.less';
39+
import EnvModal from './modal';
4540

4641
const { Paragraph } = Typography;
4742
const { Search } = Input;
@@ -59,11 +54,15 @@ enum StatusColor {
5954
enum OperationName {
6055
'启用',
6156
'禁用',
57+
'置顶',
58+
'取消置顶',
6259
}
6360

6461
enum OperationPath {
6562
'enable',
6663
'disable',
64+
'pin',
65+
'unpin',
6766
}
6867

6968
const type = 'DragableBodyRow';
@@ -181,7 +180,7 @@ const Env = () => {
181180
{
182181
title: intl.get('操作'),
183182
key: 'action',
184-
width: 120,
183+
width: 160,
185184
render: (text: string, record: any, index: number) => {
186185
const isPc = !isPhone;
187186
return (
@@ -208,6 +207,23 @@ const Env = () => {
208207
)}
209208
</a>
210209
</Tooltip>
210+
<Tooltip
211+
title={
212+
isPc
213+
? record.isPinned === 1
214+
? intl.get('取消置顶')
215+
: intl.get('置顶')
216+
: ''
217+
}
218+
>
219+
<a onClick={() => pinOrUnpinEnv(record, index)}>
220+
{record.isPinned === 1 ? (
221+
<PushpinFilled />
222+
) : (
223+
<PushpinOutlined />
224+
)}
225+
</a>
226+
</Tooltip>
211227
<Tooltip title={isPc ? intl.get('删除') : ''}>
212228
<a onClick={() => deleteEnv(record, index)}>
213229
<DeleteOutlined />
@@ -305,6 +321,51 @@ const Env = () => {
305321
setIsModalVisible(true);
306322
};
307323

324+
const pinOrUnpinEnv = (record: any, index: number) => {
325+
Modal.confirm({
326+
title: `确认${
327+
record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
328+
}`,
329+
content: (
330+
<>
331+
{intl.get('确认')}
332+
{record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')}
333+
Env{' '}
334+
<Paragraph
335+
style={{ wordBreak: 'break-all', display: 'inline' }}
336+
ellipsis={{ rows: 6, expandable: true }}
337+
type="warning"
338+
copyable
339+
>
340+
{record.name}: {record.value}
341+
</Paragraph>{' '}
342+
{intl.get('吗')}
343+
</>
344+
),
345+
onOk() {
346+
request
347+
.put(
348+
`${config.apiPrefix}envs/${
349+
record.isPinned === 1 ? 'unpin' : 'pin'
350+
}`,
351+
[record.id],
352+
)
353+
.then(({ code, data }) => {
354+
if (code === 200) {
355+
message.success(
356+
`${
357+
record.isPinned === 1
358+
? intl.get('取消置顶')
359+
: intl.get('置顶')
360+
}${intl.get('成功')}`,
361+
);
362+
getEnvs();
363+
}
364+
});
365+
},
366+
});
367+
};
368+
308369
const deleteEnv = (record: any, index: number) => {
309370
Modal.confirm({
310371
title: intl.get('确认删除'),
@@ -589,6 +650,20 @@ const Env = () => {
589650
>
590651
{intl.get('批量禁用')}
591652
</Button>
653+
<Button
654+
type="primary"
655+
onClick={() => operateEnvs(2)}
656+
style={{ marginLeft: 8, marginBottom: 5 }}
657+
>
658+
{intl.get('批量置顶')}
659+
</Button>
660+
<Button
661+
type="primary"
662+
onClick={() => operateEnvs(3)}
663+
style={{ marginLeft: 8, marginRight: 8 }}
664+
>
665+
{intl.get('批量取消置顶')}
666+
</Button>
592667
<span style={{ marginLeft: 8 }}>
593668
{intl.get('已选择')}
594669
<a>{selectedRowIds?.length}</a>

0 commit comments

Comments
 (0)