mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
refactor: async tasks (#6531)
* refactor: async tasks * refactor: selectWithTitle support optionRender * refactor: adjust export filename * fix: build error * fix: export file name --------- Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
parent
eba3f79134
commit
1b782c4f95
@ -18,7 +18,14 @@ export interface SelectWithTitleProps {
|
||||
onChange?: (...args: any[]) => void;
|
||||
}
|
||||
|
||||
export function SelectWithTitle({ title, defaultValue, onChange, options, fieldNames }: SelectWithTitleProps) {
|
||||
export function SelectWithTitle({
|
||||
title,
|
||||
defaultValue,
|
||||
onChange,
|
||||
options,
|
||||
fieldNames,
|
||||
...others
|
||||
}: SelectWithTitleProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const timerRef = useRef<any>(null);
|
||||
return (
|
||||
@ -36,6 +43,7 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN
|
||||
>
|
||||
{title}
|
||||
<Select
|
||||
{...others}
|
||||
open={open}
|
||||
data-testid={`select-${title}`}
|
||||
popupMatchSelectWidth={false}
|
||||
|
@ -568,13 +568,14 @@ export interface SchemaSettingsSelectItemProps
|
||||
extends Omit<SchemaSettingsItemProps, 'onChange' | 'onClick'>,
|
||||
Omit<SelectWithTitleProps, 'title' | 'defaultValue'> {
|
||||
value?: SelectWithTitleProps['defaultValue'];
|
||||
optionRender?: (option: any, info: { index: number }) => React.ReactNode;
|
||||
}
|
||||
export const SchemaSettingsSelectItem: FC<SchemaSettingsSelectItemProps> = (props) => {
|
||||
const { title, options, value, onChange, ...others } = props;
|
||||
const { title, options, value, onChange, optionRender, ...others } = props;
|
||||
|
||||
return (
|
||||
<SchemaSettingsItem title={title} {...others}>
|
||||
<SelectWithTitle {...{ title, defaultValue: value, onChange, options }} />
|
||||
<SelectWithTitle {...{ title, defaultValue: value, onChange, options, optionRender }} />
|
||||
</SchemaSettingsItem>
|
||||
);
|
||||
};
|
||||
|
@ -1,176 +1,24 @@
|
||||
import { PinnedPluginListProvider, SchemaComponentOptions, useApp } from '@nocobase/client';
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { PinnedPluginListProvider, SchemaComponentOptions, useRequest } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { AsyncTasks } from './components/AsyncTasks';
|
||||
import React, { useEffect, useState, createContext, useContext, useCallback } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { useT } from './locale';
|
||||
|
||||
export const AsyncTaskContext = createContext<any>(null);
|
||||
|
||||
export const useAsyncTask = () => {
|
||||
const context = useContext(AsyncTaskContext);
|
||||
if (!context) {
|
||||
throw new Error('useAsyncTask must be used within AsyncTaskManagerProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AsyncTaskManagerProvider = (props) => {
|
||||
const app = useApp();
|
||||
const t = useT();
|
||||
const [tasks, setTasks] = useState<any[]>([]);
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
const [hasProcessingTasks, setHasProcessingTasks] = useState(false);
|
||||
const [cancellingTasks, setCancellingTasks] = useState<Set<string>>(new Set());
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentError, setCurrentError] = useState<any>(null);
|
||||
const [resultModalVisible, setResultModalVisible] = useState(false);
|
||||
const [currentTask, setCurrentTask] = useState(null);
|
||||
const [wsAuthorized, setWsAuthorized] = useState(() => app.isWsAuthorized);
|
||||
|
||||
useEffect(() => {
|
||||
setHasProcessingTasks(tasks.some((task) => task.status.type !== 'success' && task.status.type !== 'failed'));
|
||||
}, [tasks]);
|
||||
|
||||
const handleTaskMessage = useCallback((event: CustomEvent) => {
|
||||
const tasks = event.detail;
|
||||
setTasks(tasks ? tasks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) : []);
|
||||
}, []);
|
||||
|
||||
const handleTaskCreated = useCallback((event: CustomEvent) => {
|
||||
const taskData = event.detail;
|
||||
setTasks((prev) => {
|
||||
const newTasks = [taskData, ...prev];
|
||||
return newTasks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
});
|
||||
setPopoverVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleTaskProgress = useCallback((event: CustomEvent) => {
|
||||
const { taskId, progress } = event.detail;
|
||||
setTasks((prev) => prev.map((task) => (task.taskId === taskId ? { ...task, progress } : task)));
|
||||
}, []);
|
||||
|
||||
const handleTaskStatus = useCallback((event: CustomEvent) => {
|
||||
const { taskId, status } = event.detail;
|
||||
if (status.type === 'cancelled') {
|
||||
setTasks((prev) => prev.filter((task) => task.taskId !== taskId));
|
||||
} else {
|
||||
setTasks((prev) => {
|
||||
const newTasks = prev.map((task) => {
|
||||
if (task.taskId === taskId) {
|
||||
if (status.type === 'success' && task.status.type !== 'success') {
|
||||
message.success(t('Task completed'));
|
||||
}
|
||||
if (status.type === 'failed' && task.status.type !== 'failed') {
|
||||
message.error(t('Task failed'));
|
||||
}
|
||||
return { ...task, status };
|
||||
}
|
||||
return task;
|
||||
});
|
||||
return newTasks;
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleWsAuthorized = useCallback(() => {
|
||||
setWsAuthorized(true);
|
||||
}, []);
|
||||
|
||||
const handleTaskCancelled = useCallback((event: CustomEvent) => {
|
||||
const { taskId } = event.detail;
|
||||
setCancellingTasks((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(taskId);
|
||||
return newSet;
|
||||
});
|
||||
message.success(t('Task cancelled'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
app.eventBus.addEventListener('ws:message:async-tasks', handleTaskMessage);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:created', handleTaskCreated);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:progress', handleTaskProgress);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:status', handleTaskStatus);
|
||||
app.eventBus.addEventListener('ws:message:authorized', handleWsAuthorized);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
|
||||
|
||||
if (wsAuthorized) {
|
||||
app.ws.send(
|
||||
JSON.stringify({
|
||||
type: 'request:async-tasks:list',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks', handleTaskMessage);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:created', handleTaskCreated);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:progress', handleTaskProgress);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:status', handleTaskStatus);
|
||||
app.eventBus.removeEventListener('ws:message:authorized', handleWsAuthorized);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
|
||||
};
|
||||
}, [
|
||||
app,
|
||||
handleTaskMessage,
|
||||
handleTaskCreated,
|
||||
handleTaskProgress,
|
||||
handleTaskStatus,
|
||||
handleWsAuthorized,
|
||||
handleTaskCancelled,
|
||||
wsAuthorized,
|
||||
]);
|
||||
|
||||
const handleCancelTask = async (taskId: string) => {
|
||||
setCancellingTasks((prev) => new Set(prev).add(taskId));
|
||||
try {
|
||||
app.ws.send(
|
||||
JSON.stringify({
|
||||
type: 'request:async-tasks:cancel',
|
||||
payload: { taskId },
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to cancel task:', error);
|
||||
setCancellingTasks((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(taskId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const contextValue = {
|
||||
tasks,
|
||||
popoverVisible,
|
||||
setPopoverVisible,
|
||||
hasProcessingTasks,
|
||||
cancellingTasks,
|
||||
modalVisible,
|
||||
setModalVisible,
|
||||
currentError,
|
||||
setCurrentError,
|
||||
resultModalVisible,
|
||||
setResultModalVisible,
|
||||
currentTask,
|
||||
setCurrentTask,
|
||||
handleCancelTask,
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncTaskContext.Provider value={contextValue}>
|
||||
<PinnedPluginListProvider
|
||||
items={
|
||||
tasks.length > 0
|
||||
? {
|
||||
asyncTasks: { order: 300, component: 'AsyncTasks', pin: true, snippet: '*' },
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<SchemaComponentOptions components={{ AsyncTasks }}>{props.children}</SchemaComponentOptions>
|
||||
</PinnedPluginListProvider>
|
||||
</AsyncTaskContext.Provider>
|
||||
<PinnedPluginListProvider
|
||||
items={{
|
||||
asyncTasks: { order: 300, component: 'AsyncTasks', pin: true, snippet: '*' },
|
||||
}}
|
||||
>
|
||||
<SchemaComponentOptions components={{ AsyncTasks }}>{props.children}</SchemaComponentOptions>
|
||||
</PinnedPluginListProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +1,31 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button, Popover, Table, Tag, Progress, Space, Tooltip, Popconfirm, Modal, Empty } from 'antd';
|
||||
import { createStyles, Icon, useApp, usePlugin } from '@nocobase/client';
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import {
|
||||
createStyles,
|
||||
Icon,
|
||||
useAPIClient,
|
||||
useApp,
|
||||
usePlugin,
|
||||
useRequest,
|
||||
useCollectionManager,
|
||||
useCompile,
|
||||
} from '@nocobase/client';
|
||||
import { Button, Empty, Modal, Popconfirm, Popover, Progress, Space, Table, Tag, Tooltip } from 'antd';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useCurrentAppInfo } from '@nocobase/client';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { useT } from '../locale';
|
||||
import { useAsyncTask } from '../AsyncTaskManagerProvider';
|
||||
import { useCurrentAppInfo } from '@nocobase/client';
|
||||
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
return {
|
||||
button: {
|
||||
@ -34,93 +52,27 @@ const renderTaskResult = (status, t) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const AsyncTasks = () => {
|
||||
const {
|
||||
tasks,
|
||||
popoverVisible,
|
||||
setPopoverVisible,
|
||||
hasProcessingTasks,
|
||||
cancellingTasks,
|
||||
modalVisible,
|
||||
setModalVisible,
|
||||
currentError,
|
||||
setCurrentError,
|
||||
resultModalVisible,
|
||||
setResultModalVisible,
|
||||
currentTask,
|
||||
setCurrentTask,
|
||||
handleCancelTask,
|
||||
} = useAsyncTask();
|
||||
const useAsyncTask = () => {
|
||||
const { data, refreshAsync, loading } = useRequest<any>({
|
||||
url: 'asyncTasks:list',
|
||||
});
|
||||
return { loading, tasks: data?.data || [], refresh: refreshAsync };
|
||||
};
|
||||
|
||||
const plugin = usePlugin<any>('async-task-manager');
|
||||
const AsyncTasksButton = (props) => {
|
||||
const { popoverVisible, setPopoverVisible, tasks, refresh, loading, hasProcessingTasks } = props;
|
||||
const app = useApp();
|
||||
const api = useAPIClient();
|
||||
const appInfo = useCurrentAppInfo();
|
||||
const t = useT();
|
||||
const { styles } = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (popoverVisible) {
|
||||
const popoverElements = document.querySelectorAll('.ant-popover');
|
||||
const buttonElement = document.querySelector('.sync-task-button');
|
||||
let clickedInside = false;
|
||||
|
||||
popoverElements.forEach((element) => {
|
||||
if (element.contains(event.target as Node)) {
|
||||
clickedInside = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (buttonElement?.contains(event.target as Node)) {
|
||||
clickedInside = true;
|
||||
}
|
||||
|
||||
if (!clickedInside) {
|
||||
setPopoverVisible(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
}, [popoverVisible, setPopoverVisible]);
|
||||
|
||||
const plugin = usePlugin<any>('async-task-manager');
|
||||
const cm = useCollectionManager();
|
||||
const compile = useCompile();
|
||||
const showTaskResult = (task) => {
|
||||
setCurrentTask(task);
|
||||
setResultModalVisible(true);
|
||||
setPopoverVisible(false);
|
||||
};
|
||||
|
||||
const renderTaskResultModal = () => {
|
||||
if (!currentTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { payload } = currentTask.status;
|
||||
const renderer = plugin.taskResultRendererManager.get(currentTask.title.actionType);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('Task result')}
|
||||
open={resultModalVisible}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setResultModalVisible(false)}>
|
||||
{t('Close')}
|
||||
</Button>,
|
||||
]}
|
||||
onCancel={() => setResultModalVisible(false)}
|
||||
>
|
||||
{renderer ? (
|
||||
React.createElement(renderer, { payload, task: currentTask })
|
||||
) : (
|
||||
<div>{t(`No renderer available for this task type, payload: ${payload}`)}</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('Created at'),
|
||||
@ -140,7 +92,7 @@ export const AsyncTasks = () => {
|
||||
if (!title) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const collection = cm.getCollection(title.collection);
|
||||
const actionTypeMap = {
|
||||
export: t('Export'),
|
||||
import: t('Import'),
|
||||
@ -156,7 +108,7 @@ export const AsyncTasks = () => {
|
||||
};
|
||||
|
||||
const taskTemplate = taskTypeMap[title.actionType] || `${actionText}`;
|
||||
return taskTemplate.replace('{collection}', title.collection);
|
||||
return taskTemplate.replace('{collection}', compile(collection.title));
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -274,7 +226,7 @@ export const AsyncTasks = () => {
|
||||
width: 180,
|
||||
render: (_, record: any) => {
|
||||
const actions = [];
|
||||
const isTaskCancelling = cancellingTasks.has(record.taskId);
|
||||
const isTaskCancelling = false;
|
||||
|
||||
if ((record.status.type === 'running' || record.status.type === 'pending') && record.cancelable) {
|
||||
actions.push(
|
||||
@ -282,7 +234,15 @@ export const AsyncTasks = () => {
|
||||
key="cancel"
|
||||
title={t('Confirm cancel')}
|
||||
description={t('Confirm cancel description')}
|
||||
onConfirm={() => handleCancelTask(record.taskId)}
|
||||
onConfirm={async () => {
|
||||
await api.request({
|
||||
url: 'asyncTasks:cancel',
|
||||
params: {
|
||||
filterByTk: record.taskId,
|
||||
},
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
okText={t('Confirm')}
|
||||
cancelText={t('Cancel')}
|
||||
disabled={isTaskCancelling}
|
||||
@ -309,8 +269,15 @@ export const AsyncTasks = () => {
|
||||
icon={<Icon type="DownloadOutlined" />}
|
||||
onClick={() => {
|
||||
const token = app.apiClient.auth.token;
|
||||
const collection = cm.getCollection(record.title.collection);
|
||||
const compiledTitle = compile(collection.title);
|
||||
const suffix = record?.title?.actionType === 'export-attachments' ? '-attachments.zip' : '.xlsx';
|
||||
const fileText = `${compiledTitle}${suffix}`;
|
||||
const filename = encodeURIComponent(fileText); // 避免中文或特殊字符问题
|
||||
const url = app.getApiUrl(
|
||||
`asyncTasks:fetchFile/${record.taskId}?token=${token}&__appName=${appInfo?.data?.name || app.name}`,
|
||||
`asyncTasks:fetchFile/${record.taskId}?token=${token}&__appName=${encodeURIComponent(
|
||||
appInfo?.data?.name || app.name,
|
||||
)}&filename=${filename}`,
|
||||
);
|
||||
window.open(url);
|
||||
}}
|
||||
@ -325,7 +292,19 @@ export const AsyncTasks = () => {
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<Icon type="EyeOutlined" />}
|
||||
onClick={() => showTaskResult(record)}
|
||||
onClick={() => {
|
||||
showTaskResult(record);
|
||||
const { payload } = record.status;
|
||||
const renderer = plugin.taskResultRendererManager.get(record.title.actionType);
|
||||
Modal.info({
|
||||
title: t('Task result'),
|
||||
content: renderer ? (
|
||||
React.createElement(renderer, { payload, task: record })
|
||||
) : (
|
||||
<div>{t(`No renderer available for this task type, payload: ${payload}`)}</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('View result')}
|
||||
</Button>,
|
||||
@ -341,9 +320,22 @@ export const AsyncTasks = () => {
|
||||
size="small"
|
||||
icon={<Icon type="ExclamationCircleOutlined" />}
|
||||
onClick={() => {
|
||||
setCurrentError(record.status.errors);
|
||||
setModalVisible(true);
|
||||
setPopoverVisible(false);
|
||||
Modal.info({
|
||||
title: t('Error Details'),
|
||||
content: record.status.errors?.map((error, index) => (
|
||||
<div key={index} style={{ marginBottom: 16 }}>
|
||||
<div style={{ color: '#ff4d4f', marginBottom: 8 }}>{error.message}</div>
|
||||
{error.code && (
|
||||
<div style={{ color: '#999', fontSize: 12 }}>
|
||||
{t('Error code')}: {error.code}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)),
|
||||
closable: true,
|
||||
width: 400,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Error details')}
|
||||
@ -357,9 +349,9 @@ export const AsyncTasks = () => {
|
||||
];
|
||||
|
||||
const content = (
|
||||
<div style={{ width: tasks.length > 0 ? 800 : 200 }}>
|
||||
<div style={{ maxHeight: '70vh', overflow: 'auto', width: tasks.length > 0 ? 800 : 200 }}>
|
||||
{tasks.length > 0 ? (
|
||||
<Table columns={columns} dataSource={tasks} size="small" pagination={false} rowKey="taskId" />
|
||||
<Table loading={loading} columns={columns} dataSource={tasks} size="small" pagination={false} rowKey="taskId" />
|
||||
) : (
|
||||
<div style={{ padding: '24px 0', display: 'flex', justifyContent: 'center' }}>
|
||||
<Empty description={t('No tasks')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
@ -383,30 +375,53 @@ export const AsyncTasks = () => {
|
||||
onClick={() => setPopoverVisible(!popoverVisible)}
|
||||
/>
|
||||
</Popover>
|
||||
{renderTaskResultModal()}
|
||||
|
||||
<Modal
|
||||
title={t('Error Details')}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="ok" type="primary" onClick={() => setModalVisible(false)}>
|
||||
{t('OK')}
|
||||
</Button>,
|
||||
]}
|
||||
width={400}
|
||||
>
|
||||
{currentError?.map((error, index) => (
|
||||
<div key={index} style={{ marginBottom: 16 }}>
|
||||
<div style={{ color: '#ff4d4f', marginBottom: 8 }}>{error.message}</div>
|
||||
{error.code && (
|
||||
<div style={{ color: '#999', fontSize: 12 }}>
|
||||
{t('Error code')}: {error.code}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AsyncTasks = () => {
|
||||
const { tasks, refresh, ...others } = useAsyncTask();
|
||||
const app = useApp();
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
const [hasProcessingTasks, setHasProcessingTasks] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setHasProcessingTasks(tasks.some((task) => task.status.type !== 'success' && task.status.type !== 'failed'));
|
||||
}, [tasks]);
|
||||
|
||||
const handleTaskCreated = useCallback(async () => {
|
||||
setPopoverVisible(true);
|
||||
}, []);
|
||||
const handleTaskProgress = useCallback(() => {
|
||||
refresh();
|
||||
console.log('handleTaskProgress');
|
||||
}, []);
|
||||
const handleTaskStatus = useCallback(() => {
|
||||
refresh();
|
||||
console.log('handleTaskStatus');
|
||||
}, []);
|
||||
const handleTaskCancelled = useCallback(() => {
|
||||
refresh();
|
||||
console.log('handleTaskCancelled');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:created', handleTaskCreated);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:progress', handleTaskProgress);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:status', handleTaskStatus);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
|
||||
|
||||
return () => {
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:created', handleTaskCreated);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:progress', handleTaskProgress);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:status', handleTaskStatus);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
|
||||
};
|
||||
}, [app, handleTaskCancelled, handleTaskCreated, handleTaskProgress, handleTaskStatus]);
|
||||
|
||||
return (
|
||||
tasks?.length > 0 && (
|
||||
<AsyncTasksButton {...{ tasks, refresh, popoverVisible, setPopoverVisible, hasProcessingTasks, ...others }} />
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -1,9 +1,18 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { BaseTaskManager } from './base-task-manager';
|
||||
import { AsyncTasksManager } from './interfaces/async-task-manager';
|
||||
import { CommandTaskType } from './command-task-type';
|
||||
import asyncTasksResource from './resourcers/async-tasks';
|
||||
import { throttle } from 'lodash';
|
||||
import { BaseTaskManager } from './base-task-manager';
|
||||
import { CommandTaskType } from './command-task-type';
|
||||
import { AsyncTasksManager } from './interfaces/async-task-manager';
|
||||
import asyncTasksResource from './resourcers/async-tasks';
|
||||
|
||||
export class PluginAsyncExportServer extends Plugin {
|
||||
private progressThrottles: Map<string, Function> = new Map();
|
||||
@ -20,7 +29,7 @@ export class PluginAsyncExportServer extends Plugin {
|
||||
});
|
||||
|
||||
this.app.container.get<AsyncTasksManager>('AsyncTaskManager').registerTaskType(CommandTaskType);
|
||||
this.app.acl.allow('asyncTasks', ['get', 'fetchFile'], 'loggedIn');
|
||||
this.app.acl.allow('asyncTasks', ['list', 'get', 'fetchFile', 'cancel'], 'loggedIn');
|
||||
}
|
||||
|
||||
getThrottledProgressEmitter(taskId: string, userId: string) {
|
||||
|
@ -1,8 +1,26 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import { basename } from 'path';
|
||||
|
||||
export default {
|
||||
name: 'asyncTasks',
|
||||
actions: {
|
||||
async list(ctx, next) {
|
||||
const userId = ctx.auth.user.id;
|
||||
const asyncTaskManager = ctx.app.container.get('AsyncTaskManager');
|
||||
const tasks = await asyncTaskManager.getTasksByTag('userId', userId);
|
||||
ctx.body = _.orderBy(tasks, 'createdAt', 'desc');
|
||||
await next();
|
||||
},
|
||||
async get(ctx, next) {
|
||||
const { filterByTk } = ctx.action.params;
|
||||
const taskManager = ctx.app.container.get('AsyncTaskManager');
|
||||
@ -11,8 +29,29 @@ export default {
|
||||
ctx.body = taskStatus;
|
||||
await next();
|
||||
},
|
||||
async fetchFile(ctx, next) {
|
||||
async cancel(ctx, next) {
|
||||
const { filterByTk } = ctx.action.params;
|
||||
const userId = ctx.auth.user.id;
|
||||
const asyncTaskManager = ctx.app.container.get('AsyncTaskManager');
|
||||
|
||||
const task = asyncTaskManager.getTask(filterByTk);
|
||||
|
||||
if (!task) {
|
||||
ctx.body = 'ok';
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.tags['userId'] != userId) {
|
||||
ctx.throw(403);
|
||||
}
|
||||
|
||||
const cancelled = await asyncTaskManager.cancelTask(filterByTk);
|
||||
ctx.body = cancelled;
|
||||
await next();
|
||||
},
|
||||
async fetchFile(ctx, next) {
|
||||
const { filterByTk, filename } = ctx.action.params;
|
||||
const taskManager = ctx.app.container.get('AsyncTaskManager');
|
||||
const taskStatus = await taskManager.getTaskStatus(filterByTk);
|
||||
// throw error if task is not success
|
||||
@ -28,10 +67,12 @@ export default {
|
||||
|
||||
// send file to client
|
||||
ctx.body = fs.createReadStream(filePath);
|
||||
|
||||
// 处理文件名
|
||||
let finalFileName = filename ? filename : basename(filePath);
|
||||
finalFileName = encodeURIComponent(finalFileName); // 避免中文或特殊字符问题
|
||||
ctx.set({
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Disposition': `attachment; filename=${basename(filePath)}`,
|
||||
'Content-Disposition': `attachment; filename=${finalFileName}`,
|
||||
});
|
||||
|
||||
await next();
|
||||
|
Loading…
x
Reference in New Issue
Block a user