From d4930faedde997011d35027346afdd6a31c77c21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 06:50:23 +0000 Subject: [PATCH] Add labels/tags feature for environment variables Agent-Logs-Url: https://github.com/whyour/qinglong/sessions/1436272f-a03a-45af-b57d-869a48ab537d Co-authored-by: whyour <22700758+whyour@users.noreply.github.com> --- back/api/env.ts | 42 +++++++++++++++++++++ back/data/env.ts | 3 ++ back/services/env.ts | 24 ++++++++++++ src/pages/env/index.tsx | 35 +++++++++++++++++- src/pages/env/modal.tsx | 82 +++++++++++++++++++++++++++++++++++++++-- 5 files changed, 181 insertions(+), 5 deletions(-) diff --git a/back/api/env.ts b/back/api/env.ts index 834bdd51..7255a4c2 100644 --- a/back/api/env.ts +++ b/back/api/env.ts @@ -44,6 +44,7 @@ export default (app: Router) => { .required() .pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/), remarks: Joi.string().optional().allow(''), + labels: Joi.array().items(Joi.string()).optional(), }), ), }), @@ -70,6 +71,7 @@ export default (app: Router) => { name: Joi.string().required(), remarks: Joi.string().optional().allow('').allow(null), id: Joi.number().required(), + labels: Joi.array().items(Joi.string()).optional(), }), }), async (req: Request, res: Response, next: NextFunction) => { @@ -230,6 +232,46 @@ export default (app: Router) => { }, ); + route.post( + '/labels', + celebrate({ + body: Joi.object({ + ids: Joi.array().items(Joi.number().required()), + labels: Joi.array().items(Joi.string().required()), + }), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const envService = Container.get(EnvService); + const data = await envService.addLabels(req.body.ids, req.body.labels); + return res.send({ code: 200, data }); + } catch (e) { + return next(e); + } + }, + ); + + route.delete( + '/labels', + celebrate({ + body: Joi.object({ + ids: Joi.array().items(Joi.number().required()), + labels: Joi.array().items(Joi.string().required()), + }), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const envService = Container.get(EnvService); + const data = await envService.removeLabels(req.body.ids, req.body.labels); + 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 bd24f22a..ea238a95 100644 --- a/back/data/env.ts +++ b/back/data/env.ts @@ -10,6 +10,7 @@ export class Env { name?: string; remarks?: string; isPinned?: 1 | 0; + labels?: string[]; constructor(options: Env) { this.value = options.value; @@ -23,6 +24,7 @@ export class Env { this.name = options.name; this.remarks = options.remarks || ''; this.isPinned = options.isPinned || 0; + this.labels = options.labels || []; } } @@ -45,4 +47,5 @@ export const EnvModel = sequelize.define('Env', { name: { type: DataTypes.STRING, unique: 'compositeIndex' }, remarks: DataTypes.STRING, isPinned: DataTypes.NUMBER, + labels: DataTypes.JSON, }); diff --git a/back/services/env.ts b/back/services/env.ts index cd4d0a84..98fcfbea 100644 --- a/back/services/env.ts +++ b/back/services/env.ts @@ -199,6 +199,30 @@ export default class EnvService { await EnvModel.update({ isPinned: 0 }, { where: { id: ids } }); } + public async addLabels(ids: number[], labels: string[]) { + const docs = await EnvModel.findAll({ where: { id: ids } }); + for (const doc of docs) { + const env = doc.get({ plain: true }); + await EnvModel.update( + { labels: Array.from(new Set((env.labels || []).concat(labels))) }, + { where: { id: env.id } }, + ); + } + return await EnvModel.findAll({ where: { id: ids } }); + } + + public async removeLabels(ids: number[], labels: string[]) { + const docs = await EnvModel.findAll({ where: { id: ids } }); + for (const doc of docs) { + const env = doc.get({ plain: true }); + await EnvModel.update( + { labels: (env.labels || []).filter((label: string) => !labels.includes(label)) }, + { where: { id: env.id } }, + ); + } + return await EnvModel.findAll({ 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 fde76fda..4f1a9cb9 100644 --- a/src/pages/env/index.tsx +++ b/src/pages/env/index.tsx @@ -36,7 +36,7 @@ import { useVT } from 'virtualizedtableforantd4'; import Copy from '../../components/copy'; import EditNameModal from './editNameModal'; import './index.less'; -import EnvModal from './modal'; +import EnvModal, { EnvLabelModal } from './modal'; const { Paragraph } = Typography; const { Search } = Input; @@ -121,6 +121,22 @@ const Env = () => { ); }, }, + { + title: intl.get('标签'), + dataIndex: 'labels', + key: 'labels', + render: (labels: string[], record: any) => { + return ( + + {labels?.filter((l) => l).map((label) => ( + + {label} + + ))} + + ); + }, + }, { title: intl.get('更新时间'), dataIndex: 'timestamp', @@ -238,6 +254,7 @@ const Env = () => { const [loading, setLoading] = useState(true); const [isModalVisible, setIsModalVisible] = useState(false); const [isEditNameModalVisible, setIsEditNameModalVisible] = useState(false); + const [isLabelModalVisible, setIsLabelModalVisible] = useState(false); const [editedEnv, setEditedEnv] = useState(); const [selectedRowIds, setSelectedRowIds] = useState([]); const [searchText, setSearchText] = useState(''); @@ -622,6 +639,13 @@ const Env = () => { > {intl.get('批量修改变量名称')} + , + , + , + ]; + + return ( + handleCancel(false)} + confirmLoading={loading} + > +
+ + + +
+
+ ); +};