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': '对比工具',