mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-17 07:20:51 +08:00
Translate UI, scheduler, validation, preload and maintenance messages dynamically, and propagate locale changes between workers. Harden update checks, downloads and terminal states, verify Debian pnpm, and ignore generated test artifacts.
600 lines
16 KiB
TypeScript
600 lines
16 KiB
TypeScript
import intl from 'react-intl-universal';
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import {
|
|
Button,
|
|
message,
|
|
Modal,
|
|
Table,
|
|
Tag,
|
|
Space,
|
|
Dropdown,
|
|
Menu,
|
|
Typography,
|
|
Input,
|
|
Tooltip,
|
|
Checkbox,
|
|
} from 'antd';
|
|
import {
|
|
ClockCircleOutlined,
|
|
Loading3QuartersOutlined,
|
|
CloseCircleOutlined,
|
|
EllipsisOutlined,
|
|
CheckCircleOutlined,
|
|
EditOutlined,
|
|
StopOutlined,
|
|
DeleteOutlined,
|
|
FileTextOutlined,
|
|
PauseCircleOutlined,
|
|
PlayCircleOutlined,
|
|
} 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 { history, useOutletContext } from '@umijs/max';
|
|
import './index.less';
|
|
import SubscriptionLogModal from './logModal';
|
|
import { SharedContext } from '@/layouts';
|
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
|
import WebSocketManager from '@/utils/websocket';
|
|
|
|
const { Text, Paragraph } = Typography;
|
|
const { Search } = Input;
|
|
|
|
export enum SubscriptionStatus {
|
|
'running',
|
|
'idle',
|
|
'disabled',
|
|
'queued',
|
|
}
|
|
|
|
export enum IntervalSchedule {
|
|
'days' = '天',
|
|
'hours' = '时',
|
|
'minutes' = '分',
|
|
'seconds' = '秒',
|
|
}
|
|
|
|
export enum SubscriptionType {
|
|
'private-repo' = '私有仓库',
|
|
'public-repo' = '公开仓库',
|
|
'file' = '单文件',
|
|
}
|
|
|
|
const Subscription = () => {
|
|
const { headerStyle, isPhone } = useOutletContext<SharedContext>();
|
|
|
|
const columns: any = [
|
|
{
|
|
title: intl.get('名称'),
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
width: 150,
|
|
sorter: {
|
|
compare: (a: any, b: any) => a.name.localeCompare(b.name),
|
|
multiple: 2,
|
|
},
|
|
},
|
|
{
|
|
title: intl.get('链接'),
|
|
dataIndex: 'url',
|
|
key: 'url',
|
|
sorter: {
|
|
compare: (a: any, b: any) => a.name.localeCompare(b.name),
|
|
multiple: 2,
|
|
},
|
|
render: (text: string, record: any) => {
|
|
return (
|
|
<Paragraph
|
|
style={{
|
|
wordBreak: 'break-all',
|
|
marginBottom: 0,
|
|
}}
|
|
ellipsis={{ tooltip: text, rows: 2 }}
|
|
>
|
|
{text}
|
|
</Paragraph>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: intl.get('类型'),
|
|
dataIndex: 'type',
|
|
key: 'type',
|
|
width: 130,
|
|
render: (text: string, record: any) => {
|
|
return intl.get((SubscriptionType as any)[record.type]);
|
|
},
|
|
},
|
|
{
|
|
title: intl.get('分支'),
|
|
dataIndex: 'branch',
|
|
key: 'branch',
|
|
width: 130,
|
|
render: (text: string, record: any) => {
|
|
return record.branch || '-';
|
|
},
|
|
},
|
|
{
|
|
title: intl.get('定时规则'),
|
|
width: 180,
|
|
render: (text: string, record: any) => {
|
|
if (record.schedule_type === 'interval') {
|
|
const { type, value } = record.interval_schedule;
|
|
return intl.get('间隔定时规则', { value, unit: intl.get((IntervalSchedule as any)[type]) });
|
|
}
|
|
return record.schedule;
|
|
},
|
|
},
|
|
{
|
|
title: intl.get('状态'),
|
|
key: 'status',
|
|
dataIndex: 'status',
|
|
width: 110,
|
|
filters: [
|
|
{
|
|
text: intl.get('运行中'),
|
|
value: 0,
|
|
},
|
|
{
|
|
text: intl.get('空闲中'),
|
|
value: 1,
|
|
},
|
|
{
|
|
text: intl.get('已禁用'),
|
|
value: 2,
|
|
},
|
|
],
|
|
onFilter: (value: number, record: any) => {
|
|
if (record.is_disabled && record.status !== 0) {
|
|
return value === 2;
|
|
} else {
|
|
return record.status === value;
|
|
}
|
|
},
|
|
render: (text: string, record: any) => (
|
|
<>
|
|
{(!record.is_disabled ||
|
|
record.status !== SubscriptionStatus.idle) && (
|
|
<>
|
|
{record.status === SubscriptionStatus.idle && (
|
|
<Tag icon={<ClockCircleOutlined />} color="default">
|
|
{intl.get('空闲中')}
|
|
</Tag>
|
|
)}
|
|
{record.status === SubscriptionStatus.running && (
|
|
<Tag
|
|
icon={<Loading3QuartersOutlined spin />}
|
|
color="processing"
|
|
>
|
|
{intl.get('运行中')}
|
|
</Tag>
|
|
)}
|
|
</>
|
|
)}
|
|
{record.is_disabled === 1 &&
|
|
record.status === SubscriptionStatus.idle && (
|
|
<Tag icon={<CloseCircleOutlined />} color="error">
|
|
{intl.get('已禁用')}
|
|
</Tag>
|
|
)}
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
title: intl.get('操作'),
|
|
key: 'action',
|
|
width: 140,
|
|
render: (text: string, record: any, index: number) => {
|
|
const isPc = !isPhone;
|
|
return (
|
|
<Space size="middle">
|
|
{record.status === SubscriptionStatus.idle && (
|
|
<a
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
runSubscription(record, index);
|
|
}}
|
|
>
|
|
{intl.get('运行')}
|
|
</a>
|
|
)}
|
|
{record.status !== SubscriptionStatus.idle && (
|
|
<a
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
stopSubsciption(record, index);
|
|
}}
|
|
>
|
|
{intl.get('停止')}
|
|
</a>
|
|
)}
|
|
<a
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setLogSubscription({ ...record, timestamp: Date.now() });
|
|
}}
|
|
>
|
|
{intl.get('日志')}
|
|
</a>
|
|
<MoreBtn key="more" record={record} index={index} />
|
|
</Space>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
const [value, setValue] = useState<any[]>([]);
|
|
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 [isLogModalVisible, setIsLogModalVisible] = useState(false);
|
|
const [logSubscription, setLogSubscription] = useState<any>();
|
|
const tableRef = useRef<HTMLDivElement>(null);
|
|
const tableScrollHeight = useTableScrollHeight(tableRef);
|
|
const deleteCheckRef = useRef(false);
|
|
|
|
const runSubscription = (record: any, index: number) => {
|
|
Modal.confirm({
|
|
title: intl.get('确认运行'),
|
|
content: (
|
|
<>
|
|
{intl.get('确认运行定时任务')}{' '}
|
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
|
{record.name}
|
|
</Text>{' '}
|
|
{intl.get('吗')}
|
|
</>
|
|
),
|
|
onOk() {
|
|
request
|
|
.put(`${config.apiPrefix}subscriptions/run`, [record.id])
|
|
.then(({ code, data }) => {
|
|
if (code === 200) {
|
|
const result = [...value];
|
|
const i = result.findIndex((x) => x.id === record.id);
|
|
if (i !== -1) {
|
|
result.splice(i, 1, {
|
|
...record,
|
|
status: SubscriptionStatus.running,
|
|
});
|
|
setValue(result);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
const stopSubsciption = (record: any, index: number) => {
|
|
Modal.confirm({
|
|
title: intl.get('确认停止'),
|
|
content: (
|
|
<>
|
|
{intl.get('确认停止定时任务')}{' '}
|
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
|
{record.name}
|
|
</Text>{' '}
|
|
{intl.get('吗')}
|
|
</>
|
|
),
|
|
onOk() {
|
|
request
|
|
.put(`${config.apiPrefix}subscriptions/stop`, [record.id])
|
|
.then(({ code, data }) => {
|
|
if (code === 200) {
|
|
const result = [...value];
|
|
const i = result.findIndex((x) => x.id === record.id);
|
|
if (i !== -1) {
|
|
result.splice(i, 1, {
|
|
...record,
|
|
pid: null,
|
|
status: SubscriptionStatus.idle,
|
|
});
|
|
setValue(result);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
const getSubscriptions = () => {
|
|
setLoading(true);
|
|
request
|
|
.get(`${config.apiPrefix}subscriptions?searchValue=${searchText}`)
|
|
.then(({ code, data }) => {
|
|
if (code === 200) {
|
|
setValue(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 onCheckChange = (e) => {
|
|
deleteCheckRef.current = e.target.checked;
|
|
};
|
|
|
|
const delSubscription = (record: any, index: number) => {
|
|
Modal.confirm({
|
|
title: intl.get('确认删除'),
|
|
content: (
|
|
<>
|
|
{intl.get('确认删除定时订阅')}{' '}
|
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
|
{record.name}
|
|
</Text>{' '}
|
|
{intl.get('吗')}
|
|
<div style={{ marginTop: 20 }}>
|
|
<Checkbox onChange={onCheckChange}>
|
|
{intl.get('同时删除关联任务和脚本')}
|
|
</Checkbox>
|
|
</div>
|
|
</>
|
|
),
|
|
onOk() {
|
|
request
|
|
.delete(`${config.apiPrefix}subscriptions`, {
|
|
data: [record.id],
|
|
params: { force: deleteCheckRef.current },
|
|
})
|
|
.then(({ code, data }) => {
|
|
if (code === 200) {
|
|
message.success(intl.get('删除成功'));
|
|
const result = [...value];
|
|
const i = result.findIndex((x) => x.id === record.id);
|
|
if (i !== -1) {
|
|
result.splice(i, 1);
|
|
setValue(result);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
const enabledOrDisabledSubscription = (record: any, index: number) => {
|
|
Modal.confirm({
|
|
title: intl.get('确认操作', {
|
|
operation:
|
|
record.is_disabled === 1 ? intl.get('启用') : intl.get('禁用'),
|
|
}),
|
|
content: (
|
|
<>
|
|
{intl.get('确认操作对象', { operation: record.is_disabled === 1 ? intl.get('启用') : intl.get('禁用'), object: intl.get('定时订阅') })}{' '}
|
|
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
|
{record.name}
|
|
</Text>{' '}
|
|
{intl.get('吗')}
|
|
</>
|
|
),
|
|
onOk() {
|
|
request
|
|
.put(
|
|
`${config.apiPrefix}subscriptions/${
|
|
record.is_disabled === 1 ? 'enable' : 'disable'
|
|
}`,
|
|
[record.id],
|
|
)
|
|
.then(({ code, data }) => {
|
|
if (code === 200) {
|
|
const newStatus = record.is_disabled === 1 ? 0 : 1;
|
|
const result = [...value];
|
|
const i = result.findIndex((x) => x.id === record.id);
|
|
if (i !== -1) {
|
|
result.splice(i, 1, {
|
|
...record,
|
|
is_disabled: newStatus,
|
|
});
|
|
setValue(result);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
const MoreBtn: React.FC<{
|
|
record: any;
|
|
index: number;
|
|
}> = ({ record, index }) => (
|
|
<Dropdown
|
|
placement="bottomRight"
|
|
trigger={['click']}
|
|
menu={{
|
|
items: [
|
|
{ label: intl.get('编辑'), key: 'edit', icon: <EditOutlined /> },
|
|
{
|
|
label:
|
|
record.is_disabled === 1 ? intl.get('启用') : intl.get('禁用'),
|
|
key: 'enableOrDisable',
|
|
icon:
|
|
record.is_disabled === 1 ? (
|
|
<CheckCircleOutlined />
|
|
) : (
|
|
<StopOutlined />
|
|
),
|
|
},
|
|
{ label: intl.get('删除'), key: 'delete', icon: <DeleteOutlined /> },
|
|
],
|
|
onClick: ({ key, domEvent }) => {
|
|
domEvent.stopPropagation();
|
|
action(key, record, index);
|
|
},
|
|
}}
|
|
>
|
|
<a onClick={(e) => e.stopPropagation()}>
|
|
<EllipsisOutlined />
|
|
</a>
|
|
</Dropdown>
|
|
);
|
|
|
|
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';
|
|
};
|
|
|
|
const handleMessage = useCallback((payload: any) => {
|
|
const { message, references } = payload;
|
|
setValue((p) => {
|
|
const result = [...p];
|
|
for (let i = 0; i < references.length; i++) {
|
|
const index = p.findIndex((x) => x.id === references[i]);
|
|
if (index !== -1) {
|
|
result.splice(index, 1, {
|
|
...p[index],
|
|
status: SubscriptionStatus.idle,
|
|
});
|
|
}
|
|
}
|
|
return result;
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const ws = WebSocketManager.getInstance();
|
|
ws.subscribe('runSubscriptionEnd', handleMessage);
|
|
|
|
return () => {
|
|
ws.unsubscribe('runSubscriptionEnd', handleMessage);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (logSubscription) {
|
|
localStorage.setItem('logSubscription', logSubscription.id);
|
|
setIsLogModalVisible(true);
|
|
}
|
|
}, [logSubscription]);
|
|
|
|
useEffect(() => {
|
|
getSubscriptions();
|
|
}, [searchText]);
|
|
|
|
useEffect(() => {
|
|
setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
|
|
}, []);
|
|
|
|
return (
|
|
<PageContainer
|
|
className="ql-container-wrapper subscriptiontab-wrapper"
|
|
title={intl.get('订阅管理')}
|
|
extra={[
|
|
<Search
|
|
placeholder={intl.get('请输入名称或者关键词')}
|
|
style={{ width: 'auto' }}
|
|
enterButton
|
|
allowClear
|
|
loading={loading}
|
|
onSearch={onSearch}
|
|
/>,
|
|
<Button key="2" type="primary" onClick={() => addSubscription()}>
|
|
{intl.get('创建订阅')}
|
|
</Button>,
|
|
]}
|
|
header={{
|
|
style: headerStyle,
|
|
}}
|
|
>
|
|
<Table
|
|
ref={tableRef}
|
|
columns={columns}
|
|
pagination={{
|
|
current: currentPage,
|
|
onChange: onPageChange,
|
|
pageSize: pageSize,
|
|
showSizeChanger: true,
|
|
simple: isPhone,
|
|
defaultPageSize: 20,
|
|
showTotal: (total: number, range: number[]) =>
|
|
intl.get('分页总数', { start: range[0], end: 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}
|
|
/>
|
|
{isModalVisible && (
|
|
<SubscriptionModal
|
|
handleCancel={handleCancel}
|
|
subscription={editedSubscription}
|
|
/>
|
|
)}
|
|
{isLogModalVisible && (
|
|
<SubscriptionLogModal
|
|
handleCancel={() => {
|
|
setIsLogModalVisible(false);
|
|
}}
|
|
subscription={logSubscription}
|
|
/>
|
|
)}
|
|
</PageContainer>
|
|
);
|
|
};
|
|
|
|
export default Subscription;
|