mirror of
https://github.com/whyour/qinglong.git
synced 2025-07-30 00:16:07 +08:00
Merge branch 'develop' into patch-1
This commit is contained in:
commit
88cbf8f8c8
|
@ -10,3 +10,4 @@ SECRET='whyour'
|
||||||
QINIU_AK=''
|
QINIU_AK=''
|
||||||
QINIU_SK=''
|
QINIU_SK=''
|
||||||
QINIU_SCOPE=''
|
QINIU_SCOPE=''
|
||||||
|
TEMP=''
|
||||||
|
|
8
.github/workflows/build_docker_image.yml
vendored
8
.github/workflows/build_docker_image.yml
vendored
|
@ -5,7 +5,6 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- 'develop'
|
- 'develop'
|
||||||
# Sequence of patterns matched against refs/tags
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
schedule:
|
schedule:
|
||||||
|
@ -13,7 +12,6 @@ on:
|
||||||
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
|
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
|
||||||
# minute hour day month dayOfWeek
|
# minute hour day month dayOfWeek
|
||||||
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
|
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
|
||||||
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -155,12 +153,16 @@ jobs:
|
||||||
build-args: |
|
build-args: |
|
||||||
MAINTAINER=${{ github.repository_owner }}
|
MAINTAINER=${{ github.repository_owner }}
|
||||||
QL_BRANCH=${{ github.ref_name }}
|
QL_BRANCH=${{ github.ref_name }}
|
||||||
|
SOURCE_COMMIT=${{ github.sha }}
|
||||||
network: host
|
network: host
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
context: docker/
|
context: .
|
||||||
|
file: ./docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=whyour/qinglong:cache
|
||||||
|
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
|
||||||
|
|
||||||
- name: Image digest
|
- name: Image digest
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|
|
@ -6,10 +6,11 @@ export default defineConfig({
|
||||||
antd: {},
|
antd: {},
|
||||||
outputPath: 'static/dist',
|
outputPath: 'static/dist',
|
||||||
fastRefresh: true,
|
fastRefresh: true,
|
||||||
favicons: ['/images/favicon.svg'],
|
favicons: ['./images/favicon.svg'],
|
||||||
mfsu: {
|
mfsu: {
|
||||||
strategy: 'eager',
|
strategy: 'eager',
|
||||||
},
|
},
|
||||||
|
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/public': {
|
'/api/public': {
|
||||||
target: 'http://127.0.0.1:5400/',
|
target: 'http://127.0.0.1:5400/',
|
||||||
|
|
|
@ -1 +1,7 @@
|
||||||
export const LOG_END_SYMBOL = '\n ';
|
export const LOG_END_SYMBOL = '\n ';
|
||||||
|
|
||||||
|
export const TASK_COMMAND = 'task';
|
||||||
|
export const QL_COMMAND = 'ql';
|
||||||
|
|
||||||
|
export const TASK_PREFIX = `${TASK_COMMAND} `;
|
||||||
|
export const QL_PREFIX = `${QL_COMMAND} `;
|
||||||
|
|
|
@ -6,15 +6,12 @@ import config from './config';
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.get('/api/public/panel/log', (req, res) => {
|
app.get('/api/public/panel/log', (req, res) => {
|
||||||
exec(
|
exec('tail -n 300 ~/.pm2/logs/panel-error.log', (err, stdout, stderr) => {
|
||||||
'pm2 logs panel --lines 500 --nostream --timestamp',
|
if (err || stderr) {
|
||||||
(err, stdout, stderr) => {
|
return res.send({ code: 400, message: (err && err.message) || stderr });
|
||||||
if (err || stderr) {
|
}
|
||||||
return res.send({ code: 400, message: (err && err.message) || stderr });
|
return res.send({ code: 200, data: stdout });
|
||||||
}
|
});
|
||||||
return res.send({ code: 200, data: stdout });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app
|
app
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { exec } from 'child_process';
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
import { CrontabModel, CrontabStatus } from './data/cron';
|
import { CrontabModel, CrontabStatus } from './data/cron';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
import { QL_PREFIX, TASK_PREFIX } from './config/const';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
@ -23,8 +24,11 @@ const run = async () => {
|
||||||
) {
|
) {
|
||||||
schedule.scheduleJob(task.schedule, function () {
|
schedule.scheduleJob(task.schedule, function () {
|
||||||
let command = task.command as string;
|
let command = task.command as string;
|
||||||
if (!command.includes('task ') && !command.includes('ql ')) {
|
if (
|
||||||
command = `task ${command}`;
|
!command.startsWith(TASK_PREFIX) &&
|
||||||
|
!command.startsWith(QL_PREFIX)
|
||||||
|
) {
|
||||||
|
command = `${TASK_PREFIX}${command}`;
|
||||||
}
|
}
|
||||||
exec(`ID=${task.id} ${command}`);
|
exec(`ID=${task.id} ${command}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
import { promises, existsSync } from 'fs';
|
import { promises, existsSync } from 'fs';
|
||||||
import { Op, where, col as colFn } from 'sequelize';
|
import { Op, where, col as colFn } from 'sequelize';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
|
@ -31,7 +32,7 @@ export default class CronService {
|
||||||
const tab = new Crontab(payload);
|
const tab = new Crontab(payload);
|
||||||
tab.saved = false;
|
tab.saved = false;
|
||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
await this.set_crontab(this.isSixCron(doc));
|
await this.set_crontab();
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +43,7 @@ export default class CronService {
|
||||||
public async update(payload: Crontab): Promise<Crontab> {
|
public async update(payload: Crontab): Promise<Crontab> {
|
||||||
payload.saved = false;
|
payload.saved = false;
|
||||||
const newDoc = await this.updateDb(payload);
|
const newDoc = await this.updateDb(payload);
|
||||||
await this.set_crontab(this.isSixCron(newDoc));
|
await this.set_crontab();
|
||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +82,7 @@ export default class CronService {
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[]) {
|
||||||
await CrontabModel.destroy({ where: { id: ids } });
|
await CrontabModel.destroy({ where: { id: ids } });
|
||||||
await this.set_crontab(true);
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pin(ids: number[]) {
|
public async pin(ids: number[]) {
|
||||||
|
@ -361,8 +362,8 @@ export default class CronService {
|
||||||
this.logger.silly('Original command: ' + command);
|
this.logger.silly('Original command: ' + command);
|
||||||
|
|
||||||
let cmdStr = command;
|
let cmdStr = command;
|
||||||
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
|
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
|
||||||
cmdStr = `task ${cmdStr}`;
|
cmdStr = `${TASK_PREFIX}${cmdStr}`;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
cmdStr.endsWith('.js') ||
|
cmdStr.endsWith('.js') ||
|
||||||
|
@ -408,12 +409,12 @@ export default class CronService {
|
||||||
|
|
||||||
public async disabled(ids: number[]) {
|
public async disabled(ids: number[]) {
|
||||||
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||||
await this.set_crontab(true);
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enabled(ids: number[]) {
|
public async enabled(ids: number[]) {
|
||||||
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||||
await this.set_crontab(true);
|
await this.set_crontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async log(id: number) {
|
public async log(id: number) {
|
||||||
|
@ -519,11 +520,17 @@ export default class CronService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private make_command(tab: Crontab) {
|
private make_command(tab: Crontab) {
|
||||||
|
if (
|
||||||
|
!tab.command.startsWith(TASK_PREFIX) &&
|
||||||
|
!tab.command.startsWith(QL_PREFIX)
|
||||||
|
) {
|
||||||
|
tab.command = `${TASK_PREFIX}${tab.command}`;
|
||||||
|
}
|
||||||
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
|
||||||
return crontab_job_string;
|
return crontab_job_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async set_crontab(needReloadSchedule: boolean = false) {
|
private async set_crontab() {
|
||||||
const tabs = await this.crontabs();
|
const tabs = await this.crontabs();
|
||||||
var crontab_string = '';
|
var crontab_string = '';
|
||||||
tabs.data.forEach((tab) => {
|
tabs.data.forEach((tab) => {
|
||||||
|
@ -546,9 +553,7 @@ export default class CronService {
|
||||||
fs.writeFileSync(config.crontabFile, crontab_string);
|
fs.writeFileSync(config.crontabFile, crontab_string);
|
||||||
|
|
||||||
execSync(`crontab ${config.crontabFile}`);
|
execSync(`crontab ${config.crontabFile}`);
|
||||||
if (needReloadSchedule) {
|
exec(`pm2 reload schedule`);
|
||||||
exec(`pm2 reload schedule`);
|
|
||||||
}
|
|
||||||
await CrontabModel.update({ saved: true }, { where: {} });
|
await CrontabModel.update({ saved: true }, { where: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import SockService from './sock';
|
||||||
import CronService from './cron';
|
import CronService from './cron';
|
||||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { LOG_END_SYMBOL } from '../config/const';
|
import { TASK_COMMAND } from '../config/const';
|
||||||
import { getPid, killTask } from '../config/util';
|
import { getPid, killTask } from '../config/util';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
|
@ -42,7 +42,7 @@ export default class ScriptService {
|
||||||
|
|
||||||
public async runScript(filePath: string) {
|
public async runScript(filePath: string) {
|
||||||
const relativePath = path.relative(config.scriptPath, filePath);
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
const command = `task -l ${relativePath} now`;
|
const command = `${TASK_COMMAND} -l ${relativePath} now`;
|
||||||
const pid = await this.scheduleService.runTask(
|
const pid = await this.scheduleService.runTask(
|
||||||
command,
|
command,
|
||||||
this.taskCallbacks(filePath),
|
this.taskCallbacks(filePath),
|
||||||
|
@ -56,7 +56,7 @@ export default class ScriptService {
|
||||||
let str = '';
|
let str = '';
|
||||||
if (!pid) {
|
if (!pid) {
|
||||||
const relativePath = path.relative(config.scriptPath, filePath);
|
const relativePath = path.relative(config.scriptPath, filePath);
|
||||||
pid = await getPid(`task -l ${relativePath} now`);
|
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await killTask(pid);
|
await killTask(pid);
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
FROM python:3.10-alpine as builder
|
||||||
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||||
|
RUN set -x \
|
||||||
|
&& apk update \
|
||||||
|
&& apk add nodejs npm \
|
||||||
|
&& npm i -g pnpm \
|
||||||
|
&& cd /tmp/build \
|
||||||
|
&& pnpm install --prod
|
||||||
|
|
||||||
FROM python:3.10-alpine
|
FROM python:3.10-alpine
|
||||||
|
|
||||||
ARG QL_MAINTAINER="whyour"
|
ARG QL_MAINTAINER="whyour"
|
||||||
|
@ -13,8 +22,6 @@ ENV PNPM_HOME=/root/.local/share/pnpm \
|
||||||
QL_DIR=/ql \
|
QL_DIR=/ql \
|
||||||
QL_BRANCH=${QL_BRANCH}
|
QL_BRANCH=${QL_BRANCH}
|
||||||
|
|
||||||
WORKDIR ${QL_DIR}
|
|
||||||
|
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||||
&& apk update -f \
|
&& apk update -f \
|
||||||
|
@ -42,19 +49,24 @@ RUN set -x \
|
||||||
&& git config --global http.postBuffer 524288000 \
|
&& git config --global http.postBuffer 524288000 \
|
||||||
&& npm install -g pnpm \
|
&& npm install -g pnpm \
|
||||||
&& pnpm add -g pm2 ts-node typescript tslib \
|
&& pnpm add -g pm2 ts-node typescript tslib \
|
||||||
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
&& rm -rf /root/.pnpm-store \
|
||||||
|
&& rm -rf /root/.local/share/pnpm/store \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& rm -rf /root/.npm
|
||||||
|
|
||||||
|
ARG SOURCE_COMMIT
|
||||||
|
RUN git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
|
||||||
&& cd ${QL_DIR} \
|
&& cd ${QL_DIR} \
|
||||||
&& cp -f .env.example .env \
|
&& cp -f .env.example .env \
|
||||||
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
&& chmod 777 ${QL_DIR}/shell/*.sh \
|
||||||
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
&& chmod 777 ${QL_DIR}/docker/*.sh \
|
||||||
&& pnpm install --prod \
|
|
||||||
&& rm -rf /root/.pnpm-store \
|
|
||||||
&& rm -rf /root/.local/share/pnpm/store \
|
|
||||||
&& rm -rf /root/.cache \
|
|
||||||
&& rm -rf /root/.npm \
|
|
||||||
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
|
||||||
&& mkdir -p ${QL_DIR}/static \
|
&& mkdir -p ${QL_DIR}/static \
|
||||||
&& cp -rf /static/* ${QL_DIR}/static \
|
&& cp -rf /static/* ${QL_DIR}/static \
|
||||||
&& rm -rf /static
|
&& rm -rf /static
|
||||||
|
|
||||||
|
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
|
||||||
|
|
||||||
|
WORKDIR ${QL_DIR}
|
||||||
|
|
||||||
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
|
||||||
|
|
13
package.json
13
package.json
|
@ -42,7 +42,8 @@
|
||||||
"monaco-editor",
|
"monaco-editor",
|
||||||
"rc-field-form",
|
"rc-field-form",
|
||||||
"@types/lodash.merge",
|
"@types/lodash.merge",
|
||||||
"rollup"
|
"rollup",
|
||||||
|
"styled-components"
|
||||||
],
|
],
|
||||||
"allowedVersions": {
|
"allowedVersions": {
|
||||||
"react": "18",
|
"react": "18",
|
||||||
|
@ -90,7 +91,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^4.7.0",
|
"@ant-design/icons": "^4.7.0",
|
||||||
"@ant-design/pro-layout": "^6.33.1",
|
"@ant-design/pro-layout": "6.38.22",
|
||||||
"@monaco-editor/react": "4.4.6",
|
"@monaco-editor/react": "4.4.6",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@sentry/react": "^7.12.1",
|
"@sentry/react": "^7.12.1",
|
||||||
|
@ -108,21 +109,22 @@
|
||||||
"@types/nodemailer": "^6.4.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"@types/qrcode.react": "^1.0.2",
|
"@types/qrcode.react": "^1.0.2",
|
||||||
"@types/react": "^18.0.20",
|
"@types/react": "^18.0.20",
|
||||||
|
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/serve-handler": "^6.1.1",
|
"@types/serve-handler": "^6.1.1",
|
||||||
"@types/sockjs": "^0.3.33",
|
"@types/sockjs": "^0.3.33",
|
||||||
"@types/sockjs-client": "^1.5.1",
|
"@types/sockjs-client": "^1.5.1",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@umijs/max": "^4.0.21",
|
"@umijs/max": "^4.0.42",
|
||||||
"@umijs/ssr-darkreader": "^4.9.45",
|
"@umijs/ssr-darkreader": "^4.9.45",
|
||||||
"ansi-to-react": "^6.1.6",
|
"ansi-to-react": "^6.1.6",
|
||||||
"antd": "^4.23.0",
|
"antd": "^4.24.7",
|
||||||
"antd-img-crop": "^4.2.3",
|
"antd-img-crop": "^4.2.3",
|
||||||
"codemirror": "^5.65.2",
|
"codemirror": "^5.65.2",
|
||||||
"compression-webpack-plugin": "9.2.0",
|
"compression-webpack-plugin": "9.2.0",
|
||||||
"concurrently": "^7.0.0",
|
"concurrently": "^7.0.0",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"monaco-editor": "^0.34.1",
|
"monaco-editor": "0.33.0",
|
||||||
"nodemon": "^2.0.15",
|
"nodemon": "^2.0.15",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"qiniu": "^7.4.0",
|
"qiniu": "^7.4.0",
|
||||||
|
@ -131,6 +133,7 @@
|
||||||
"rc-tween-one": "^3.0.6",
|
"rc-tween-one": "^3.0.6",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-codemirror2": "^7.2.1",
|
"react-codemirror2": "^7.2.1",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-diff-viewer": "^3.1.1",
|
"react-diff-viewer": "^3.1.1",
|
||||||
"react-dnd": "^14.0.2",
|
"react-dnd": "^14.0.2",
|
||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "^14.0.0",
|
||||||
|
|
5379
pnpm-lock.yaml
5379
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -162,6 +162,7 @@ export AIBOTK_NAME=""
|
||||||
|
|
||||||
## 14. SMTP
|
## 14. SMTP
|
||||||
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
|
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
|
||||||
|
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
|
||||||
export SMTP_SERVER=""
|
export SMTP_SERVER=""
|
||||||
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
|
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
|
||||||
export SMTP_SSL="false"
|
export SMTP_SSL="false"
|
||||||
|
|
|
@ -1087,6 +1087,16 @@ async function smtpNotify(text, desp) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function smtpNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (SMTP_SERVER && SMTP_SSL && SMTP_EMAIL && SMTP_PASSWORD && SMTP_NAME) {
|
||||||
|
// todo: Node.js并没有内置的 smtp 实现,需要调用外部库,因为不清楚这个文件的模块依赖情况,所以留给有缘人实现
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendNotify,
|
sendNotify,
|
||||||
BARK_PUSH,
|
BARK_PUSH,
|
||||||
|
|
|
@ -81,18 +81,19 @@ run_nohup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
check_server() {
|
check_server() {
|
||||||
local top_result=$(top -b -n 1)
|
if [[ $cpu_warn ]] && [[ $mem_warn ]] && [[ $disk_warn ]]; then
|
||||||
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
|
local top_result=$(top -b -n 1)
|
||||||
|
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
|
||||||
|
|
||||||
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
|
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
|
||||||
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
|
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
|
||||||
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
|
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
|
||||||
|
|
||||||
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
|
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
|
||||||
|
if [[ $cpu_use -gt $cpu_warn ]] && [[ $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
|
||||||
if [[ $cpu_use -gt $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
|
local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
|
||||||
local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
|
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
|
||||||
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,9 +79,9 @@ import_config() {
|
||||||
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
|
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cpu_warn=${CpuWarn:-80}
|
cpu_warn=${CpuWarn}
|
||||||
mem_warn=${MemoryWarn:-80}
|
mem_warn=${MemoryWarn}
|
||||||
disk_warn=${DiskWarn:-90}
|
disk_warn=${DiskWarn}
|
||||||
}
|
}
|
||||||
|
|
||||||
set_proxy() {
|
set_proxy() {
|
||||||
|
|
|
@ -224,7 +224,7 @@ update_qinglong() {
|
||||||
if [ "$githubStatus" == "" ]; then
|
if [ "$githubStatus" == "" ]; then
|
||||||
mirror="gitee"
|
mirror="gitee"
|
||||||
fi
|
fi
|
||||||
echo -e "\n使用 ${mirror} 源更新...\n"
|
echo -e "使用 ${mirror} 源更新...\n"
|
||||||
export isFirstStartServer=false
|
export isFirstStartServer=false
|
||||||
|
|
||||||
local all_branch=$(cd ${dir_root} && git branch -a)
|
local all_branch=$(cd ${dir_root} && git branch -a)
|
||||||
|
@ -238,6 +238,7 @@ update_qinglong() {
|
||||||
|
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新青龙源文件成功...\n"
|
echo -e "\n更新青龙源文件成功...\n"
|
||||||
|
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
|
||||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||||
update_depend
|
update_depend
|
||||||
|
|
||||||
|
@ -263,6 +264,7 @@ update_qinglong_static() {
|
||||||
fi
|
fi
|
||||||
if [[ $exit_status -eq 0 ]]; then
|
if [[ $exit_status -eq 0 ]]; then
|
||||||
echo -e "\n更新青龙静态资源成功...\n"
|
echo -e "\n更新青龙静态资源成功...\n"
|
||||||
|
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
|
||||||
|
|
||||||
rm -rf $dir_static/*
|
rm -rf $dir_static/*
|
||||||
cp -rf $ql_static_repo/* $dir_static
|
cp -rf $ql_static_repo/* $dir_static
|
||||||
|
|
39
src/components/copy.tsx
Normal file
39
src/components/copy.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
|
import { Tooltip, Typography } from 'antd';
|
||||||
|
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
|
||||||
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
|
|
||||||
|
const { Link } = Typography;
|
||||||
|
|
||||||
|
const Copy = ({ text }: { text: string }) => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const copyIdRef = useRef<number>();
|
||||||
|
|
||||||
|
const copyText = (e?: React.MouseEvent) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
e?.stopPropagation();
|
||||||
|
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
|
cleanCopyId();
|
||||||
|
copyIdRef.current = window.setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanCopyId = () => {
|
||||||
|
window.clearTimeout(copyIdRef.current!);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link onClick={copyText} style={{ marginLeft: 1 }}>
|
||||||
|
<CopyToClipboard text={text}>
|
||||||
|
<Tooltip key="copy" title={copied ? '复制成功' : '复制'}>
|
||||||
|
{copied ? <CheckOutlined /> : <CopyOutlined />}
|
||||||
|
</Tooltip>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Copy;
|
|
@ -10,6 +10,7 @@ import {
|
||||||
ContainerOutlined,
|
ContainerOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import IconFont from '@/components/iconfont';
|
import IconFont from '@/components/iconfont';
|
||||||
|
import { BasicLayoutProps } from '@ant-design/pro-layout';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
route: {
|
route: {
|
||||||
|
@ -93,4 +94,4 @@ export default {
|
||||||
contentWidth: 'Fixed',
|
contentWidth: 'Fixed',
|
||||||
splitMenus: false,
|
splitMenus: false,
|
||||||
siderWidth: 180,
|
siderWidth: 180,
|
||||||
} as any;
|
} as BasicLayoutProps;
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
Popover,
|
Popover,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
MenuProps,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import SockJS from 'sockjs-client';
|
import SockJS from 'sockjs-client';
|
||||||
|
@ -222,9 +223,6 @@ export default function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
|
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
|
||||||
document.title = `${
|
|
||||||
(config.documentTitleMap as any)[location.pathname]
|
|
||||||
} - 控制面板`;
|
|
||||||
if (systemInfo?.isInitialized && location.pathname === '/initialization') {
|
if (systemInfo?.isInitialized && location.pathname === '/initialization') {
|
||||||
history.push('/crontab');
|
history.push('/crontab');
|
||||||
}
|
}
|
||||||
|
@ -251,13 +249,17 @@ export default function () {
|
||||||
!navigator.userAgent.includes('Chrome');
|
!navigator.userAgent.includes('Chrome');
|
||||||
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
|
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
|
||||||
|
|
||||||
const menu = (
|
const menu: MenuProps = {
|
||||||
<Menu
|
items: [
|
||||||
className="side-menu-user-drop-menu"
|
{
|
||||||
items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]}
|
label: '退出登录',
|
||||||
onClick={logout}
|
className: 'side-menu-user-drop-menu',
|
||||||
/>
|
onClick: logout,
|
||||||
);
|
key: 'logout',
|
||||||
|
icon: <LogoutOutlined />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
return loading ? (
|
return loading ? (
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
) : (
|
) : (
|
||||||
|
@ -266,6 +268,7 @@ export default function () {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
ErrorBoundary={Sentry.ErrorBoundary}
|
ErrorBoundary={Sentry.ErrorBoundary}
|
||||||
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
|
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
|
||||||
|
// @ts-ignore
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<span style={{ fontSize: 16 }}>控制面板</span>
|
<span style={{ fontSize: 16 }}>控制面板</span>
|
||||||
|
@ -308,16 +311,15 @@ export default function () {
|
||||||
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
|
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
|
||||||
}}
|
}}
|
||||||
pageTitleRender={(props, pageName, info) => {
|
pageTitleRender={(props, pageName, info) => {
|
||||||
if (info && typeof info.pageName === 'string') {
|
const title =
|
||||||
return `${info.pageName} - 控制面板`;
|
(config.documentTitleMap as any)[location.pathname] || '未找到';
|
||||||
}
|
return `${title} - 控制面板`;
|
||||||
return '控制面板';
|
|
||||||
}}
|
}}
|
||||||
onCollapse={setCollapsed}
|
onCollapse={setCollapsed}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
rightContentRender={() =>
|
rightContentRender={() =>
|
||||||
ctx.isPhone && (
|
ctx.isPhone && (
|
||||||
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}>
|
<Dropdown menu={menu} placement="bottomRight" trigger={['click']}>
|
||||||
<span className="side-menu-user-wrapper">
|
<span className="side-menu-user-wrapper">
|
||||||
<Avatar
|
<Avatar
|
||||||
shape="square"
|
shape="square"
|
||||||
|
@ -339,7 +341,7 @@ export default function () {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!collapsed && !ctx.isPhone && (
|
{!collapsed && !ctx.isPhone && (
|
||||||
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}>
|
<Dropdown menu={menu} placement="topLeft" trigger={['hover']}>
|
||||||
<span className="side-menu-user-wrapper">
|
<span className="side-menu-user-wrapper">
|
||||||
<Avatar
|
<Avatar
|
||||||
shape="square"
|
shape="square"
|
||||||
|
|
18
src/pages/404.tsx
Normal file
18
src/pages/404.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, Result, Typography } from 'antd';
|
||||||
|
|
||||||
|
const { Link } = Typography;
|
||||||
|
|
||||||
|
const NotFound: React.FC = () => (
|
||||||
|
<Result
|
||||||
|
status="404"
|
||||||
|
title="404"
|
||||||
|
extra={
|
||||||
|
<Button type="primary">
|
||||||
|
<Link href="/">返回首页</Link>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NotFound;
|
|
@ -14,6 +14,7 @@ import {
|
||||||
Popover,
|
Popover,
|
||||||
Tabs,
|
Tabs,
|
||||||
TablePaginationConfig,
|
TablePaginationConfig,
|
||||||
|
MenuProps,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
|
@ -51,6 +52,7 @@ import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
||||||
import { SharedContext } from '@/layouts';
|
import { SharedContext } from '@/layouts';
|
||||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
import { getCommandScript } from '@/utils';
|
import { getCommandScript } from '@/utils';
|
||||||
|
import { ColumnProps } from 'antd/lib/table';
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
@ -82,9 +84,23 @@ enum OperationPath {
|
||||||
'unpin',
|
'unpin',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICrontab {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
schedule: string;
|
||||||
|
id: number;
|
||||||
|
status: number;
|
||||||
|
isDisabled?: 1 | 0;
|
||||||
|
isPinned?: 1 | 0;
|
||||||
|
labels?: string[];
|
||||||
|
last_running_time?: number;
|
||||||
|
last_execution_time?: number;
|
||||||
|
nextRunTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
const Crontab = () => {
|
const Crontab = () => {
|
||||||
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||||
const columns: any = [
|
const columns: ColumnProps<ICrontab>[] = [
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
|
@ -112,7 +128,6 @@ const Crontab = () => {
|
||||||
style={{ cursor: 'point' }}
|
style={{ cursor: 'point' }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setSearchValue(`label:${label}`);
|
|
||||||
setSearchText(`label:${label}`);
|
setSearchText(`label:${label}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -138,7 +153,7 @@ const Crontab = () => {
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
sorter: {
|
sorter: {
|
||||||
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name),
|
compare: (a, b) => a?.name?.localeCompare(b?.name),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -147,7 +162,7 @@ const Crontab = () => {
|
||||||
key: 'command',
|
key: 'command',
|
||||||
width: 300,
|
width: 300,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (text: string, record: any) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<Paragraph
|
<Paragraph
|
||||||
style={{
|
style={{
|
||||||
|
@ -178,7 +193,7 @@ const Crontab = () => {
|
||||||
width: 110,
|
width: 110,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
sorter: {
|
sorter: {
|
||||||
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
|
compare: (a, b) => a.schedule.localeCompare(b.schedule),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -188,11 +203,11 @@ const Crontab = () => {
|
||||||
key: 'last_execution_time',
|
key: 'last_execution_time',
|
||||||
width: 150,
|
width: 150,
|
||||||
sorter: {
|
sorter: {
|
||||||
compare: (a: any, b: any) => {
|
compare: (a, b) => {
|
||||||
return a.last_execution_time - b.last_execution_time;
|
return (a.last_execution_time || 0) - (b.last_execution_time || 0);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text, record) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
const language = navigator.language || navigator.languages[0];
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
@ -222,7 +237,7 @@ const Crontab = () => {
|
||||||
return a.last_running_time - b.last_running_time;
|
return a.last_running_time - b.last_running_time;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text, record) => {
|
||||||
return record.last_running_time
|
return record.last_running_time
|
||||||
? diffTime(record.last_running_time)
|
? diffTime(record.last_running_time)
|
||||||
: '-';
|
: '-';
|
||||||
|
@ -237,7 +252,7 @@ const Crontab = () => {
|
||||||
return a.nextRunTime - b.nextRunTime;
|
return a.nextRunTime - b.nextRunTime;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text, record) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
const language = navigator.language || navigator.languages[0];
|
||||||
return record.nextRunTime
|
return record.nextRunTime
|
||||||
.toLocaleString(language, {
|
.toLocaleString(language, {
|
||||||
|
@ -271,14 +286,14 @@ const Crontab = () => {
|
||||||
value: 3,
|
value: 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onFilter: (value: number, record: any) => {
|
onFilter: (value, record) => {
|
||||||
if (record.isDisabled && record.status !== 0) {
|
if (record.isDisabled && record.status !== 0) {
|
||||||
return value === 2;
|
return value === 2;
|
||||||
} else {
|
} else {
|
||||||
return record.status === value;
|
return record.status === value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => (
|
render: (text, record) => (
|
||||||
<>
|
<>
|
||||||
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
|
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
|
||||||
<>
|
<>
|
||||||
|
@ -315,7 +330,7 @@ const Crontab = () => {
|
||||||
key: 'action',
|
key: 'action',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text, record, index) => {
|
||||||
const isPc = !isPhone;
|
const isPc = !isPhone;
|
||||||
return (
|
return (
|
||||||
<Space size="middle">
|
<Space size="middle">
|
||||||
|
@ -378,7 +393,6 @@ const Crontab = () => {
|
||||||
const [viewConf, setViewConf] = useState<any>();
|
const [viewConf, setViewConf] = useState<any>();
|
||||||
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
|
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
|
||||||
const [detailCron, setDetailCron] = useState<any>();
|
const [detailCron, setDetailCron] = useState<any>();
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
const [total, setTotal] = useState<number>();
|
const [total, setTotal] = useState<number>();
|
||||||
const [isCreateViewModalVisible, setIsCreateViewModalVisible] =
|
const [isCreateViewModalVisible, setIsCreateViewModalVisible] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
@ -671,15 +685,13 @@ const Crontab = () => {
|
||||||
arrow={{ pointAtCenter: true }}
|
arrow={{ pointAtCenter: true }}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
overlay={
|
menu={{
|
||||||
<Menu
|
items: getMenuItems(record),
|
||||||
items={getMenuItems(record)}
|
onClick: ({ key, domEvent }) => {
|
||||||
onClick={({ key, domEvent }) => {
|
domEvent.stopPropagation();
|
||||||
domEvent.stopPropagation();
|
action(key, record, index);
|
||||||
action(key, record, index);
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<a onClick={(e) => e.stopPropagation()}>
|
<a onClick={(e) => e.stopPropagation()}>
|
||||||
<EllipsisOutlined />
|
<EllipsisOutlined />
|
||||||
|
@ -876,41 +888,39 @@ const Crontab = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const menu = (
|
const menu: MenuProps = {
|
||||||
<Menu
|
onClick: ({ key, domEvent }) => {
|
||||||
onClick={({ key, domEvent }) => {
|
domEvent.stopPropagation();
|
||||||
domEvent.stopPropagation();
|
viewAction(key);
|
||||||
viewAction(key);
|
},
|
||||||
}}
|
items: [
|
||||||
items={[
|
...[...enabledCronViews].slice(4).map((x) => ({
|
||||||
...[...enabledCronViews].slice(4).map((x) => ({
|
label: (
|
||||||
label: (
|
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<span>{x.name}</span>
|
||||||
<span>{x.name}</span>
|
{viewConf?.id === x.id && (
|
||||||
{viewConf?.id === x.id && (
|
<CheckOutlined style={{ color: '#1890ff' }} />
|
||||||
<CheckOutlined style={{ color: '#1890ff' }} />
|
)}
|
||||||
)}
|
</Space>
|
||||||
</Space>
|
),
|
||||||
),
|
key: x.id,
|
||||||
key: x.id,
|
icon: <UnorderedListOutlined />,
|
||||||
icon: <UnorderedListOutlined />,
|
})),
|
||||||
})),
|
{
|
||||||
{
|
type: 'divider' as 'group',
|
||||||
type: 'divider',
|
},
|
||||||
},
|
{
|
||||||
{
|
label: '新建视图',
|
||||||
label: '新建视图',
|
key: 'new',
|
||||||
key: 'new',
|
icon: <PlusOutlined />,
|
||||||
icon: <PlusOutlined />,
|
},
|
||||||
},
|
{
|
||||||
{
|
label: '视图管理',
|
||||||
label: '视图管理',
|
key: 'manage',
|
||||||
key: 'manage',
|
icon: <SettingOutlined />,
|
||||||
icon: <SettingOutlined />,
|
},
|
||||||
},
|
],
|
||||||
]}
|
};
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const getCronViews = () => {
|
const getCronViews = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -945,8 +955,6 @@ const Crontab = () => {
|
||||||
enterButton
|
enterButton
|
||||||
allowClear
|
allowClear
|
||||||
loading={loading}
|
loading={loading}
|
||||||
value={searchValue}
|
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
/>,
|
/>,
|
||||||
<Button key="2" type="primary" onClick={() => addCron()}>
|
<Button key="2" type="primary" onClick={() => addCron()}>
|
||||||
|
@ -964,7 +972,7 @@ const Crontab = () => {
|
||||||
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
|
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
|
||||||
tabBarExtraContent={
|
tabBarExtraContent={
|
||||||
<Dropdown
|
<Dropdown
|
||||||
overlay={menu}
|
menu={menu}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
overlayStyle={{ minWidth: 200 }}
|
overlayStyle={{ minWidth: 200 }}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
|
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
|
@ -34,7 +34,6 @@ const CronLogModal = ({
|
||||||
const [loading, setLoading] = useState<any>(true);
|
const [loading, setLoading] = useState<any>(true);
|
||||||
const [executing, setExecuting] = useState<any>(true);
|
const [executing, setExecuting] = useState<any>(true);
|
||||||
const [isPhone, setIsPhone] = useState(false);
|
const [isPhone, setIsPhone] = useState(false);
|
||||||
const [theme, setTheme] = useState<string>('');
|
|
||||||
|
|
||||||
const getCronLog = (isFirst?: boolean) => {
|
const getCronLog = (isFirst?: boolean) => {
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
|
@ -49,10 +48,14 @@ const CronLogModal = ({
|
||||||
) {
|
) {
|
||||||
const log = data as string;
|
const log = data as string;
|
||||||
setValue(log || '暂无日志');
|
setValue(log || '暂无日志');
|
||||||
setExecuting(
|
const hasNext = log && !logEnded(log) && !log.includes('重启面板');
|
||||||
log && !logEnded(log) && !log.includes('重启面板'),
|
setExecuting(hasNext);
|
||||||
);
|
setTimeout(() => {
|
||||||
if (log && !logEnded(log) && !log.includes('重启面板')) {
|
document
|
||||||
|
.querySelector('#log-flag')!
|
||||||
|
.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, 1000);
|
||||||
|
if (hasNext) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getCronLog();
|
getCronLog();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
@ -155,6 +158,7 @@ const CronLogModal = ({
|
||||||
{value}
|
{value}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
|
<div id="log-flag"></div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -80,7 +80,7 @@ const CronModal = ({
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
autoSize={true}
|
autoSize={true}
|
||||||
placeholder="请输入要执行的命令"
|
placeholder="使用 task 命令运行脚本或其他任意 Linux 可执行命令"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|
6
src/pages/env/index.less
vendored
6
src/pages/env/index.less
vendored
|
@ -5,3 +5,9 @@ tr.drop-over-downward td {
|
||||||
tr.drop-over-upward td {
|
tr.drop-over-upward td {
|
||||||
border-top: 2px dashed #1890ff;
|
border-top: 2px dashed #1890ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-ellipsis {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
33
src/pages/env/index.tsx
vendored
33
src/pages/env/index.tsx
vendored
|
@ -32,8 +32,8 @@ import { exportJson } from '@/utils/index';
|
||||||
import { useOutletContext } from '@umijs/max';
|
import { useOutletContext } from '@umijs/max';
|
||||||
import { SharedContext } from '@/layouts';
|
import { SharedContext } from '@/layouts';
|
||||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
|
import Copy from '../../components/copy';
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
enum Status {
|
enum Status {
|
||||||
|
@ -128,17 +128,12 @@ const Env = () => {
|
||||||
width: '35%',
|
width: '35%',
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
return (
|
return (
|
||||||
<Paragraph
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
style={{
|
<Tooltip title={text} placement="topLeft">
|
||||||
wordBreak: 'break-all',
|
<div className="text-ellipsis">{text}</div>
|
||||||
marginBottom: 0,
|
</Tooltip>
|
||||||
textAlign: 'left',
|
<Copy text={text} />
|
||||||
}}
|
</div>
|
||||||
ellipsis={{ tooltip: text, rows: 2 }}
|
|
||||||
copyable
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Paragraph>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -147,6 +142,13 @@ const Env = () => {
|
||||||
dataIndex: 'remarks',
|
dataIndex: 'remarks',
|
||||||
key: 'remarks',
|
key: 'remarks',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
return (
|
||||||
|
<Tooltip title={text} placement="topLeft">
|
||||||
|
<div className="text-ellipsis">{text}</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '更新时间',
|
title: '更新时间',
|
||||||
|
@ -256,7 +258,7 @@ const Env = () => {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [importLoading, setImportLoading] = useState(false);
|
const [importLoading, setImportLoading] = useState(false);
|
||||||
const tableRef = useRef<any>();
|
const tableRef = useRef<any>();
|
||||||
const tableScrollHeight = useTableScrollHeight(tableRef, 59)
|
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
|
||||||
|
|
||||||
const getEnvs = () => {
|
const getEnvs = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -286,7 +288,8 @@ const Env = () => {
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(
|
.put(
|
||||||
`${config.apiPrefix}envs/${record.status === Status.已禁用 ? 'enable' : 'disable'
|
`${config.apiPrefix}envs/${
|
||||||
|
record.status === Status.已禁用 ? 'enable' : 'disable'
|
||||||
}`,
|
}`,
|
||||||
{
|
{
|
||||||
data: [record.id],
|
data: [record.id],
|
||||||
|
|
|
@ -9,20 +9,37 @@ import './index.less';
|
||||||
import { SharedContext } from '@/layouts';
|
import { SharedContext } from '@/layouts';
|
||||||
|
|
||||||
const Error = () => {
|
const Error = () => {
|
||||||
const { user, theme } = useOutletContext<SharedContext>();
|
const { user, theme, reloadUser } = useOutletContext<SharedContext>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState('暂无日志');
|
const [data, setData] = useState('暂无日志');
|
||||||
|
|
||||||
const getLog = () => {
|
const getTimes = () => {
|
||||||
setLoading(true);
|
return parseInt(localStorage.getItem('error_retry_times') || '0', 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
let times = getTimes();
|
||||||
|
|
||||||
|
const getLog = (needLoading: boolean = true) => {
|
||||||
|
needLoading && setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}public/panel/log`)
|
.get(`${config.apiPrefix}public/panel/log`)
|
||||||
.then(({ code, data }) => {
|
.then(({ code, data }) => {
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
setData(data);
|
setData(data);
|
||||||
|
if (!data) {
|
||||||
|
times = getTimes();
|
||||||
|
if (times > 5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem('error_retry_times', `${times + 1}`);
|
||||||
|
setTimeout(() => {
|
||||||
|
reloadUser();
|
||||||
|
getLog(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => needLoading && setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -39,7 +56,7 @@ const Error = () => {
|
||||||
<div className="error-wrapper">
|
<div className="error-wrapper">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<PageLoading />
|
<PageLoading />
|
||||||
) : (
|
) : data ? (
|
||||||
<Terminal
|
<Terminal
|
||||||
name="服务错误"
|
name="服务错误"
|
||||||
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
|
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
|
||||||
|
@ -55,6 +72,10 @@ const Error = () => {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
) : times > 5 ? (
|
||||||
|
<>服务启动超时,请手动进入容器执行 ql -l check 后刷新再试</>
|
||||||
|
) : (
|
||||||
|
<PageLoading tip="启动中,请稍后..." />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -101,7 +101,7 @@ const EditModal = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
if (!cNode || !cNode.title) {
|
if (!cNode || !cNode.title || !currentPid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
request
|
request
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Menu,
|
Menu,
|
||||||
Empty,
|
Empty,
|
||||||
|
MenuProps,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
|
@ -66,7 +67,8 @@ const Script = () => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
|
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
|
||||||
const [isRenameFileModalVisible, setIsRenameFileModalVisible] = useState(false);
|
const [isRenameFileModalVisible, setIsRenameFileModalVisible] =
|
||||||
|
useState(false);
|
||||||
const [currentNode, setCurrentNode] = useState<any>();
|
const [currentNode, setCurrentNode] = useState<any>();
|
||||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
@ -293,12 +295,12 @@ const Script = () => {
|
||||||
|
|
||||||
const renameFile = () => {
|
const renameFile = () => {
|
||||||
setIsRenameFileModalVisible(true);
|
setIsRenameFileModalVisible(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleRenameFileCancel = () => {
|
const handleRenameFileCancel = () => {
|
||||||
setIsRenameFileModalVisible(false);
|
setIsRenameFileModalVisible(false);
|
||||||
getScripts(false);
|
getScripts(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
const addFile = () => {
|
const addFile = () => {
|
||||||
setIsAddFileModalVisible(true);
|
setIsAddFileModalVisible(true);
|
||||||
|
@ -402,46 +404,44 @@ const Script = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const menu = isEditing ? (
|
const menu: MenuProps = isEditing
|
||||||
<Menu
|
? {
|
||||||
items={[
|
items: [
|
||||||
{ label: '保存', key: 'save', icon: <PlusOutlined /> },
|
{ label: '保存', key: 'save', icon: <PlusOutlined /> },
|
||||||
{ label: '退出编辑', key: 'exit', icon: <EditOutlined /> },
|
{ label: '退出编辑', key: 'exit', icon: <EditOutlined /> },
|
||||||
]}
|
],
|
||||||
onClick={({ key, domEvent }) => {
|
onClick: ({ key, domEvent }) => {
|
||||||
domEvent.stopPropagation();
|
domEvent.stopPropagation();
|
||||||
action(key);
|
action(key);
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Menu
|
|
||||||
items={[
|
|
||||||
{ label: '新建', key: 'add', icon: <PlusOutlined /> },
|
|
||||||
{
|
|
||||||
label: '编辑',
|
|
||||||
key: 'edit',
|
|
||||||
icon: <EditOutlined />,
|
|
||||||
disabled: !select,
|
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
label: '重命名',
|
: {
|
||||||
key: 'rename',
|
items: [
|
||||||
icon: <IconFont type="ql-icon-rename" />,
|
{ label: '新建', key: 'add', icon: <PlusOutlined /> },
|
||||||
disabled: !select,
|
{
|
||||||
|
label: '编辑',
|
||||||
|
key: 'edit',
|
||||||
|
icon: <EditOutlined />,
|
||||||
|
disabled: !select,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '重命名',
|
||||||
|
key: 'rename',
|
||||||
|
icon: <IconFont type="ql-icon-rename" />,
|
||||||
|
disabled: !select,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
key: 'delete',
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
disabled: !select,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onClick: ({ key, domEvent }) => {
|
||||||
|
domEvent.stopPropagation();
|
||||||
|
menuAction(key);
|
||||||
},
|
},
|
||||||
{
|
};
|
||||||
label: '删除',
|
|
||||||
key: 'delete',
|
|
||||||
icon: <DeleteOutlined />,
|
|
||||||
disabled: !select,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onClick={({ key, domEvent }) => {
|
|
||||||
domEvent.stopPropagation();
|
|
||||||
menuAction(key);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
|
@ -464,7 +464,7 @@ const Script = () => {
|
||||||
allowClear
|
allowClear
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>,
|
/>,
|
||||||
<Dropdown overlay={menu} trigger={['click']}>
|
<Dropdown menu={menu} trigger={['click']}>
|
||||||
<Button type="primary" icon={<EllipsisOutlined />} />
|
<Button type="primary" icon={<EllipsisOutlined />} />
|
||||||
</Dropdown>,
|
</Dropdown>,
|
||||||
]
|
]
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
import config from '@/utils/config';
|
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 { request } from '@/utils/http';
|
||||||
import * as DarkReader from '@umijs/ssr-darkreader';
|
|
||||||
import AppModal from './appModal';
|
import AppModal from './appModal';
|
||||||
import {
|
import {
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
@ -27,18 +26,13 @@ import {
|
||||||
import SecuritySettings from './security';
|
import SecuritySettings from './security';
|
||||||
import LoginLog from './loginLog';
|
import LoginLog from './loginLog';
|
||||||
import NotificationSetting from './notification';
|
import NotificationSetting from './notification';
|
||||||
import CheckUpdate from './checkUpdate';
|
import Other from './other';
|
||||||
import About from './about';
|
import About from './about';
|
||||||
import { useOutletContext } from '@umijs/max';
|
import { useOutletContext } from '@umijs/max';
|
||||||
import { SharedContext } from '@/layouts';
|
import { SharedContext } from '@/layouts';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const optionsWithDisabled = [
|
|
||||||
{ label: '亮色', value: 'light' },
|
|
||||||
{ label: '暗色', value: 'dark' },
|
|
||||||
{ label: '跟随系统', value: 'auto' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const Setting = () => {
|
const Setting = () => {
|
||||||
const {
|
const {
|
||||||
|
@ -121,37 +115,12 @@ const Setting = () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
|
||||||
const [dataSource, setDataSource] = useState<any[]>([]);
|
const [dataSource, setDataSource] = useState<any[]>([]);
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [editedApp, setEditedApp] = useState<any>();
|
const [editedApp, setEditedApp] = useState<any>();
|
||||||
const [tabActiveKey, setTabActiveKey] = useState('security');
|
const [tabActiveKey, setTabActiveKey] = useState('security');
|
||||||
const [loginLogData, setLoginLogData] = useState<any[]>([]);
|
const [loginLogData, setLoginLogData] = useState<any[]>([]);
|
||||||
const [notificationInfo, setNotificationInfo] = useState<any>();
|
const [notificationInfo, setNotificationInfo] = useState<any>();
|
||||||
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number>();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const {
|
|
||||||
enable: enableDarkMode,
|
|
||||||
disable: disableDarkMode,
|
|
||||||
exportGeneratedCSS: collectCSS,
|
|
||||||
setFetchMethod,
|
|
||||||
auto: followSystemColorScheme,
|
|
||||||
} = DarkReader || {};
|
|
||||||
|
|
||||||
const themeChange = (e: any) => {
|
|
||||||
const _theme = e.target.value;
|
|
||||||
localStorage.setItem('qinglong_dark_theme', e.target.value);
|
|
||||||
setFetchMethod(fetch);
|
|
||||||
|
|
||||||
if (_theme === 'dark') {
|
|
||||||
enableDarkMode({});
|
|
||||||
} else if (_theme === 'light') {
|
|
||||||
disableDarkMode();
|
|
||||||
} else {
|
|
||||||
followSystemColorScheme({});
|
|
||||||
}
|
|
||||||
reloadTheme();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getApps = () => {
|
const getApps = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -276,8 +245,6 @@ const Setting = () => {
|
||||||
getLoginLog();
|
getLoginLog();
|
||||||
} else if (activeKey === 'notification') {
|
} else if (activeKey === 'notification') {
|
||||||
getNotification();
|
getNotification();
|
||||||
} else if (activeKey === 'other') {
|
|
||||||
getLogRemoveFrequency();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -294,37 +261,6 @@ const Setting = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLogRemoveFrequency = () => {
|
|
||||||
request
|
|
||||||
.get(`${config.apiPrefix}system/log/remove`)
|
|
||||||
.then(({ code, data }) => {
|
|
||||||
if (code === 200 && data.info) {
|
|
||||||
const { frequency } = data.info;
|
|
||||||
setLogRemoveFrequency(frequency);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error: any) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateRemoveLogFrequency = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
request
|
|
||||||
.put(`${config.apiPrefix}system/log/remove`, {
|
|
||||||
data: { frequency: logRemoveFrequency },
|
|
||||||
})
|
|
||||||
.then(({ code, data }) => {
|
|
||||||
if (code === 200) {
|
|
||||||
message.success('更新成功');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error: any) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
className="ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container"
|
className="ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container"
|
||||||
|
@ -382,46 +318,11 @@ const Setting = () => {
|
||||||
key: 'other',
|
key: 'other',
|
||||||
label: '其他设置',
|
label: '其他设置',
|
||||||
children: (
|
children: (
|
||||||
<Form layout="vertical" form={form}>
|
<Other
|
||||||
<Form.Item
|
reloadTheme={reloadTheme}
|
||||||
label="主题设置"
|
socketMessage={socketMessage}
|
||||||
name="theme"
|
systemInfo={systemInfo}
|
||||||
initialValue={defaultTheme}
|
/>
|
||||||
>
|
|
||||||
<Radio.Group
|
|
||||||
options={optionsWithDisabled}
|
|
||||||
onChange={themeChange}
|
|
||||||
value={defaultTheme}
|
|
||||||
optionType="button"
|
|
||||||
buttonStyle="solid"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="日志删除频率"
|
|
||||||
name="frequency"
|
|
||||||
tooltip="每x天自动删除x天以前的日志"
|
|
||||||
>
|
|
||||||
<Input.Group compact>
|
|
||||||
<InputNumber
|
|
||||||
addonBefore="每"
|
|
||||||
addonAfter="天"
|
|
||||||
style={{ width: 150 }}
|
|
||||||
min={0}
|
|
||||||
value={logRemoveFrequency}
|
|
||||||
onChange={(value) => setLogRemoveFrequency(value)}
|
|
||||||
/>
|
|
||||||
<Button type="primary" onClick={updateRemoveLogFrequency}>
|
|
||||||
确认
|
|
||||||
</Button>
|
|
||||||
</Input.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="检查更新" name="update">
|
|
||||||
<CheckUpdate
|
|
||||||
systemInfo={systemInfo}
|
|
||||||
socketMessage={socketMessage}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
120
src/pages/setting/other.tsx
Normal file
120
src/pages/setting/other.tsx
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button, InputNumber, Form, Radio, message, Input } from 'antd';
|
||||||
|
import * as DarkReader from '@umijs/ssr-darkreader';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import CheckUpdate from './checkUpdate';
|
||||||
|
import { SharedContext } from '@/layouts';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const optionsWithDisabled = [
|
||||||
|
{ label: '亮色', value: 'light' },
|
||||||
|
{ label: '暗色', value: 'dark' },
|
||||||
|
{ label: '跟随系统', value: 'auto' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const Other = ({
|
||||||
|
systemInfo,
|
||||||
|
socketMessage,
|
||||||
|
reloadTheme,
|
||||||
|
}: Pick<SharedContext, 'socketMessage' | 'reloadTheme' | 'systemInfo'>) => {
|
||||||
|
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
||||||
|
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number | null>();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const {
|
||||||
|
enable: enableDarkMode,
|
||||||
|
disable: disableDarkMode,
|
||||||
|
exportGeneratedCSS: collectCSS,
|
||||||
|
setFetchMethod,
|
||||||
|
auto: followSystemColorScheme,
|
||||||
|
} = DarkReader || {};
|
||||||
|
|
||||||
|
const themeChange = (e: any) => {
|
||||||
|
const _theme = e.target.value;
|
||||||
|
localStorage.setItem('qinglong_dark_theme', e.target.value);
|
||||||
|
setFetchMethod(fetch);
|
||||||
|
|
||||||
|
if (_theme === 'dark') {
|
||||||
|
enableDarkMode({});
|
||||||
|
} else if (_theme === 'light') {
|
||||||
|
disableDarkMode();
|
||||||
|
} else {
|
||||||
|
followSystemColorScheme({});
|
||||||
|
}
|
||||||
|
reloadTheme();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogRemoveFrequency = () => {
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}system/log/remove`)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200 && data.info) {
|
||||||
|
const { frequency } = data.info;
|
||||||
|
setLogRemoveFrequency(frequency);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRemoveLogFrequency = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
request
|
||||||
|
.put(`${config.apiPrefix}system/log/remove`, {
|
||||||
|
data: { frequency: logRemoveFrequency },
|
||||||
|
})
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
message.success('更新成功');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLogRemoveFrequency();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form layout="vertical" form={form}>
|
||||||
|
<Form.Item label="主题设置" name="theme" initialValue={defaultTheme}>
|
||||||
|
<Radio.Group
|
||||||
|
options={optionsWithDisabled}
|
||||||
|
onChange={themeChange}
|
||||||
|
value={defaultTheme}
|
||||||
|
optionType="button"
|
||||||
|
buttonStyle="solid"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="日志删除频率"
|
||||||
|
name="frequency"
|
||||||
|
tooltip="每x天自动删除x天以前的日志"
|
||||||
|
>
|
||||||
|
<Input.Group compact>
|
||||||
|
<InputNumber
|
||||||
|
addonBefore="每"
|
||||||
|
addonAfter="天"
|
||||||
|
style={{ width: 150 }}
|
||||||
|
min={0}
|
||||||
|
value={logRemoveFrequency}
|
||||||
|
onChange={(value) => setLogRemoveFrequency(value)}
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={updateRemoveLogFrequency}>
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
</Input.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="检查更新" name="update">
|
||||||
|
<CheckUpdate systemInfo={systemInfo} socketMessage={socketMessage} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Other;
|
|
@ -243,11 +243,10 @@ const Subscription = () => {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(20);
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
||||||
const [logSubscription, setLogSubscription] = useState<any>();
|
const [logSubscription, setLogSubscription] = useState<any>();
|
||||||
const tableRef = useRef<any>();
|
const tableRef = useRef<any>();
|
||||||
const tableScrollHeight = useTableScrollHeight(tableRef)
|
const tableScrollHeight = useTableScrollHeight(tableRef);
|
||||||
|
|
||||||
const runSubscription = (record: any, index: number) => {
|
const runSubscription = (record: any, index: number) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
|
@ -428,28 +427,26 @@ const Subscription = () => {
|
||||||
arrow={{ pointAtCenter: true }}
|
arrow={{ pointAtCenter: true }}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
overlay={
|
menu={{
|
||||||
<Menu
|
items: [
|
||||||
items={[
|
{ label: '编辑', key: 'edit', icon: <EditOutlined /> },
|
||||||
{ label: '编辑', key: 'edit', icon: <EditOutlined /> },
|
{
|
||||||
{
|
label: record.is_disabled === 1 ? '启用' : '禁用',
|
||||||
label: record.is_disabled === 1 ? '启用' : '禁用',
|
key: 'enableOrDisable',
|
||||||
key: 'enableOrDisable',
|
icon:
|
||||||
icon:
|
record.is_disabled === 1 ? (
|
||||||
record.is_disabled === 1 ? (
|
<CheckCircleOutlined />
|
||||||
<CheckCircleOutlined />
|
) : (
|
||||||
) : (
|
<StopOutlined />
|
||||||
<StopOutlined />
|
),
|
||||||
),
|
},
|
||||||
},
|
{ label: '删除', key: 'delete', icon: <DeleteOutlined /> },
|
||||||
{ label: '删除', key: 'delete', icon: <DeleteOutlined /> },
|
],
|
||||||
]}
|
onClick: ({ key, domEvent }) => {
|
||||||
onClick={({ key, domEvent }) => {
|
domEvent.stopPropagation();
|
||||||
domEvent.stopPropagation();
|
action(key, record, index);
|
||||||
action(key, record, index);
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<a onClick={(e) => e.stopPropagation()}>
|
<a onClick={(e) => e.stopPropagation()}>
|
||||||
<EllipsisOutlined />
|
<EllipsisOutlined />
|
||||||
|
@ -553,8 +550,6 @@ const Subscription = () => {
|
||||||
enterButton
|
enterButton
|
||||||
allowClear
|
allowClear
|
||||||
loading={loading}
|
loading={loading}
|
||||||
value={searchValue}
|
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
/>,
|
/>,
|
||||||
<Button key="2" type="primary" onClick={() => addSubscription()}>
|
<Button key="2" type="primary" onClick={() => addSubscription()}>
|
||||||
|
|
|
@ -296,7 +296,7 @@ export default {
|
||||||
documentTitleMap: {
|
documentTitleMap: {
|
||||||
'/login': '登录',
|
'/login': '登录',
|
||||||
'/initialization': '初始化',
|
'/initialization': '初始化',
|
||||||
'/cron': '定时任务',
|
'/crontab': '定时任务',
|
||||||
'/env': '环境变量',
|
'/env': '环境变量',
|
||||||
'/subscription': '订阅管理',
|
'/subscription': '订阅管理',
|
||||||
'/config': '配置文件',
|
'/config': '配置文件',
|
||||||
|
@ -305,6 +305,7 @@ export default {
|
||||||
'/log': '日志管理',
|
'/log': '日志管理',
|
||||||
'/setting': '系统设置',
|
'/setting': '系统设置',
|
||||||
'/error': '错误日志',
|
'/error': '错误日志',
|
||||||
|
'/dependence': '依赖管理',
|
||||||
},
|
},
|
||||||
dependenceTypes: ['nodejs', 'python3', 'linux'],
|
dependenceTypes: ['nodejs', 'python3', 'linux'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -56,7 +56,6 @@ _request.interceptors.request.use((url, options) => {
|
||||||
_request.interceptors.response.use(async (response) => {
|
_request.interceptors.response.use(async (response) => {
|
||||||
const responseStatus = response.status;
|
const responseStatus = response.status;
|
||||||
if ([502, 504].includes(responseStatus)) {
|
if ([502, 504].includes(responseStatus)) {
|
||||||
message.error('服务异常,请稍后刷新!');
|
|
||||||
history.push('/error');
|
history.push('/error');
|
||||||
} else if (responseStatus === 401) {
|
} else if (responseStatus === 401) {
|
||||||
if (history.location.pathname !== '/login') {
|
if (history.location.pathname !== '/login') {
|
||||||
|
|
11
version.yaml
11
version.yaml
|
@ -1,6 +1,7 @@
|
||||||
version: 2.15.4
|
version: 2.15.5
|
||||||
changeLogLink: https://t.me/jiao_long/355
|
changeLogLink: https://t.me/jiao_long/356
|
||||||
changeLog: |
|
changeLog: |
|
||||||
1. 脚本管理支持重命名文件/文件夹
|
1. 修复定时任务命令不以task开头时,不能自动执行
|
||||||
2. 关于增加更新日志查看
|
2. 修改服务异常逻辑
|
||||||
3. 修复定时任务脚本识别和跳转
|
3. 修复退出调试报错
|
||||||
|
4. 修复脚本编辑器加载慢
|
||||||
|
|
Loading…
Reference in New Issue
Block a user