mirror of
https://github.com/whyour/qinglong.git
synced 2025-11-10 00:26:09 +08:00
Merge branch 'develop' into copilot/add-scenario-mode-support
This commit is contained in:
commit
00b1bc71e0
|
|
@ -1,12 +1,12 @@
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Joi, celebrate } from 'celebrate';
|
||||||
import { Container } from 'typedi';
|
import { NextFunction, Request, Response, Router } from 'express';
|
||||||
import EnvService from '../services/env';
|
|
||||||
import { Logger } from 'winston';
|
|
||||||
import { celebrate, Joi } from 'celebrate';
|
|
||||||
import multer from 'multer';
|
|
||||||
import config from '../config';
|
|
||||||
import fs from 'fs';
|
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 { safeJSONParse } from '../config/util';
|
||||||
|
import EnvService from '../services/env';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
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(
|
route.post(
|
||||||
'/upload',
|
'/upload',
|
||||||
upload.single('env'),
|
upload.single('env'),
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export class Crontab {
|
||||||
extra_schedules?: Array<{ schedule: string }>;
|
extra_schedules?: Array<{ schedule: string }>;
|
||||||
task_before?: string;
|
task_before?: string;
|
||||||
task_after?: string;
|
task_after?: string;
|
||||||
|
log_name?: string;
|
||||||
|
|
||||||
constructor(options: Crontab) {
|
constructor(options: Crontab) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
|
|
@ -45,6 +46,7 @@ export class Crontab {
|
||||||
this.extra_schedules = options.extra_schedules;
|
this.extra_schedules = options.extra_schedules;
|
||||||
this.task_before = options.task_before;
|
this.task_before = options.task_before;
|
||||||
this.task_after = options.task_after;
|
this.task_after = options.task_after;
|
||||||
|
this.log_name = options.log_name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,7 +57,7 @@ export enum CrontabStatus {
|
||||||
'disabled',
|
'disabled',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CronInstance extends Model<Crontab, Crontab>, Crontab { }
|
export interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
|
||||||
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||||
name: {
|
name: {
|
||||||
unique: 'compositeIndex',
|
unique: 'compositeIndex',
|
||||||
|
|
@ -84,4 +86,5 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
||||||
extra_schedules: DataTypes.JSON,
|
extra_schedules: DataTypes.JSON,
|
||||||
task_before: DataTypes.STRING,
|
task_before: DataTypes.STRING,
|
||||||
task_after: DataTypes.STRING,
|
task_after: DataTypes.STRING,
|
||||||
|
log_name: DataTypes.STRING,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { DataTypes, Model } from 'sequelize';
|
||||||
import { sequelize } from '.';
|
import { sequelize } from '.';
|
||||||
import { DataTypes, Model, ModelDefined } from 'sequelize';
|
|
||||||
|
|
||||||
export class Env {
|
export class Env {
|
||||||
value?: string;
|
value?: string;
|
||||||
|
|
@ -9,6 +9,7 @@ export class Env {
|
||||||
position?: number;
|
position?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
remarks?: string;
|
remarks?: string;
|
||||||
|
isPinned?: 1 | 0;
|
||||||
|
|
||||||
constructor(options: Env) {
|
constructor(options: Env) {
|
||||||
this.value = options.value;
|
this.value = options.value;
|
||||||
|
|
@ -21,6 +22,7 @@ export class Env {
|
||||||
this.position = options.position;
|
this.position = options.position;
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.remarks = options.remarks || '';
|
this.remarks = options.remarks || '';
|
||||||
|
this.isPinned = options.isPinned || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,4 +44,5 @@ export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
||||||
position: DataTypes.NUMBER,
|
position: DataTypes.NUMBER,
|
||||||
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||||
remarks: DataTypes.STRING,
|
remarks: DataTypes.STRING,
|
||||||
|
isPinned: { type: DataTypes.NUMBER, field: 'is_pinned' },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,12 @@ export default async () => {
|
||||||
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
try {
|
try {
|
||||||
await sequelize.query('alter table Scenarios add column workflowGraph JSON');
|
await sequelize.query(
|
||||||
|
'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) {}
|
} catch (error) {}
|
||||||
|
|
||||||
Logger.info('✌️ DB loaded');
|
Logger.info('✌️ DB loaded');
|
||||||
|
|
|
||||||
|
|
@ -476,15 +476,61 @@ export default class CronService {
|
||||||
`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`,
|
`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
let { id, command, log_path } = cron;
|
let { id, command, log_path, log_name } = cron;
|
||||||
const uniqPath = await getUniqPath(command, `${id}`);
|
|
||||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
// Check if log_name is an absolute path
|
||||||
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
const isAbsolutePath = log_name && log_name.startsWith('/');
|
||||||
if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
|
|
||||||
await fs.mkdir(logDirPath, { recursive: true });
|
let uniqPath: string;
|
||||||
|
let absolutePath: string;
|
||||||
|
let logPath: string;
|
||||||
|
|
||||||
|
if (isAbsolutePath) {
|
||||||
|
// Special case: /dev/null is allowed as-is to discard logs
|
||||||
|
if (log_name === '/dev/null') {
|
||||||
|
uniqPath = log_name;
|
||||||
|
absolutePath = log_name;
|
||||||
|
logPath = log_name;
|
||||||
|
} else {
|
||||||
|
// For other absolute paths, ensure they are within the safe log directory
|
||||||
|
const normalizedLogName = path.normalize(log_name!);
|
||||||
|
const normalizedLogPath = path.normalize(config.logPath);
|
||||||
|
|
||||||
|
if (!normalizedLogName.startsWith(normalizedLogPath)) {
|
||||||
|
this.logger.error(
|
||||||
|
`[panel][日志路径安全检查失败] 绝对路径必须在日志目录内: ${log_name}`,
|
||||||
|
);
|
||||||
|
// Fallback to auto-generated path for security
|
||||||
|
const fallbackUniqPath = await getUniqPath(command, `${id}`);
|
||||||
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||||
|
const logDirPath = path.resolve(config.logPath, `${fallbackUniqPath}`);
|
||||||
|
if (log_path?.split('/')?.every((x) => x !== fallbackUniqPath)) {
|
||||||
|
await fs.mkdir(logDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
logPath = `${fallbackUniqPath}/${logTime}.log`;
|
||||||
|
absolutePath = path.resolve(config.logPath, `${logPath}`);
|
||||||
|
uniqPath = fallbackUniqPath;
|
||||||
|
} else {
|
||||||
|
// Absolute path is safe, use it
|
||||||
|
uniqPath = log_name!;
|
||||||
|
absolutePath = log_name!;
|
||||||
|
logPath = log_name!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sanitize log_name to prevent path traversal for relative paths
|
||||||
|
const sanitizedLogName = log_name
|
||||||
|
? log_name.replace(/[\/\\\.]/g, '_').replace(/^_+|_+$/g, '')
|
||||||
|
: '';
|
||||||
|
uniqPath = sanitizedLogName || (await getUniqPath(command, `${id}`));
|
||||||
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||||
|
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
|
||||||
|
if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
|
||||||
|
await fs.mkdir(logDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
logPath = `${uniqPath}/${logTime}.log`;
|
||||||
|
absolutePath = path.resolve(config.logPath, `${logPath}`);
|
||||||
}
|
}
|
||||||
const logPath = `${uniqPath}/${logTime}.log`;
|
|
||||||
const absolutePath = path.resolve(config.logPath, `${logPath}`);
|
|
||||||
const cp = spawn(
|
const cp = spawn(
|
||||||
`real_log_path=${logPath} no_delay=true ${this.makeCommand(
|
`real_log_path=${logPath} no_delay=true ${this.makeCommand(
|
||||||
cron,
|
cron,
|
||||||
|
|
|
||||||
|
|
@ -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 winston from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as fs from 'fs/promises';
|
|
||||||
import {
|
import {
|
||||||
Env,
|
Env,
|
||||||
EnvModel,
|
EnvModel,
|
||||||
|
|
@ -11,8 +12,6 @@ import {
|
||||||
minPosition,
|
minPosition,
|
||||||
stepPosition,
|
stepPosition,
|
||||||
} from '../data/env';
|
} from '../data/env';
|
||||||
import groupBy from 'lodash/groupBy';
|
|
||||||
import { FindOptions, Op } from 'sequelize';
|
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
|
|
@ -147,6 +146,7 @@ export default class EnvService {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.find(condition, [
|
const result = await this.find(condition, [
|
||||||
|
['isPinned', 'DESC'],
|
||||||
['position', 'DESC'],
|
['position', 'DESC'],
|
||||||
['createdAt', 'ASC'],
|
['createdAt', 'ASC'],
|
||||||
]);
|
]);
|
||||||
|
|
@ -190,6 +190,14 @@ export default class EnvService {
|
||||||
await this.set_envs();
|
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() {
|
public async set_envs() {
|
||||||
const envs = await this.envs('', {
|
const envs = await this.envs('', {
|
||||||
name: { [Op.not]: null },
|
name: { [Op.not]: null },
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { Joi } from 'celebrate';
|
import { Joi } from 'celebrate';
|
||||||
import cron_parser from 'cron-parser';
|
import cron_parser from 'cron-parser';
|
||||||
import { ScheduleType } from '../interface/schedule';
|
import { ScheduleType } from '../interface/schedule';
|
||||||
|
import path from 'path';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
const validateSchedule = (value: string, helpers: any) => {
|
const validateSchedule = (value: string, helpers: any) => {
|
||||||
if (
|
if (
|
||||||
|
|
@ -37,4 +39,43 @@ export const commonCronSchema = {
|
||||||
extra_schedules: Joi.array().optional().allow(null),
|
extra_schedules: Joi.array().optional().allow(null),
|
||||||
task_before: Joi.string().optional().allow('').allow(null),
|
task_before: Joi.string().optional().allow('').allow(null),
|
||||||
task_after: Joi.string().optional().allow('').allow(null),
|
task_after: Joi.string().optional().allow('').allow(null),
|
||||||
|
log_name: Joi.string()
|
||||||
|
.optional()
|
||||||
|
.allow('')
|
||||||
|
.allow(null)
|
||||||
|
.custom((value, helpers) => {
|
||||||
|
if (!value) return value;
|
||||||
|
|
||||||
|
// Check if it's an absolute path
|
||||||
|
if (value.startsWith('/')) {
|
||||||
|
// Allow /dev/null as special case
|
||||||
|
if (value === '/dev/null') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other absolute paths, ensure they are within the safe log directory
|
||||||
|
const normalizedValue = path.normalize(value);
|
||||||
|
const normalizedLogPath = path.normalize(config.logPath);
|
||||||
|
|
||||||
|
if (!normalizedValue.startsWith(normalizedLogPath)) {
|
||||||
|
return helpers.error('string.unsafePath');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For relative names, enforce strict pattern
|
||||||
|
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
||||||
|
return helpers.error('string.pattern.base');
|
||||||
|
}
|
||||||
|
if (value.length > 100) {
|
||||||
|
return helpers.error('string.max');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
.messages({
|
||||||
|
'string.pattern.base': '日志名称只能包含字母、数字、下划线和连字符',
|
||||||
|
'string.max': '日志名称不能超过100个字符',
|
||||||
|
'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ copy_dep() {
|
||||||
|
|
||||||
pm2_log() {
|
pm2_log() {
|
||||||
echo -e "---> pm2日志"
|
echo -e "---> pm2日志"
|
||||||
local panelOut="/root/.pm2/logs/panel-out.log"
|
local panelOut="/root/.pm2/logs/qinglong-out.log"
|
||||||
local panelError="/root/.pm2/logs/panel-error.log"
|
local panelError="/root/.pm2/logs/qinglong-error.log"
|
||||||
tail -n 300 "$panelOut"
|
tail -n 300 "$panelOut"
|
||||||
tail -n 300 "$panelError"
|
tail -n 300 "$panelError"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -592,4 +592,13 @@
|
||||||
"否": "No",
|
"否": "No",
|
||||||
"共": "Total",
|
"共": "Total",
|
||||||
"项": "items"
|
"项": "items"
|
||||||
|
"日志名称": "Log Name",
|
||||||
|
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate",
|
||||||
|
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate. Supports absolute paths like /dev/null",
|
||||||
|
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持 /dev/null 丢弃日志,其他绝对路径必须在日志目录内": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate. Supports /dev/null to discard logs, other absolute paths must be within log directory",
|
||||||
|
"请输入自定义日志文件夹名称": "Please enter a custom log folder name",
|
||||||
|
"请输入自定义日志文件夹名称或绝对路径": "Please enter a custom log folder name or absolute path",
|
||||||
|
"请输入自定义日志文件夹名称或 /dev/null": "Please enter a custom log folder name or /dev/null",
|
||||||
|
"日志名称只能包含字母、数字、下划线和连字符": "Log name can only contain letters, numbers, underscores and hyphens",
|
||||||
|
"日志名称不能超过100个字符": "Log name cannot exceed 100 characters"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -592,4 +592,13 @@
|
||||||
"否": "否",
|
"否": "否",
|
||||||
"共": "共",
|
"共": "共",
|
||||||
"项": "项"
|
"项": "项"
|
||||||
|
"日志名称": "日志名称",
|
||||||
|
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成",
|
||||||
|
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null",
|
||||||
|
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持 /dev/null 丢弃日志,其他绝对路径必须在日志目录内": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持 /dev/null 丢弃日志,其他绝对路径必须在日志目录内",
|
||||||
|
"请输入自定义日志文件夹名称": "请输入自定义日志文件夹名称",
|
||||||
|
"请输入自定义日志文件夹名称或绝对路径": "请输入自定义日志文件夹名称或绝对路径",
|
||||||
|
"请输入自定义日志文件夹名称或 /dev/null": "请输入自定义日志文件夹名称或 /dev/null",
|
||||||
|
"日志名称只能包含字母、数字、下划线和连字符": "日志名称只能包含字母、数字、下划线和连字符",
|
||||||
|
"日志名称不能超过100个字符": "日志名称不能超过100个字符"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,42 @@ const CronModal = ({
|
||||||
<Form.Item name="labels" label={intl.get('标签')}>
|
<Form.Item name="labels" label={intl.get('标签')}>
|
||||||
<EditableTagGroup />
|
<EditableTagGroup />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="log_name"
|
||||||
|
label={intl.get('日志名称')}
|
||||||
|
tooltip={intl.get(
|
||||||
|
'自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持 /dev/null 丢弃日志,其他绝对路径必须在日志目录内',
|
||||||
|
)}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: (_, value) => {
|
||||||
|
if (!value) return Promise.resolve();
|
||||||
|
// Allow /dev/null specifically
|
||||||
|
if (value === '/dev/null') return Promise.resolve();
|
||||||
|
// Warn about other absolute paths (server will validate)
|
||||||
|
if (value.startsWith('/')) {
|
||||||
|
// We can't validate the exact path on frontend, but inform user
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
// For relative names, enforce strict pattern
|
||||||
|
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
||||||
|
return Promise.reject(
|
||||||
|
intl.get('日志名称只能包含字母、数字、下划线和连字符'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value.length > 100) {
|
||||||
|
return Promise.reject(intl.get('日志名称不能超过100个字符'));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={intl.get('请输入自定义日志文件夹名称或 /dev/null')}
|
||||||
|
maxLength={200}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="task_before"
|
name="task_before"
|
||||||
label={intl.get('执行前')}
|
label={intl.get('执行前')}
|
||||||
|
|
@ -312,4 +348,3 @@ const CronLabelModal = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export { CronLabelModal, CronModal as default };
|
export { CronLabelModal, CronModal as default };
|
||||||
|
|
||||||
|
|
|
||||||
147
src/pages/env/index.tsx
vendored
147
src/pages/env/index.tsx
vendored
|
|
@ -1,47 +1,42 @@
|
||||||
import intl from 'react-intl-universal';
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
import React, {
|
import { SharedContext } from '@/layouts';
|
||||||
useCallback,
|
import config from '@/utils/config';
|
||||||
useRef,
|
import { request } from '@/utils/http';
|
||||||
useState,
|
import { exportJson } from '@/utils/index';
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
} from 'react';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
message,
|
|
||||||
Modal,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Space,
|
|
||||||
Typography,
|
|
||||||
Tooltip,
|
|
||||||
Input,
|
|
||||||
UploadProps,
|
|
||||||
Upload,
|
|
||||||
} from 'antd';
|
|
||||||
import {
|
|
||||||
EditOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
SyncOutlined,
|
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
PushpinFilled,
|
||||||
|
PushpinOutlined,
|
||||||
StopOutlined,
|
StopOutlined,
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import config from '@/utils/config';
|
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
import { request } from '@/utils/http';
|
import { useOutletContext } from '@umijs/max';
|
||||||
import EnvModal from './modal';
|
import {
|
||||||
import EditNameModal from './editNameModal';
|
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 { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import './index.less';
|
import intl from 'react-intl-universal';
|
||||||
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 { useVT } from 'virtualizedtableforantd4';
|
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 { Paragraph } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
@ -59,11 +54,15 @@ enum StatusColor {
|
||||||
enum OperationName {
|
enum OperationName {
|
||||||
'启用',
|
'启用',
|
||||||
'禁用',
|
'禁用',
|
||||||
|
'置顶',
|
||||||
|
'取消置顶',
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OperationPath {
|
enum OperationPath {
|
||||||
'enable',
|
'enable',
|
||||||
'disable',
|
'disable',
|
||||||
|
'pin',
|
||||||
|
'unpin',
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = 'DragableBodyRow';
|
const type = 'DragableBodyRow';
|
||||||
|
|
@ -181,7 +180,7 @@ const Env = () => {
|
||||||
{
|
{
|
||||||
title: intl.get('操作'),
|
title: intl.get('操作'),
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 120,
|
width: 160,
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
const isPc = !isPhone;
|
const isPc = !isPhone;
|
||||||
return (
|
return (
|
||||||
|
|
@ -208,6 +207,23 @@ const Env = () => {
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
isPc
|
||||||
|
? record.isPinned === 1
|
||||||
|
? intl.get('取消置顶')
|
||||||
|
: intl.get('置顶')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a onClick={() => pinOrUnpinEnv(record, index)}>
|
||||||
|
{record.isPinned === 1 ? (
|
||||||
|
<PushpinFilled />
|
||||||
|
) : (
|
||||||
|
<PushpinOutlined />
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip title={isPc ? intl.get('删除') : ''}>
|
<Tooltip title={isPc ? intl.get('删除') : ''}>
|
||||||
<a onClick={() => deleteEnv(record, index)}>
|
<a onClick={() => deleteEnv(record, index)}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
|
|
@ -305,6 +321,51 @@ const Env = () => {
|
||||||
setIsModalVisible(true);
|
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{' '}
|
||||||
|
<Paragraph
|
||||||
|
style={{ wordBreak: 'break-all', display: 'inline' }}
|
||||||
|
ellipsis={{ rows: 6, expandable: true }}
|
||||||
|
type="warning"
|
||||||
|
copyable
|
||||||
|
>
|
||||||
|
{record.name}: {record.value}
|
||||||
|
</Paragraph>{' '}
|
||||||
|
{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) => {
|
const deleteEnv = (record: any, index: number) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: intl.get('确认删除'),
|
title: intl.get('确认删除'),
|
||||||
|
|
@ -589,6 +650,20 @@ const Env = () => {
|
||||||
>
|
>
|
||||||
{intl.get('批量禁用')}
|
{intl.get('批量禁用')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => operateEnvs(2)}
|
||||||
|
style={{ marginLeft: 8, marginBottom: 5 }}
|
||||||
|
>
|
||||||
|
{intl.get('批量置顶')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => operateEnvs(3)}
|
||||||
|
style={{ marginLeft: 8, marginRight: 8 }}
|
||||||
|
>
|
||||||
|
{intl.get('批量取消置顶')}
|
||||||
|
</Button>
|
||||||
<span style={{ marginLeft: 8 }}>
|
<span style={{ marginLeft: 8 }}>
|
||||||
{intl.get('已选择')}
|
{intl.get('已选择')}
|
||||||
<a>{selectedRowIds?.length}</a>
|
<a>{selectedRowIds?.length}</a>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user