feat: add environment variable labels

This commit is contained in:
whyour 2026-05-23 23:21:38 +08:00
parent 7a8917f8e4
commit 8bc0906949
10 changed files with 792 additions and 9 deletions

View File

@ -18,6 +18,10 @@ const storage = multer.diskStorage({
},
});
const upload = multer({ storage: storage });
const labelSchema = Joi.array()
.items(Joi.string().trim().required())
.min(1)
.required();
export default (app: Router) => {
app.use('/envs', route);
@ -44,6 +48,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().trim()).optional(),
}),
),
}),
@ -70,6 +75,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().trim()).optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@ -230,6 +236,44 @@ export default (app: Router) => {
},
);
route.post(
'/labels',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.number().required()).min(1).required(),
labels: labelSchema,
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
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()).min(1).required(),
labels: labelSchema,
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
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'),
@ -248,6 +292,7 @@ export default (app: Router) => {
name: x.name,
value: x.value,
remarks: x.remarks,
labels: x.labels,
})),
);
return res.send({ code: 200, data: result });

View File

@ -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<EnvInstance>('Env', {
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
remarks: DataTypes.STRING,
isPinned: DataTypes.NUMBER,
labels: DataTypes.JSON,
});

View File

@ -40,6 +40,7 @@ export default async () => {
type: 'NUMBER',
},
{ table: 'Envs', column: 'isPinned', type: 'NUMBER' },
{ table: 'Envs', column: 'labels', type: 'JSON' },
];
for (const migration of migrations) {

View File

@ -199,6 +199,44 @@ export default class EnvService {
await EnvModel.update({ isPinned: 0 }, { where: { id: ids } });
}
public async addLabels(ids: number[], labels: string[]) {
await sequelize.transaction(async (transaction) => {
const docs = await EnvModel.findAll({
where: { id: ids },
transaction,
});
for (const doc of docs) {
const env = doc.get({ plain: true });
await EnvModel.update(
{ labels: Array.from(new Set([...(env.labels || []), ...labels])) },
{ where: { id: env.id }, transaction },
);
}
});
return await this.find({ id: ids });
}
public async removeLabels(ids: number[], labels: string[]) {
await sequelize.transaction(async (transaction) => {
const docs = await EnvModel.findAll({
where: { id: ids },
transaction,
});
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 }, transaction },
);
}
});
return await this.find({ id: ids });
}
public async set_envs() {
const envs = await this.envs('', {
name: { [Op.not]: null },

View File

@ -0,0 +1,550 @@
# Project Architecture Guide
This document is written for AI coding agents and maintainers who need to understand and modify this project safely. It focuses on where behavior lives, how the application starts, and which files are usually involved for common changes.
## Project Summary
Qinglong is a timed task management platform. It provides a web admin panel for managing cron jobs, scripts, environment variables, subscriptions, dependencies, logs, configuration files, and system settings.
The repository is organized as a full-stack TypeScript application:
- `src/`: frontend admin panel, built with Umi Max, React, Ant Design, and Ant Design Pro Layout.
- `back/`: backend application, built with Express, TypeScript, typedi, Sequelize, SQLite, gRPC, and worker processes.
- `shell/`: runtime shell scripts used to execute tasks and preload task environments.
- `data/`: local runtime data, including scripts, logs, configs, SQLite database, uploaded files, and cloned repositories.
- `static/`: built frontend and backend artifacts.
- `docker/`: Docker images, compose file, and entrypoint.
- `sample/`: sample scripts and default config templates.
## High-Level Runtime Flow
```text
Browser
-> src/pages/*
-> src/utils/http.tsx
-> /api/*
-> back/api/*
-> back/services/*
-> back/data/* Sequelize models
-> data/db/database.sqlite
Cron/task execution
-> back/services/cron.ts
-> shell/task.sh or shell/otask.sh
-> data/scripts/*
-> data/log/*
Frontend assets in production
-> static/dist/*
-> served by back/loaders/express.ts
```
## Main Startup Path
Development starts from `package.json`:
```bash
pnpm start
```
This runs:
- `start:back`: `nodemon ./back/app.ts`
- `start:front`: `max dev`
Backend startup begins in `back/app.ts`.
Important details:
- The backend uses Node `cluster`.
- The primary process initializes the database first.
- A gRPC worker starts before the HTTP worker.
- The HTTP worker starts Express and serves API routes plus frontend static files.
- If the gRPC worker restarts, the HTTP worker is asked to re-register cron jobs.
Production-style backend output is generated by:
```bash
pnpm run build:back
```
The compiled backend is placed under `static/build`.
Frontend output is generated by:
```bash
pnpm run build:front
```
The compiled frontend is placed under `static/dist`.
## Backend Architecture
### Entry Point
- `back/app.ts`
Responsibilities:
- Creates the Express application.
- Starts primary/worker process logic.
- Initializes database in the primary process.
- Starts gRPC and HTTP workers.
- Handles graceful shutdown.
- Re-registers cron jobs after gRPC worker recovery.
### Loaders
- `back/loaders/app.ts`
- `back/loaders/express.ts`
- `back/loaders/db.ts`
- `back/loaders/depInjector.ts`
- `back/loaders/initData.ts`
- `back/loaders/initFile.ts`
- `back/loaders/initTask.ts`
- `back/loaders/server.ts`
- `back/loaders/sock.ts`
Loader responsibilities:
- Register dependency injection bindings.
- Sync Sequelize models.
- Initialize files and default data.
- Initialize scheduled tasks.
- Configure Express middleware.
- Register routes.
- Attach socket/server behavior.
`back/loaders/express.ts` is the main HTTP middleware and routing setup. It handles:
- CORS.
- Helmet.
- body parser.
- static frontend serving.
- JWT validation.
- token validation against shared auth state.
- `/open/*` rewrite to `/api/*`.
- route mounting through `back/api/index.ts`.
- frontend fallback to `static/dist/index.html`.
- API error handling.
### API Routes
- `back/api/index.ts`
This file registers all API modules:
- `user.ts`: login, initialization, authentication-related user endpoints.
- `env.ts`: environment variable endpoints.
- `config.ts`: config file endpoints.
- `log.ts`: log endpoints.
- `cron.ts`: cron/task endpoints.
- `script.ts`: script file endpoints.
- `open.ts`: open API/app token endpoints.
- `dependence.ts`: dependency management endpoints.
- `system.ts`: system information/settings endpoints.
- `subscription.ts`: subscription endpoints.
- `update.ts`: update/check endpoints.
- `health.ts`: health check endpoints.
Route files should stay thin. They should validate input, get a service from `typedi`'s `Container`, call the service, and return `{ code, data, message }` style responses.
### Services
- `back/services/*`
Services contain most business logic. Common examples:
- `cron.ts`: create/update/delete/run cron jobs, generate crontab data, manage logs, call scheduler client.
- `env.ts`: manage environment variables.
- `config.ts`: read/write config files.
- `script.ts`: manage script files.
- `subscription.ts`: manage script subscriptions and repository pulls.
- `dependence.ts`: install/manage runtime dependencies.
- `system.ts`: system info and settings.
- `notify.ts`: notification behavior.
- `sock.ts`: socket/log stream behavior.
- `grpc.ts`: gRPC server lifecycle.
- `http.ts`: HTTP server lifecycle.
When changing backend behavior, first find the API route, then follow it into the matching service. In most cases, the service is the right place for behavioral changes.
### Data Models
- `back/data/index.ts`
- `back/data/*.ts`
The backend uses Sequelize with SQLite. Database storage is configured in `back/data/index.ts`:
```text
data/db/database.sqlite
```
Common model files:
- `cron.ts`: cron job model.
- `cronView.ts`: saved cron table views.
- `env.ts`: environment variable model.
- `dependence.ts`: dependency model.
- `open.ts`: open API app/token model.
- `subscription.ts`: subscription model.
- `system.ts`: system settings model.
- `notify.ts`: notification-related data.
Model sync and simple column migrations are currently handled in `back/loaders/db.ts`.
### Configuration
- `back/config/index.ts`
This is the central runtime config file. It reads `.env`, establishes `QL_DIR`, and defines important paths:
- `dataPath`: runtime data root.
- `configPath`: config files.
- `scriptPath`: user scripts.
- `repoPath`: subscription repositories.
- `logPath`: task logs.
- `dbPath`: SQLite database location.
- `uploadPath`: uploaded files.
- `shellPath`: shell runtime scripts.
- `preloadPath`: JS/Python/Shell preload files.
Before hardcoding paths, check `back/config/index.ts`.
### Scheduling And gRPC
- `back/schedule/*`
- `back/protos/*`
- `back/services/grpc.ts`
The project has two scheduling paths:
- Standard crontab-style tasks are persisted and written through backend cron logic.
- Node/gRPC scheduler logic handles cases such as second-level cron expressions or additional schedules.
`back/services/cron.ts` decides whether a task needs the Node scheduler using schedule shape and `extra_schedules`.
### Shared Backend Utilities
- `back/shared/*`
- `back/config/util.ts`
- `back/config/share.ts`
- `back/config/http.ts`
Use these before adding new global helpers. Existing shared code includes:
- auth helpers.
- shared store.
- log stream manager.
- task runner helpers.
- concurrency limits.
- file locking utilities.
- HTTP/proxy helpers.
## Frontend Architecture
### Umi Config
- `.umirc.ts`
Important behavior:
- Dev server proxies API requests to `http://127.0.0.1:5700/`.
- Frontend build output is `static/dist`.
- Runtime env script is loaded from `./api/env.js`.
- `QlBaseUrl` affects frontend public path and routing base.
### App Initialization
- `src/app.ts`
Responsibilities:
- Load Chinese and English locale JSON.
- Determine locale from URL/cookie/localStorage.
- Set Umi locale.
- Apply `QlBaseUrl` as public path and router basename.
### Layout And Routes
- `src/layouts/defaultProps.tsx`
- `src/layouts/index.tsx`
`defaultProps.tsx` defines the main route/menu list. If adding a new page visible in the sidebar, update this file.
Current major pages:
- `src/pages/crontab`: timed task management.
- `src/pages/subscription`: subscription management.
- `src/pages/env`: environment variables.
- `src/pages/config`: config files.
- `src/pages/script`: script management.
- `src/pages/dependence`: dependency management.
- `src/pages/log`: log management.
- `src/pages/diff`: diff tool.
- `src/pages/setting`: system settings.
- `src/pages/login`: login.
- `src/pages/initialization`: first-run initialization.
- `src/pages/error`: error page.
### Frontend Utilities
- `src/utils/http.tsx`: API request helper.
- `src/utils/websocket.ts`: socket connection behavior.
- `src/utils/config.ts`: frontend config helpers.
- `src/utils/const.ts`: constants.
- `src/utils/date.ts`: date formatting helpers.
- `src/utils/init.ts`: initialization helpers.
- `src/utils/codemirror/*`: CodeMirror integration.
- `src/utils/monaco/*`: Monaco integration.
When changing a page's API behavior, inspect both the page file and `src/utils/http.tsx`.
### Components And Styling
- `src/components/*`: reusable UI components.
- `src/pages/**/index.less`: page-level styles.
- `src/pages/script/index.module.less` and `src/pages/log/index.module.less`: CSS module styles.
- `src/assets/fonts/*`: bundled fonts.
- `src/locales/*.json`: i18n text.
Follow the existing Ant Design and Ant Design Pro patterns when modifying UI.
## Shell Runtime
- `shell/task.sh`: task execution path.
- `shell/otask.sh`: alternate/manual task execution path.
- `shell/api.sh`: shell-side API helpers.
- `shell/env.sh`: environment setup.
- `shell/check.sh`: runtime check helpers.
- `shell/update.sh`: update helpers.
- `shell/rmlog.sh`: log cleanup.
- `shell/share.sh`: shared shell helpers.
- `shell/preload/*`: preload files injected into JS/Python/Shell task environments.
The backend often coordinates task execution, but the actual user script process environment is shaped by files in `shell/`.
## Runtime Data Directory
- `data/`
This directory is runtime state, not just source code. Be careful when modifying or deleting files here.
Important subdirectories:
- `data/db`: SQLite database.
- `data/config`: generated and user-edited config files.
- `data/scripts`: user scripts.
- `data/repo`: cloned subscription repositories.
- `data/log`: task logs.
- `data/upload`: uploaded files.
- `data/syslog`: system logs.
- `data/ssh.d`: SSH-related runtime data.
- `data/dep_cache`: dependency cache, when present.
Many bugs that appear as "backend logic" may involve state stored under `data/`.
## Docker And Release Files
- `docker/Dockerfile`
- `docker/310.Dockerfile`
- `docker/docker-compose.yml`
- `docker/docker-entrypoint.sh`
- `ecosystem.config.js`
- `version.yaml`
Use these when changing deployment, container startup, PM2 behavior, or release metadata.
## Common Modification Map
### Add Or Modify A Backend API
Typical files:
1. Add or update route in `back/api/<module>.ts`.
2. Add or update service logic in `back/services/<module>.ts`.
3. Add or update model in `back/data/<module>.ts` if persistence changes.
4. Add validation with `celebrate`/`Joi` near the route.
5. Update frontend caller in `src/pages/**` or `src/utils/**`.
### Add A New Frontend Page
Typical files:
1. Create `src/pages/<page>/index.tsx`.
2. Add styles in `src/pages/<page>/index.less` if needed.
3. Register route/menu in `src/layouts/defaultProps.tsx`.
4. Add locale strings in `src/locales/zh-CN.json` and `src/locales/en-US.json`.
5. Add API calls through the existing request helper.
### Change Cron/Task Behavior
Start with:
- `back/api/cron.ts`
- `back/services/cron.ts`
- `back/schedule/*`
- `shell/task.sh`
- `shell/otask.sh`
- `shell/preload/*`
Also inspect:
- `back/data/cron.ts`
- `back/validation/schedule.ts`
- `data/config/crontab.list`
- `data/log/*`
### Change Environment Variable Behavior
Start with:
- `back/api/env.ts`
- `back/services/env.ts`
- `back/data/env.ts`
- `src/pages/env/index.tsx`
Also inspect:
- `shell/preload/env.sh`
- `shell/preload/env.js`
- `shell/preload/env.py`
### Change Script Management
Start with:
- `back/api/script.ts`
- `back/services/script.ts`
- `src/pages/script/index.tsx`
- `data/scripts/*`
### Change Login/Auth/Security
Start with:
- `back/api/user.ts`
- `back/services/user.ts`
- `back/shared/auth.ts`
- `back/shared/store.ts`
- `back/loaders/express.ts`
- `back/token.ts`
- `src/pages/login/index.tsx`
- `src/pages/initialization/index.tsx`
Be careful with:
- JWT behavior.
- open API token behavior.
- first-run initialization.
- platform-specific session limits.
### Change Subscription Behavior
Start with:
- `back/api/subscription.ts`
- `back/services/subscription.ts`
- `back/data/subscription.ts`
- `src/pages/subscription/index.tsx`
- `data/repo/*`
### Change Dependency Management
Start with:
- `back/api/dependence.ts`
- `back/services/dependence.ts`
- `back/data/dependence.ts`
- `src/pages/dependence/index.tsx`
- `data/deps`
- `data/dep_cache`
### Change Logs Or Live Log Streaming
Start with:
- `back/api/log.ts`
- `back/services/log.ts`
- `back/services/sock.ts`
- `back/shared/logStreamManager.ts`
- `src/pages/log/index.tsx`
- `src/components/terminal.tsx`
- `data/log/*`
## Coding Conventions
Backend:
- Prefer adding business logic to services, not route files.
- Use `typedi` services consistently.
- Use existing config paths from `back/config/index.ts`.
- Return API responses in the existing `{ code, data, message }` shape.
- Use existing utilities before adding new helpers.
- Preserve current SQLite/Sequelize style unless doing a larger data-layer refactor.
Frontend:
- Follow existing Umi/React/Ant Design patterns.
- Keep route/menu changes in `src/layouts/defaultProps.tsx`.
- Use existing request/WebSocket helpers.
- Add or update locale strings for visible UI text.
- Keep page-specific styles near the page.
Shell/runtime:
- Treat `shell/` as part of production behavior.
- Test task execution changes with realistic scripts when possible.
- Be careful with path quoting and environment variable propagation.
Data:
- Treat `data/` as mutable runtime state.
- Do not delete runtime state unless explicitly requested.
- Schema changes should account for existing SQLite databases.
## Suggested First Steps For AI Agents
When asked to modify behavior:
1. Identify whether the change is frontend, backend, shell runtime, data model, or deployment.
2. Search by feature name in `src/pages`, `back/api`, and `back/services`.
3. Read the route file and matching service before editing.
4. If persistence is involved, read the matching `back/data` model and `back/loaders/db.ts`.
5. If task execution is involved, inspect `shell/` and `back/services/cron.ts`.
6. Make the smallest scoped change that matches existing patterns.
7. Run the most relevant check:
- `pnpm run build:back` for backend TypeScript changes.
- `pnpm run build:front` for frontend build changes.
- targeted manual task/API checks for shell and scheduler changes.
## Quick Directory Reference
```text
.
├── back/ Backend TypeScript application
│ ├── api/ Express route modules
│ ├── config/ Runtime config, paths, constants, helpers
│ ├── data/ Sequelize models and SQLite connection
│ ├── loaders/ Startup initialization and Express setup
│ ├── middlewares/ Express middlewares
│ ├── protos/ gRPC proto files and generated TS
│ ├── schedule/ Scheduler/gRPC client helpers
│ ├── services/ Business logic services
│ ├── shared/ Shared backend utilities
│ └── validation/ Joi validation schemas
├── src/ Frontend Umi/React application
│ ├── assets/ Fonts and static frontend assets
│ ├── components/ Shared UI components
│ ├── hooks/ Frontend hooks
│ ├── layouts/ Main layout and menu route config
│ ├── locales/ i18n JSON
│ ├── pages/ Feature pages
│ └── utils/ HTTP, WebSocket, editor, date, and config utilities
├── shell/ Task runtime shell scripts and preload files
├── data/ Runtime state: db, logs, scripts, repos, configs
├── docker/ Docker build and compose files
├── sample/ Sample scripts and default config templates
├── static/ Built frontend/backend artifacts
└── docs/ Project documentation
```

View File

@ -66,9 +66,7 @@ const EditableTagGroup = ({
}, [inputVisible]);
useEffect(() => {
if (value) {
setTags(value);
}
setTags(value || []);
}, [value]);
return (

View File

@ -437,6 +437,9 @@
"Cron表达式格式有误": "Incorrect Cron Expression Format",
"添加Labels成功": "Labels added successfully",
"删除Labels成功": "Labels deleted successfully",
"添加标签成功": "Tags added successfully",
"删除标签成功": "Tags deleted successfully",
"请至少输入一个标签": "Please enter at least one tag",
"编辑视图": "Edit View",
"排序方式": "Sort Order",
"开始时间": "Start Time",

View File

@ -437,6 +437,9 @@
"Cron表达式格式有误": "Cron表达式格式有误",
"添加Labels成功": "添加Labels成功",
"删除Labels成功": "删除Labels成功",
"添加标签成功": "添加标签成功",
"删除标签成功": "删除标签成功",
"请至少输入一个标签": "请至少输入一个标签",
"编辑视图": "编辑视图",
"排序方式": "排序方式",
"开始时间": "开始时间",

View File

@ -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,25 @@ const Env = () => {
);
},
},
{
title: intl.get('标签'),
dataIndex: 'labels',
key: 'labels',
render: (labels: string[] | null) => {
const envLabels = Array.isArray(labels) ? labels : [];
return (
<Space size={[0, 4]} wrap>
{envLabels
.filter((label) => label)
.map((label) => (
<Tag key={label} color="blue">
{label}
</Tag>
))}
</Space>
);
},
},
{
title: intl.get('更新时间'),
dataIndex: 'timestamp',
@ -238,6 +257,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<string[]>([]);
const [searchText, setSearchText] = useState('');
@ -408,6 +428,13 @@ const Env = () => {
getEnvs();
};
const handleLabelCancel = (needUpdate?: boolean) => {
setIsLabelModalVisible(false);
if (needUpdate) {
getEnvs();
}
};
const [vt, setVT] = useVT(
() => ({ scroll: { y: tableScrollHeight } }),
[tableScrollHeight],
@ -542,7 +569,12 @@ const Env = () => {
const exportEnvs = () => {
const envs = value
.filter((x) => selectedRowIds.includes(x.id))
.map((x) => ({ value: x.value, name: x.name, remarks: x.remarks }));
.map((x) => ({
value: x.value,
name: x.name,
remarks: x.remarks,
labels: x.labels,
}));
exportJson('env.json', JSON.stringify(envs));
};
@ -550,6 +582,10 @@ const Env = () => {
setIsEditNameModalVisible(true);
};
const modifyLabels = () => {
setIsLabelModalVisible(true);
};
const onSearch = (value: string) => {
setSearchText(value.trim());
};
@ -622,6 +658,13 @@ const Env = () => {
>
{intl.get('批量修改变量名称')}
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={modifyLabels}
>
{intl.get('批量修改标签')}
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
@ -700,6 +743,12 @@ const Env = () => {
ids={selectedRowIds}
/>
)}
{isLabelModalVisible && (
<EnvLabelModal
handleCancel={handleLabelCancel}
ids={selectedRowIds}
/>
)}
</PageContainer>
);
};

View File

@ -1,8 +1,9 @@
import intl from 'react-intl-universal';
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Radio } from 'antd';
import { Modal, message, Input, Form, Radio, Button } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import EditableTagGroup from '@/components/tag';
const EnvModal = ({
env,
@ -16,7 +17,7 @@ const EnvModal = ({
const handleOk = async (values: any) => {
setLoading(true);
const { value, split, name, remarks } = values;
const { value, split, name, remarks, labels } = values;
const method = env ? 'put' : 'post';
let payload;
if (!env) {
@ -27,10 +28,11 @@ const EnvModal = ({
name: name,
value: x,
remarks: remarks,
labels: labels || [],
};
});
} else {
payload = [{ value, name, remarks }];
payload = [{ value, name, remarks, labels: labels || [] }];
}
} else {
payload = { ...values, id: env.id };
@ -123,9 +125,100 @@ const EnvModal = ({
<Form.Item name="remarks" label={intl.get('备注')}>
<Input placeholder={intl.get('请输入备注')} />
</Form.Item>
<Form.Item name="labels" label={intl.get('标签')}>
<EditableTagGroup />
</Form.Item>
</Form>
</Modal>
);
};
export default EnvModal;
export { EnvModal as default };
export const EnvLabelModal = ({
ids,
handleCancel,
}: {
ids: string[];
handleCancel: (needUpdate?: boolean) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const update = async (action: 'add' | 'delete') => {
try {
const values = await form.validateFields();
const payload = { ids, labels: values.labels };
setLoading(true);
const { code } =
action === 'add'
? await request.post(`${config.apiPrefix}envs/labels`, payload)
: await request.delete(`${config.apiPrefix}envs/labels`, {
data: payload,
});
if (code === 200) {
message.success(
action === 'add'
? intl.get('添加标签成功')
: intl.get('删除标签成功'),
);
handleCancel(true);
}
} catch (error: any) {
if (error?.errorFields) {
return;
}
} finally {
setLoading(false);
}
};
return (
<Modal
title={intl.get('批量修改标签')}
open={true}
footer={[
<Button key="cancel" onClick={() => handleCancel(false)}>
{intl.get('取消')}
</Button>,
<Button
key="delete"
type="primary"
danger
loading={loading}
onClick={() => update('delete')}
>
{intl.get('删除')}
</Button>,
<Button
key="add"
type="primary"
loading={loading}
onClick={() => update('add')}
>
{intl.get('添加')}
</Button>,
]}
centered
maskClosable={false}
forceRender
onCancel={() => handleCancel(false)}
>
<Form form={form} layout="vertical" name="env_label_modal">
<Form.Item
name="labels"
label={intl.get('标签')}
rules={[
{
required: true,
message: intl.get('请至少输入一个标签'),
},
]}
>
<EditableTagGroup />
</Form.Item>
</Form>
</Modal>
);
};