Compare commits

..

12 Commits

Author SHA1 Message Date
whyour 54d99a4954 更新版本 v2.14.2 2022-09-07 19:30:19 +08:00
whyour 9947594366 更新issue模板 2022-09-07 17:28:57 +08:00
whyour d7ff934336 增加github issue模版 2022-09-07 16:03:15 +08:00
whyour dc27856441 修复定时任务状态筛选 2022-09-07 11:30:11 +08:00
whyour bd28682769 排除认证失败错误上报 2022-09-06 23:57:21 +08:00
whyour 2060e62da4 更新sentry版本 2022-09-06 21:50:03 +08:00
whyour 70eac1e747 修改pnpm安装全局模块命令 2022-09-06 15:53:21 +08:00
whyour 6f5a5fe066 修改定时任务打开详情交互 2022-09-06 14:49:42 +08:00
景大侠 8d24949202 修复#1614 正确解析多级目录的脚本路径 (#1616) 2022-09-06 13:25:12 +08:00
whyour f7b6237355 修复新建任务视图交互 2022-09-06 00:40:06 +08:00
景大侠 daf6f94c51 修复任务视图bug (#1612)
* 修复了视图无排序时无法再次修改的问题

* 修复在视图管理中编辑、新建视图点击确定后不能关闭页面的问题

* 修复#1611 避免查询条件被覆盖

* 修复视图筛选不能正确处理`不包含`
2022-09-06 00:25:05 +08:00
whyour 687d16e21b 增加npm镜像源配置 2022-09-06 00:21:53 +08:00
19 changed files with 291 additions and 87 deletions
+42
View File
@@ -0,0 +1,42 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve
body:
- type: input
id: version
attributes:
label: Qinglong version
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: |
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
placeholder: Steps to reproduce
validations:
required: true
- type: textarea
id: expected
attributes:
label: What is expected?
validations:
required: true
- type: textarea
id: actually-happening
attributes:
label: What is actually happening?
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
- type: textarea
id: additional-comments
attributes:
label: Any additional comments?
description: e.g. some background/context of how you ran into this bug.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Telegram Chat
url: https://t.me/jiao_long
about: Ask questions and discuss with other Qinglong users in real time.
- name: Questions & Discussions
url: https://github.com/whyour/qinglong/discussions/new?category=q-a
about: Use GitHub discussions for message-board style questions and discussions.
@@ -0,0 +1,40 @@
name: "\U0001F680 New feature proposal"
description: Suggest an idea for this project
labels: [":sparkles: feature request"]
body:
- type: markdown
attributes:
value: |
Thanks for your interest in the project and taking the time to fill out this feature report!
- type: textarea
id: feature-description
attributes:
label: Clear and concise description of the problem
description: "Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?"
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested solution
description: "In module [xy] we could provide following implementation..."
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Alternative
description: Clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Any other context or screenshots about the feature request here.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
required: true
+31
View File
@@ -0,0 +1,31 @@
## PR Type
What kind of change does this PR introduce?
<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Other... Please describe:
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
## Does this PR introduce a breaking change?
- [ ] Yes
- [ ] No
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information
+7
View File
@@ -0,0 +1,7 @@
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: |
💖 Thanks for opening this pull request! 💖
Please be patient and we will get back to you as soon as we can.
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request! 🎉🎉🎉
+2 -2
View File
@@ -28,7 +28,7 @@ export default (app: Router) => {
celebrate({
body: Joi.object({
name: Joi.string().required(),
sorts: Joi.array().optional(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
}),
}),
@@ -49,7 +49,7 @@ export default (app: Router) => {
body: Joi.object({
name: Joi.string().required(),
id: Joi.number().required(),
sorts: Joi.array().optional(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
}),
}),
+19 -7
View File
@@ -3,7 +3,7 @@ import bodyParser from 'body-parser';
import cors from 'cors';
import routes from '../api';
import config from '../config';
import jwt from 'express-jwt';
import jwt, { UnauthorizedError } from 'express-jwt';
import fs from 'fs';
import { getPlatform, getToken } from '../config/util';
import Container from 'typedi';
@@ -75,22 +75,24 @@ export default ({ app }: { app: Application }) => {
if (
!headerToken &&
originPath &&
config.apiWhiteList.includes(originPath) &&
originPath !== '/api/crons/status'
config.apiWhiteList.includes(originPath)
) {
return next();
}
const data = fs.readFileSync(config.authConfigFile, 'utf8');
if (data) {
if (data && headerToken) {
const { token = '', tokens = {} } = JSON.parse(data);
if (headerToken === token || tokens[req.platform] === headerToken) {
return next();
}
}
const err: any = new Error('UnauthorizedError');
err.status = 401;
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
const errorMessage = headerToken
? 'jwt malformed'
: 'No authorization token was found';
const err = new UnauthorizedError(errorCode, { message: errorMessage });
next(err);
});
@@ -168,7 +170,17 @@ export default ({ app }: { app: Application }) => {
},
);
app.use(Sentry.Handlers.errorHandler());
app.use(
Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// 排除 SequelizeUniqueConstraintError / NotFound
return (
!['SequelizeUniqueConstraintError'].includes(error.name) ||
!['Not Found'].includes(error.message)
);
},
}),
);
app.use(
(
+46 -13
View File
@@ -115,18 +115,25 @@ export default class CronService {
private formatViewQuery(query: any, viewQuery: any) {
if (viewQuery.filters && viewQuery.filters.length > 0) {
if (!query[Op.and]) {
query[Op.and] = [];
}
for (const col of viewQuery.filters) {
const { property, value, operation } = col;
let q: any = {};
let operate2 = null;
let operate = null;
switch (operation) {
case 'Reg':
operate = Op.like;
operate2 = Op.or;
break;
case 'NotReg':
operate = Op.notLike;
operate2 = Op.and;
break;
case 'In':
query[Op.or] = [
q[Op.or] = [
{
[property]: value,
},
@@ -136,7 +143,7 @@ export default class CronService {
];
break;
case 'Nin':
query[Op.and] = [
q[Op.and] = [
{
[property]: {
[Op.notIn]: value,
@@ -150,20 +157,25 @@ export default class CronService {
default:
break;
}
if (operate) {
query[property] = {
[Op.or]: [
if (operate && operate2) {
q[property] = {
[operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
],
};
}
query[Op.and].push(q);
}
}
}
private formatSearchText(query: any, searchText: string | undefined) {
if (searchText) {
if (!query[Op.and]) {
query[Op.and] = [];
}
let q: any = {};
const textArray = searchText.split(':');
switch (textArray[0]) {
case 'name':
@@ -171,7 +183,7 @@ export default class CronService {
case 'schedule':
case 'label':
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
query[column] = {
q[column] = {
[Op.or]: [
{ [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
@@ -185,7 +197,7 @@ export default class CronService {
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
],
};
query[Op.or] = [
q[Op.or] = [
{
name: reg,
},
@@ -201,6 +213,23 @@ export default class CronService {
];
break;
}
query[Op.and].push(q);
}
}
private formatFilterQuery(query: any, filterQuery: any) {
if (filterQuery) {
if (!query[Op.and]) {
query[Op.and] = [];
}
const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) {
let q: any = {};
if (filterKeys[key]) {
q[key] = filterKeys[key];
}
query[Op.and].push(q);
}
}
}
@@ -216,16 +245,16 @@ export default class CronService {
searchValue: string;
page: string;
size: string;
sortField: string;
sortType: string;
sorter: string;
filters: string;
queryString: string;
}): Promise<{ data: Crontab[]; total: number }> {
const searchText = params?.searchValue;
const page = Number(params?.page || '0');
const size = Number(params?.size || '0');
const sortField = params?.sortField || '';
const sortType = params?.sortType || '';
const viewQuery = JSON.parse(params?.queryString || '{}');
const filterQuery = JSON.parse(params?.filters || '{}');
const sorterQuery = JSON.parse(params?.sorter || '{}');
let query: any = {};
let order = [
@@ -237,10 +266,14 @@ export default class CronService {
this.formatViewQuery(query, viewQuery);
this.formatSearchText(query, searchText);
this.formatFilterQuery(query, filterQuery);
this.formatViewSort(order, viewQuery);
if (sortType && sortField) {
order.unshift([sortField, sortType]);
if (sorterQuery) {
const { field, type } = sorterQuery;
if (field && type) {
order.unshift([field, type]);
}
}
let condition: any = {
where: query,
+7
View File
@@ -11,6 +11,13 @@ cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
echo
echo -e "======================2. 安装依赖========================\n"
+3 -3
View File
@@ -36,8 +36,8 @@
},
"dependencies": {
"@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.0.0",
"@sentry/tracing": "^7.0.0",
"@sentry/node": "^7.12.1",
"@sentry/tracing": "^7.12.1",
"body-parser": "^1.19.2",
"celebrate": "^15.0.1",
"chokidar": "^3.5.3",
@@ -73,7 +73,7 @@
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "^4.3.1",
"@sentry/react": "^7.0.0",
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
+4 -1
View File
@@ -46,9 +46,12 @@ AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl=""
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
## 安装python依赖时指定pip源,默认使用清华源
PipMirror="https://pypi.doubanio.com/simple/"
## 安装node依赖时指定pip源,默认使用清华源
NpmMirror="https://registry.npmmirror.com"
## 通知环境变量
## 1. Server酱
## https://sct.ftqq.com
+3 -5
View File
@@ -245,15 +245,13 @@ fix_config() {
}
npm_install_sub() {
set_proxy
if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links --registry=https://registry.npmmirror.com || npm install --production --no-bin-links
npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then
npm install --production --registry=https://registry.npmmirror.com || npm install --production
npm install --production
else
pnpm install --loglevel error --production --registry=https://registry.npmmirror.com || pnpm install --production --loglevel error
pnpm install --loglevel error --production
fi
unset_proxy
}
npm_install_1() {
+8 -1
View File
@@ -303,7 +303,7 @@ patch_version() {
fi
if ! type ts-node &>/dev/null; then
pnpm i -g ts-node typescript tslib
pnpm add -g ts-node typescript tslib
fi
# 兼容pnpm@7
@@ -338,6 +338,13 @@ patch_version() {
echo
fi
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
}
## 对比脚本
+7 -3
View File
@@ -153,9 +153,13 @@ const CronDetailModal = ({
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
setScriptInfo({ parent: p, filename: s });
+8 -6
View File
@@ -2,12 +2,6 @@
margin-bottom: 0 !important;
}
.cron {
&:hover {
cursor: pointer;
}
}
.crontab-detail {
.card-wrapper {
height: 80vh;
@@ -161,6 +155,14 @@
}
}
.view-create-modal-sorts {
display: flex;
.ant-space-item:nth-child(2) {
flex: 1;
}
}
tr.drop-over-downward td {
border-bottom: 2px dashed #1890ff;
}
+35 -29
View File
@@ -93,10 +93,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
<>
<a
onClick={() => {
goToScriptManager(record);
setDetailCron(record);
setIsDetailModalVisible(true);
}}
>
{record.labels?.length > 0 && record.labels[0] !== '' ? (
{record.labels?.length > 0 && record.labels[0] !== '' && false ? (
<Popover
placement="right"
trigger={isPhone ? 'click' : 'hover'}
@@ -109,6 +110,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onClick={(e) => {
e.stopPropagation();
setSearchValue(`label:${label}`);
setSearchText(`label:${label}`);
}}
>
<a>{label}</a>
@@ -140,7 +142,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
title: '命令',
dataIndex: 'command',
key: 'command',
width: 250,
width: 300,
align: 'center' as const,
render: (text: string, record: any) => {
return (
@@ -152,7 +154,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}}
ellipsis={{ tooltip: text, rows: 2 }}
>
{text}
<a
onClick={() => {
goToScriptManager(record);
}}
>
{text}
</a>
</Paragraph>
);
},
@@ -173,6 +181,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
{
title: '最后运行时间',
align: 'center' as const,
dataIndex: 'last_execution_time',
key: 'last_execution_time',
width: 150,
sorter: {
compare: (a: any, b: any) => {
@@ -189,10 +199,10 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
>
{record.last_execution_time
? new Date(record.last_execution_time * 1000)
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')
: '-'}
</span>
);
@@ -202,6 +212,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
title: '最后运行时长',
align: 'center' as const,
width: 120,
dataIndex: 'last_running_time',
key: 'last_running_time',
sorter: {
compare: (a: any, b: any) => {
return a.last_running_time - b.last_running_time;
@@ -358,6 +370,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
page: number;
size: number;
sorter: any;
filters: any;
}>({} as any);
const [viewConf, setViewConf] = useState<any>();
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
@@ -380,9 +393,13 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
history.push(`/script?p=${p}&s=${s}`);
@@ -393,12 +410,10 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const getCrons = () => {
setLoading(true);
const { page, size, sorter } = pageConf;
let url = `${config.apiPrefix}crons?searchValue=${searchText}&page=${page}&size=${size}`;
const { page, size, sorter, filters } = pageConf;
let url = `${config.apiPrefix}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(filters)}`;
if (sorter && sorter.field) {
url += `&sortField=${sorter.field}&sortType=${
sorter.order === 'ascend' ? 'ASC' : 'DESC'
}`;
url += `&sorter=${JSON.stringify({ field: sorter.field, type: sorter.order === 'ascend' ? 'ASC' : 'DESC' })}`;
}
if (viewConf) {
url += `&queryString=${JSON.stringify({
@@ -562,8 +577,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() {
request
.put(
`${config.apiPrefix}crons/${
record.isDisabled === 1 ? 'enable' : 'disable'
`${config.apiPrefix}crons/${record.isDisabled === 1 ? 'enable' : 'disable'
}`,
{
data: [record.id],
@@ -608,8 +622,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() {
request
.put(
`${config.apiPrefix}crons/${
record.isPinned === 1 ? 'unpin' : 'pin'
`${config.apiPrefix}crons/${record.isPinned === 1 ? 'unpin' : 'pin'
}`,
{
data: [record.id],
@@ -815,7 +828,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
sorter: SorterResult<any> | SorterResult<any>[],
) => {
const { current, pageSize } = pagination;
setPageConf({ page: current as number, size: pageSize as number, sorter });
setPageConf({ page: current as number, size: pageSize as number, sorter, filters });
localStorage.setItem('pageSize', String(pageSize));
};
@@ -852,6 +865,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
filters: {}
});
setTimeout(() => {
setTableScrollHeight(getTableScroll());
@@ -931,14 +945,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
(a, b) => a - b,
),
}}
onRow={(record) => {
return {
onClick: (event) => {
setDetailCron(record);
setIsDetailModalVisible(true);
},
};
}}
dataSource={value}
rowKey="id"
size="middle"
+8 -9
View File
@@ -101,9 +101,9 @@ const ViewCreateModal = ({
</Select>
);
const propertyElement = (props: any) => {
const propertyElement = (props: any, style: React.CSSProperties = {}) => {
return (
<Select style={{ width: 120 }}>
<Select style={style}>
{props.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
@@ -114,7 +114,7 @@ const ViewCreateModal = ({
};
const typeElement = (
<Select style={{ width: 120 }}>
<Select>
{SORTTYPES.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
@@ -170,6 +170,7 @@ const ViewCreateModal = ({
label={index === 0 ? '筛选条件' : ''}
key={key}
style={{ marginBottom: 0 }}
required
>
<Space className="view-create-modal-filters" align="baseline">
<Form.Item
@@ -177,7 +178,7 @@ const ViewCreateModal = ({
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES)}
{propertyElement(PROPERTIES, { width: 120 })}
</Form.Item>
<Form.Item
{...restField}
@@ -225,13 +226,13 @@ const ViewCreateModal = ({
key={key}
style={{ marginBottom: 0 }}
>
<Space className="view-create-modal-filters" align="baseline">
<Space className="view-create-modal-sorts" align="baseline">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES)}
{propertyElement(PROPERTIES, { width: 240 })}
</Form.Item>
<Form.Item
{...restField}
@@ -240,9 +241,7 @@ const ViewCreateModal = ({
>
{typeElement}
</Form.Item>
{index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)}
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
</Form.Item>
))}
+5 -2
View File
@@ -231,7 +231,10 @@ const ViewManageModal = ({
<Button
key="2"
type="primary"
onClick={() => setIsCreateViewModalVisible(true)}
onClick={() => {
setEditedView(null);
setIsCreateViewModalVisible(true);
}}
>
</Button>
@@ -258,8 +261,8 @@ const ViewManageModal = ({
view={editedView}
visible={isCreateViewModalVisible}
handleCancel={(data) => {
cronViewChange(data);
setIsCreateViewModalVisible(false);
cronViewChange(data);
}}
/>
</Modal>
+8 -6
View File
@@ -1,7 +1,9 @@
export const version = '2.14.1';
export const changeLogLink = 'https://t.me/jiao_long/326';
export const changeLog = `2.14.1 版本说明
1. 修复定时任务搜索
2. 修复视图切换默认页码
3. 增加群辉chat通知方式,感谢 https://github.com/Appoip
export const version = '2.14.2';
export const changeLogLink = 'https://t.me/jiao_long/327';
export const changeLog = `2.14.2 版本说明
1. 增加npm镜像源配置 NpmMirror="https://registry.npmmirror.com"
2. 修复任务视图筛选条件和编辑视图,感谢 https://github.com/cddjr
3. 修复获取多级脚本路径错误,感谢 https://github.com/cddjr
4. 修改定时任务详情交互,点击任务名称打开任务详情,点击任务命令跳转脚本
5. 其他bug修复
`;