diff --git a/back/loaders/app.ts b/back/loaders/app.ts
index 20c38e27..b2bea108 100644
--- a/back/loaders/app.ts
+++ b/back/loaders/app.ts
@@ -1,14 +1,12 @@
import expressLoader from './express';
-import dependencyInjectorLoader from './dependencyInjector';
+import depInjectorLoader from './depInjector';
import Logger from './logger';
import initData from './initData';
import { Application } from 'express';
import linkDeps from './deps';
export default async ({ expressApp }: { expressApp: Application }) => {
- await dependencyInjectorLoader({
- models: [],
- });
+ await depInjectorLoader();
Logger.info('✌️ Dependency Injector loaded');
await expressLoader({ app: expressApp });
diff --git a/back/loaders/dependencyInjector.ts b/back/loaders/depInjector.ts
similarity index 61%
rename from back/loaders/dependencyInjector.ts
rename to back/loaders/depInjector.ts
index 744057e9..be70cb2c 100644
--- a/back/loaders/dependencyInjector.ts
+++ b/back/loaders/depInjector.ts
@@ -1,12 +1,8 @@
import { Container } from 'typedi';
import LoggerInstance from './logger';
-export default ({ models }: { models: { name: string; model: any }[] }) => {
+export default () => {
try {
- models.forEach((m) => {
- Container.set(m.name, m.model);
- });
-
Container.set('logger', LoggerInstance);
} catch (e) {
LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);
diff --git a/package.json b/package.json
index 78e1c970..c1c73048 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"schedule": "npm run build:back && node static/build/schedule.js",
"public": "npm run build:back && node static/build/public.js",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
- "prepare": "umi generate tmp 2>/dev/null || true",
+ "postinstall": "umi generate tmp 2>/dev/null || true",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
diff --git a/src/components/iconfont.tsx b/src/components/iconfont.tsx
index 620f49ed..ede96ae4 100644
--- a/src/components/iconfont.tsx
+++ b/src/components/iconfont.tsx
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
- scriptUrl: ['//at.alicdn.com/t/font_3354854_pk18p04ny1a.js'],
+ scriptUrl: ['//at.alicdn.com/t/font_3354854_ds8pa06q1qa.js'],
});
export default IconFont;
diff --git a/src/layouts/defaultProps.tsx b/src/layouts/defaultProps.tsx
index 93766082..5856dc31 100644
--- a/src/layouts/defaultProps.tsx
+++ b/src/layouts/defaultProps.tsx
@@ -9,6 +9,7 @@ import {
ControlOutlined,
ContainerOutlined,
} from '@ant-design/icons';
+import IconFont from '@/components/iconfont';
export default {
route: {
@@ -34,43 +35,49 @@ export default {
{
path: '/crontab',
name: '定时任务',
- icon: ,
+ icon: ,
component: '@/pages/crontab/index',
},
+ {
+ path: '/subscription',
+ name: '订阅管理',
+ icon: ,
+ component: '@/pages/subscription/index',
+ },
{
path: '/env',
name: '环境变量',
- icon: ,
+ icon: ,
component: '@/pages/env/index',
},
{
path: '/config',
name: '配置文件',
- icon: ,
+ icon: ,
component: '@/pages/config/index',
},
{
path: '/script',
name: '脚本管理',
- icon: ,
+ icon: ,
component: '@/pages/script/index',
},
{
path: '/dependence',
name: '依赖管理',
- icon: ,
+ icon: ,
component: '@/pages/dependence/index',
},
{
path: '/diff',
name: '对比工具',
- icon: ,
+ icon: ,
component: '@/pages/diff/index',
},
{
path: '/log',
name: '任务日志',
- icon: ,
+ icon: ,
component: '@/pages/log/index',
},
{
diff --git a/src/pages/crontab/detail.tsx b/src/pages/crontab/detail.tsx
index 0b30686e..93d82060 100644
--- a/src/pages/crontab/detail.tsx
+++ b/src/pages/crontab/detail.tsx
@@ -396,8 +396,8 @@ const CronDetailModal = ({
}
@@ -412,8 +412,8 @@ const CronDetailModal = ({
}
diff --git a/src/pages/subscription/index.less b/src/pages/subscription/index.less
new file mode 100644
index 00000000..e69de29b
diff --git a/src/pages/subscription/index.tsx b/src/pages/subscription/index.tsx
new file mode 100644
index 00000000..5eac2df0
--- /dev/null
+++ b/src/pages/subscription/index.tsx
@@ -0,0 +1,445 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Button,
+ message,
+ Modal,
+ Table,
+ Tag,
+ Space,
+ Dropdown,
+ Menu,
+ Typography,
+ Input,
+} from 'antd';
+import {
+ ClockCircleOutlined,
+ Loading3QuartersOutlined,
+ CloseCircleOutlined,
+ EllipsisOutlined,
+ CheckCircleOutlined,
+ EditOutlined,
+ StopOutlined,
+ DeleteOutlined,
+} from '@ant-design/icons';
+import config from '@/utils/config';
+import { PageContainer } from '@ant-design/pro-layout';
+import { request } from '@/utils/http';
+import SubscriptionModal from './modal';
+import { getTableScroll } from '@/utils/index';
+import { history } from 'umi';
+import './index.less';
+
+const { Text, Paragraph } = Typography;
+const { Search } = Input;
+
+export enum SubscriptionStatus {
+ 'running',
+ 'idle',
+ 'disabled',
+ 'queued',
+}
+
+const Subscription = ({ headerStyle, isPhone, theme }: any) => {
+ const columns: any = [
+ {
+ title: '订阅名',
+ dataIndex: 'name',
+ key: 'name',
+ width: 150,
+ align: 'center' as const,
+ sorter: {
+ compare: (a: any, b: any) => a.name.localeCompare(b.name),
+ multiple: 2,
+ },
+ },
+ {
+ title: '订阅',
+ dataIndex: 'command',
+ key: 'command',
+ width: 250,
+ align: 'center' as const,
+ render: (text: string, record: any) => {
+ return (
+
+ {text}
+
+ );
+ },
+ sorter: {
+ compare: (a: any, b: any) => a.command.localeCompare(b.command),
+ multiple: 3,
+ },
+ },
+ {
+ title: '订阅定时',
+ dataIndex: 'schedule',
+ key: 'schedule',
+ width: 110,
+ align: 'center' as const,
+ sorter: {
+ compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
+ multiple: 1,
+ },
+ },
+ {
+ title: '状态',
+ key: 'status',
+ dataIndex: 'status',
+ align: 'center' as const,
+ width: 85,
+ filters: [
+ {
+ text: '运行中',
+ value: 0,
+ },
+ {
+ text: '空闲中',
+ value: 1,
+ },
+ {
+ text: '已禁用',
+ value: 2,
+ },
+ ],
+ onFilter: (value: number, record: any) => {
+ if (record.isDisabled && record.status !== 0) {
+ return value === 2;
+ } else {
+ return record.status === value;
+ }
+ },
+ render: (text: string, record: any) => (
+ <>
+ {(!record.isDisabled ||
+ record.status !== SubscriptionStatus.idle) && (
+ <>
+ {record.status === SubscriptionStatus.idle && (
+ } color="default">
+ 空闲中
+
+ )}
+ {record.status === SubscriptionStatus.running && (
+ }
+ color="processing"
+ >
+ 运行中
+
+ )}
+ >
+ )}
+ {record.isDisabled === 1 &&
+ record.status === SubscriptionStatus.idle && (
+ } color="error">
+ 已禁用
+
+ )}
+ >
+ ),
+ },
+ {
+ title: '操作',
+ key: 'action',
+ align: 'center' as const,
+ width: 100,
+ render: (text: string, record: any, index: number) => {
+ return (
+
+
+
+ );
+ },
+ },
+ ];
+
+ const [value, setValue] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [editedSubscription, setEditedSubscription] = useState();
+ const [searchText, setSearchText] = useState('');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(20);
+ const [tableScrollHeight, setTableScrollHeight] = useState();
+ const [searchValue, setSearchValue] = useState('');
+
+ const goToScriptManager = (record: any) => {
+ const cmd = record.command.split(' ') as string[];
+ if (cmd[0] === 'task') {
+ if (cmd[1].startsWith('/ql/data/scripts')) {
+ cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
+ }
+
+ let [p, s] = cmd[1].split('/');
+ if (!s) {
+ s = p;
+ p = '';
+ }
+ history.push(`/script?p=${p}&s=${s}`);
+ } else if (cmd[1] === 'repo') {
+ location.href = cmd[2];
+ }
+ };
+
+ const getSubscriptions = () => {
+ setLoading(true);
+ request
+ .get(`${config.apiPrefix}subscriptions?searchValue=${searchText}`)
+ .then((data: any) => {
+ setValue(data.data);
+ setCurrentPage(1);
+ })
+ .finally(() => setLoading(false));
+ };
+
+ const addSubscription = () => {
+ setEditedSubscription(null as any);
+ setIsModalVisible(true);
+ };
+
+ const editSubscription = (record: any, index: number) => {
+ setEditedSubscription(record);
+ setIsModalVisible(true);
+ };
+
+ const delSubscription = (record: any, index: number) => {
+ Modal.confirm({
+ title: '确认删除',
+ content: (
+ <>
+ 确认删除定时订阅{' '}
+
+ {record.name}
+ {' '}
+ 吗
+ >
+ ),
+ onOk() {
+ request
+ .delete(`${config.apiPrefix}subscriptions`, { data: [record.id] })
+ .then((data: any) => {
+ if (data.code === 200) {
+ message.success('删除成功');
+ const result = [...value];
+ const i = result.findIndex((x) => x.id === record.id);
+ result.splice(i, 1);
+ setValue(result);
+ } else {
+ message.error(data);
+ }
+ });
+ },
+ onCancel() {
+ console.log('Cancel');
+ },
+ });
+ };
+
+ const enabledOrDisabledSubscription = (record: any, index: number) => {
+ Modal.confirm({
+ title: `确认${record.isDisabled === 1 ? '启用' : '禁用'}`,
+ content: (
+ <>
+ 确认{record.isDisabled === 1 ? '启用' : '禁用'}
+ 定时订阅{' '}
+
+ {record.name}
+ {' '}
+ 吗
+ >
+ ),
+ onOk() {
+ request
+ .put(
+ `${config.apiPrefix}subscriptions/${
+ record.isDisabled === 1 ? 'enable' : 'disable'
+ }`,
+ {
+ data: [record.id],
+ },
+ )
+ .then((data: any) => {
+ if (data.code === 200) {
+ const newStatus = record.isDisabled === 1 ? 0 : 1;
+ const result = [...value];
+ const i = result.findIndex((x) => x.id === record.id);
+ result.splice(i, 1, {
+ ...record,
+ isDisabled: newStatus,
+ });
+ setValue(result);
+ } else {
+ message.error(data);
+ }
+ });
+ },
+ onCancel() {
+ console.log('Cancel');
+ },
+ });
+ };
+
+ const MoreBtn: React.FC<{
+ record: any;
+ index: number;
+ }> = ({ record, index }) => (
+ {
+ domEvent.stopPropagation();
+ action(key, record, index);
+ }}
+ >
+ }>
+ 编辑
+
+
+ ) : (
+
+ )
+ }
+ >
+ {record.isDisabled === 1 ? '启用' : '禁用'}
+
+ }>
+ 删除
+
+
+ }
+ >
+ e.stopPropagation()}>
+
+
+
+ );
+
+ const action = (key: string | number, record: any, index: number) => {
+ switch (key) {
+ case 'edit':
+ editSubscription(record, index);
+ break;
+ case 'enableOrDisable':
+ enabledOrDisabledSubscription(record, index);
+ break;
+ case 'delete':
+ delSubscription(record, index);
+ break;
+ default:
+ break;
+ }
+ };
+
+ const handleCancel = (subscription?: any) => {
+ setIsModalVisible(false);
+ if (subscription) {
+ handleSubscriptions(subscription);
+ }
+ };
+
+ const onSearch = (value: string) => {
+ setSearchText(value.trim());
+ };
+
+ const handleSubscriptions = (subscription: any) => {
+ const index = value.findIndex((x) => x.id === subscription.id);
+ const result = [...value];
+ if (index === -1) {
+ result.unshift(subscription);
+ } else {
+ result.splice(index, 1, {
+ ...subscription,
+ });
+ }
+ setValue(result);
+ };
+
+ const onPageChange = (page: number, pageSize: number | undefined) => {
+ setCurrentPage(page);
+ setPageSize(pageSize as number);
+ localStorage.setItem('pageSize', pageSize + '');
+ };
+
+ const getRowClassName = (record: any, index: number) => {
+ return record.isPinned
+ ? 'pinned-subscription subscription'
+ : 'subscription';
+ };
+
+ useEffect(() => {
+ getSubscriptions();
+ }, [searchText]);
+
+ useEffect(() => {
+ setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
+ setTimeout(() => {
+ setTableScrollHeight(getTableScroll());
+ });
+ }, []);
+
+ return (
+ setSearchValue(e.target.value)}
+ onSearch={onSearch}
+ />,
+ ,
+ ]}
+ header={{
+ style: headerStyle,
+ }}
+ >
+
+ `第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
+ pageSizeOptions: [20, 100, 500, 1000] as any,
+ }}
+ dataSource={value}
+ rowKey="id"
+ size="middle"
+ scroll={{ x: 1000, y: tableScrollHeight }}
+ loading={loading}
+ rowClassName={getRowClassName}
+ />
+
+
+ );
+};
+
+export default Subscription;
diff --git a/src/pages/subscription/modal.tsx b/src/pages/subscription/modal.tsx
new file mode 100644
index 00000000..90ba243c
--- /dev/null
+++ b/src/pages/subscription/modal.tsx
@@ -0,0 +1,116 @@
+import React, { useEffect, useState } from 'react';
+import { Modal, message, Input, Form, Button } from 'antd';
+import { request } from '@/utils/http';
+import config from '@/utils/config';
+import cron_parser from 'cron-parser';
+import EditableTagGroup from '@/components/tag';
+
+const SubscriptionModal = ({
+ subscription,
+ handleCancel,
+ visible,
+}: {
+ subscription?: any;
+ visible: boolean;
+ handleCancel: (needUpdate?: boolean) => void;
+}) => {
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+
+ const handleOk = async (values: any) => {
+ setLoading(true);
+ const method = subscription ? 'put' : 'post';
+ const payload = { ...values };
+ if (subscription) {
+ payload.id = subscription.id;
+ }
+ try {
+ const { code, data } = await request[method](
+ `${config.apiPrefix}subscriptions`,
+ {
+ data: payload,
+ },
+ );
+ if (code === 200) {
+ message.success(
+ subscription ? '更新Subscription成功' : '新建Subscription成功',
+ );
+ } else {
+ message.error(data);
+ }
+ setLoading(false);
+ handleCancel(data);
+ } catch (error: any) {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ form.resetFields();
+ }, [subscription, visible]);
+
+ return (
+ {
+ form
+ .validateFields()
+ .then((values) => {
+ handleOk(values);
+ })
+ .catch((info) => {
+ console.log('Validate Failed:', info);
+ });
+ }}
+ onCancel={() => handleCancel()}
+ confirmLoading={loading}
+ >
+
+
+
+
+
+
+ {
+ if (!value || cron_parser.parseExpression(value).hasNext()) {
+ return Promise.resolve();
+ } else {
+ return Promise.reject('Subscription表达式格式有误');
+ }
+ },
+ },
+ ]}
+ >
+
+
+
+
+
+
+
+ );
+};
+
+export default SubscriptionModal;
diff --git a/src/utils/config.ts b/src/utils/config.ts
index 9d9b323e..e2d596db 100644
--- a/src/utils/config.ts
+++ b/src/utils/config.ts
@@ -43,6 +43,10 @@ export default {
name: '环境变量',
value: 'envs',
},
+ {
+ name: '订阅管理',
+ value: 'subscriptions',
+ },
{
name: '配置文件',
value: 'configs',
@@ -67,6 +71,7 @@ export default {
scopesMap: {
crons: '定时任务',
envs: '环境变量',
+ subscriptions: '订阅管理',
configs: '配置文件',
scripts: '脚本管理',
logs: '任务日志',
@@ -214,6 +219,7 @@ export default {
'/initialization': '初始化',
'/cron': '定时任务',
'/env': '环境变量',
+ '/subscription': '订阅管理',
'/config': '配置文件',
'/script': '脚本管理',
'/diff': '对比工具',