From af91da14003c2be799ae62eed9be1bb09ad00008 Mon Sep 17 00:00:00 2001 From: devinxl <94832688+devinxl@users.noreply.github.com> Date: Thu, 16 May 2024 17:02:01 +0800 Subject: [PATCH] feat(dcellar-web-ui): introduce the stop upload feature (#385) * feat(dcellar-web-ui): introduce the stop upload feature * fix(dcellar-web-ui): the uploading name text ellispsis * fix(dcellar-web-ui): change the stop status icon * feat(dcellar-web-ui): introduce activities feature for bucket, object and group * fix(dcellar-web-ui): text case error * refactor(dcellar-web-ui): the transfer in style & toolbox style * feat(dcellar-web-ui): add discord and release note link * feat(dcellar-web-ui): introduce the stop upload feature --- .../public/js/iconfont_v0.1.12.min.js | 1 + .../GlobalObjectUploadManager.tsx | 26 ++++++- .../src/modules/upload/ObjectUploadStatus.tsx | 14 ++-- .../src/modules/upload/UploadActionButton.tsx | 65 +++++++++++++---- .../src/modules/upload/UploadingObjects.tsx | 15 ++-- .../modules/upload/UploadingObjectsList.tsx | 43 +++++++----- .../modules/upload/useTaskManagementTab.tsx | 22 +++--- apps/dcellar-web-ui/src/pages/_document.tsx | 2 +- .../dcellar-web-ui/src/store/slices/global.ts | 69 +++++++++++++++---- 9 files changed, 193 insertions(+), 64 deletions(-) create mode 100644 apps/dcellar-web-ui/public/js/iconfont_v0.1.12.min.js diff --git a/apps/dcellar-web-ui/public/js/iconfont_v0.1.12.min.js b/apps/dcellar-web-ui/public/js/iconfont_v0.1.12.min.js new file mode 100644 index 00000000..1e4a3576 --- /dev/null +++ b/apps/dcellar-web-ui/public/js/iconfont_v0.1.12.min.js @@ -0,0 +1 @@ +!function(e){var t,n,d,o,i,a,r='';function c(){i||(i=!0,d())}t=function(){var e,t,n;(n=document.createElement("div")).innerHTML=r,r=null,(t=n.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",e=t,(n=document.body).firstChild?(t=n.firstChild).parentNode.insertBefore(e,t):n.appendChild(e))},document.addEventListener?["complete","loaded","interactive"].indexOf(document.readyState)>-1?setTimeout(t,0):(n=function(){document.removeEventListener("DOMContentLoaded",n,!1),t()},document.addEventListener("DOMContentLoaded",n,!1)):document.attachEvent&&(d=t,o=e.document,i=!1,(a=function(){try{o.documentElement.doScroll("left")}catch(e){return void setTimeout(a,50)}c()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,c())})}(window); \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx b/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx index d776f4fb..a94a817a 100644 --- a/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx +++ b/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx @@ -222,6 +222,7 @@ export const GlobalObjectUploadManager = memo( } else { axios .put(url, task.waitObject.file, { + signal: task.abortController?.signal, async onUploadProgress(progressEvent) { const progress = progressEvent.total ? Math.floor((progressEvent.loaded / progressEvent.total) * 100) @@ -265,6 +266,7 @@ export const GlobalObjectUploadManager = memo( setupUploadTaskErrorMsg({ account: loginAccount, task, + status: e?.code === 'ERR_CANCELED' ? 'CANCEL' : 'ERROR', errorMsg: authExpired ? 'Authentication expired.' : message || e?.message || 'upload error', @@ -461,8 +463,28 @@ export const GlobalObjectUploadManager = memo( // 3. upload useAsyncEffect(async () => { if (!uploadTasks.length) return; - dispatch(updateUploadStatus({ ids: uploadTasks, status: 'UPLOAD', account: loginAccount })); - const tasks = queue.filter((t) => uploadTasks.includes(t.id)); + // Add abortController to each task + const extraFields: Record> = uploadTasks.reduce( + (acc, id) => { + acc[id] = { + abortController: new AbortController(), + }; + return acc; + }, + {} as Record>, + ); + dispatch( + updateUploadStatus({ + ids: uploadTasks, + status: 'UPLOAD', + account: loginAccount, + extraFields, + }), + ); + + const tasks = queue + .filter((t) => uploadTasks.includes(t.id)) + .map((t) => ({ ...t, ...extraFields[t.id] })); tasks.forEach(runUploadTask); }, [uploadTasks.join('')]); diff --git a/apps/dcellar-web-ui/src/modules/upload/ObjectUploadStatus.tsx b/apps/dcellar-web-ui/src/modules/upload/ObjectUploadStatus.tsx index 04d298e3..e0ab9a25 100644 --- a/apps/dcellar-web-ui/src/modules/upload/ObjectUploadStatus.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/ObjectUploadStatus.tsx @@ -1,10 +1,14 @@ import { UploadObject } from '@/store/slices/global'; -import { Text } from '@node-real/uikit'; import { Loading } from '@/components/common/Loading'; import { UploadProgress } from './UploadProgress'; import { IconFont } from '@/components/IconFont'; +import { memo } from 'react'; -export const ObjectUploadStatus = ({ task }: { task: UploadObject }) => { +export const ObjectUploadStatus = memo(function ObjectUploadStatus({ + task, +}: { + task: UploadObject; +}) { switch (task.status) { case 'RETRY_CHECK': case 'RETRY_CHECKING': @@ -61,11 +65,11 @@ export const ObjectUploadStatus = ({ task }: { task: UploadObject }) => { case 'CANCEL': return ( <> - - Cancelled + + Stopped ); default: return null; } -}; +}); diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadActionButton.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadActionButton.tsx index 9721f0bc..03f23425 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadActionButton.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadActionButton.tsx @@ -1,36 +1,58 @@ import { IconFont } from '@/components/IconFont'; import { DCButton } from '@/components/common/DCButton'; import { useAppDispatch, useAppSelector } from '@/store'; -import { clearUploadRecords, retryUploadTasks } from '@/store/slices/global'; +import { + cancelUploadingRequests, + clearUploadRecords, + retryUploadTasks, + updateUploadStatus, +} from '@/store/slices/global'; +<<<<<<< HEAD +<<<<<<< HEAD +import React, { useCallback } from 'react'; +======= +======= +>>>>>>> f143b09b (feat(dcellar-web-ui): introduce the stop upload feature) import React from 'react'; +>>>>>>> 2ee2f675 (feat(dcellar-web-ui): introduce the stop upload feature) export type ActionButtonProps = { - type: 'clear' | 'retry' | 'clear-all' | 'retry-all'; + type: 'clear' | 'retry' | 'clear-all' | 'retry-all' | 'cancel' | 'cancel-all'; ids: number[]; text?: string; }; const actionItems = [ { - type: 'clear', - text: 'Clear', - icon: 'delete', + type: 'cancel', + text: 'Cancel', + icon: 'stop', + }, + { + type: 'cancel-all', + text: 'Stop Uploading', + icon: 'stop', }, { type: 'retry', text: 'Retry', icon: 'retry', }, - { - type: 'clear-all', - text: 'Clear All Records', - icon: 'delete', - }, { type: 'retry-all', text: 'Retry All', icon: 'retry', }, + { + type: 'clear', + text: 'Clear', + icon: 'delete', + }, + { + type: 'clear-all', + text: 'Clear All Records', + icon: 'delete', + }, ]; export const UploadActionButton = React.memo(function UploadActionButton({ @@ -42,6 +64,19 @@ export const UploadActionButton = React.memo(function UploadActionButton({ const dispatch = useAppDispatch(); const actionItem = actionItems.find((item) => item.type === type); + const onCancel = useCallback( + (ids: number[]) => { + dispatch( + updateUploadStatus({ + account: loginAccount, + ids, + status: 'CANCEL', + }), + ); + dispatch(cancelUploadingRequests({ ids })); + }, + [dispatch, loginAccount], + ); const onClear = (ids: number[]) => { dispatch(clearUploadRecords({ ids, loginAccount })); }; @@ -50,14 +85,18 @@ export const UploadActionButton = React.memo(function UploadActionButton({ }; const onClick = () => { switch (type) { - case 'clear': - case 'clear-all': - onClear(ids); + case 'cancel': + case 'cancel-all': + onCancel(ids); break; case 'retry': case 'retry-all': onRetry(ids); break; + case 'clear': + case 'clear-all': + onClear(ids); + break; default: break; } diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadingObjects.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadingObjects.tsx index 4b9cd11d..d1c33a5e 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadingObjects.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadingObjects.tsx @@ -19,7 +19,7 @@ import { UploadingPanelKey, useTaskManagementTab } from './useTaskManagementTab' import { IconFont } from '@/components/IconFont'; import { UploadingObjectsList } from './UploadingObjectsList'; -import { UploadObject } from '@/store/slices/global'; +import { UPLOADING_STATUSES, UPLOAD_FAILED_STATUSES, UploadObject } from '@/store/slices/global'; import { UploadActionButton } from './UploadActionButton'; interface UploadingObjectsProps {} @@ -36,15 +36,20 @@ export const UploadingObjects = memo(function UploadingOb panelKey: UploadingPanelKey; data: UploadObject[]; }) => { - if ([UploadingPanelKey.ALL, UploadingPanelKey.UPLOADING].includes(panelKey)) { - return null; - } return ( {panelKey === UploadingPanelKey.COMPLETE && ( item.id)} /> )} - {panelKey === UploadingPanelKey.FAILED && ( + {(panelKey === UploadingPanelKey.UPLOADING || panelKey === UploadingPanelKey.ALL) && ( + UPLOADING_STATUSES.includes(item.status)) + .map((item) => item.id)} + /> + )} + {(panelKey === UploadingPanelKey.FAILED || panelKey === UploadingPanelKey.STOPPED) && ( item.id)} /> item.id)} /> diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx index 191cf48a..22060ca0 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx @@ -1,10 +1,5 @@ import { DCTable } from '@/components/common/DCTable'; -import { - UPLOADING_STATUSES, - UPLOAD_FAILED_STATUSES, - UPLOAD_SUCCESS_STATUS, - UploadObject, -} from '@/store/slices/global'; +import { UploadObject } from '@/store/slices/global'; import { ColumnProps } from 'antd/es/table'; import React, { useState } from 'react'; import { NameItem } from './NameItem'; @@ -34,7 +29,6 @@ export const UploadingObjectsList = ({ data }: { data: UploadObject[] }) => { size={record.waitObject.size} msg={record.msg} status={record.status} - w={234} task={record} /> ); @@ -72,7 +66,8 @@ export const UploadingObjectsList = ({ data }: { data: UploadObject[] }) => { title: 'Action', width: 146, render: (record) => { - if (UPLOADING_STATUSES.includes(record.status)) { + const { status, id } = record; + if (['SEAL', 'SEALING'].includes(status)) { return ( -- @@ -80,15 +75,29 @@ export const UploadingObjectsList = ({ data }: { data: UploadObject[] }) => { ); } - if (UPLOAD_SUCCESS_STATUS === record.status) { - return ; - } else if (UPLOAD_FAILED_STATUSES.includes(record.status)) { - return ( - - - - - ); + switch (status) { + case 'FINISH': + return ; + + case 'CANCEL': + case 'ERROR': + return ( + + + + + ); + + case 'WAIT': + case 'HASH': + case 'HASHED': + case 'SIGN': + case 'SIGNED': + case 'UPLOAD': + return ; + + default: + return null; } }, }, diff --git a/apps/dcellar-web-ui/src/modules/upload/useTaskManagementTab.tsx b/apps/dcellar-web-ui/src/modules/upload/useTaskManagementTab.tsx index 19bcb443..1d63ff10 100644 --- a/apps/dcellar-web-ui/src/modules/upload/useTaskManagementTab.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/useTaskManagementTab.tsx @@ -1,14 +1,15 @@ import { useAppSelector } from '@/store'; -import { UploadObject } from '@/store/slices/global'; +import { UPLOADING_STATUSES, UploadObject } from '@/store/slices/global'; import { sortBy } from 'lodash-es'; import { useMemo, useState } from 'react'; export enum UploadingPanelKey { ALL = 'ALL', - UPLOADING = 'HASH-UPLOAD-SEAL', + UPLOADING = 'RETRY-WAIT-HASH-UPLOAD-SIGN-SEAL', + STOPPED = 'CANCEL', COMPLETE = 'FINISH', - FAILED = 'ERROR-CANCEL', + FAILED = 'ERROR', } export const useTaskManagementTab = () => { @@ -17,14 +18,14 @@ export const useTaskManagementTab = () => { const queue = sortBy(objectUploadQueue[loginAccount] || [], (o) => o.waitObject.time); - const { uploadingQueue, completeQueue, errorQueue } = useMemo(() => { - const uploadingQueue = queue?.filter((i) => - ['HASH', 'UPLOAD', 'SEAL', 'SEALING'].includes(i.status), - ); + const { uploadingQueue, stoppedQueue, completeQueue, errorQueue } = useMemo(() => { + const uploadingQueue = queue?.filter((i) => UPLOADING_STATUSES.includes(i.status)); const completeQueue = queue?.filter((i) => i.status === 'FINISH'); - const errorQueue = queue?.filter((i) => ['ERROR', 'CANCEL'].includes(i.status)); + const stoppedQueue = queue?.filter((i) => i.status === 'CANCEL'); + const errorQueue = queue?.filter((i) => ['ERROR'].includes(i.status)); return { uploadingQueue, + stoppedQueue, completeQueue, errorQueue, }; @@ -46,6 +47,11 @@ export const useTaskManagementTab = () => { key: UploadingPanelKey.UPLOADING, data: uploadingQueue, }, + { + title: 'Stopped', + key: UploadingPanelKey.STOPPED, + data: stoppedQueue, + }, { title: 'Complete', key: UploadingPanelKey.COMPLETE, diff --git a/apps/dcellar-web-ui/src/pages/_document.tsx b/apps/dcellar-web-ui/src/pages/_document.tsx index c150f269..e5bed808 100644 --- a/apps/dcellar-web-ui/src/pages/_document.tsx +++ b/apps/dcellar-web-ui/src/pages/_document.tsx @@ -26,7 +26,7 @@ export default function Document() { __html: `window.__ASSET_PREFIX = ${flatted.stringify(assetPrefix)}`, }} > - + diff --git a/apps/dcellar-web-ui/src/store/slices/global.ts b/apps/dcellar-web-ui/src/store/slices/global.ts index 697906cc..2526221a 100644 --- a/apps/dcellar-web-ui/src/store/slices/global.ts +++ b/apps/dcellar-web-ui/src/store/slices/global.ts @@ -105,6 +105,7 @@ export type UploadObject = { msg: string; progress: number; delegateUpload?: boolean; + abortController?: AbortController; }; export interface GlobalState { @@ -175,14 +176,32 @@ export const globalSlice = createSlice({ state, { payload, - }: PayloadAction<{ account: string; ids: number[]; status: UploadObject['status'] }>, + }: PayloadAction<{ + account: string; + ids: number[]; + status: UploadObject['status']; + extraFields?: Record>; + }>, ) { - const { account, ids, status } = payload; + const { account, ids, status, extraFields } = payload; const isErrorStatus = UPLOAD_FAILED_STATUSES.includes(status); const queue = state.objectUploadQueue[account] || []; - state.objectUploadQueue[account] = queue.map((q) => - ids.includes(q.id) ? { ...q, status, msg: isErrorStatus ? q.msg : '' } : q, - ); + state.objectUploadQueue[account] = queue.map((item) => { + if (ids.includes(item.id)) { + const updatedItem = { ...item, ...extraFields?.[item.id], status }; + + if (status === 'RETRY_CHECK') { + return { ...updatedItem, msg: '', progress: 0 }; + } + if (isErrorStatus) { + return { ...updatedItem, msg: item.msg }; + } + + return updatedItem; + } + + return item; + }); if (status === 'SEAL') { ids.forEach((id) => { @@ -222,15 +241,17 @@ export const globalSlice = createSlice({ task.status = 'ERROR'; task.msg = msg; }, - updateUploadTaskMsg( + updateUploadTaskErrorMsg( state, - { payload }: PayloadAction<{ account: string; id: number; msg: string }>, + { + payload, + }: PayloadAction<{ account: string; id: number; msg: string; status?: UploadObjectStatus }>, ) { - const { id, msg } = payload; + const { id, msg, status } = payload; const task = find(state.objectUploadQueue[payload.account], (t) => t.id === id); if (!task) return; - task.status = 'ERROR'; - task.msg = msg; + task.status = task.status !== 'CANCEL' ? status ?? 'ERROR' : 'CANCEL'; + task.msg = task.status !== 'CANCEL' ? msg : ''; }, updateWaitObjectStatus( state, @@ -429,7 +450,7 @@ export const { setBnbUsdtExchangeRate, updateWaitObjectStatus, updateWaitTaskMsg, - updateUploadTaskMsg, + updateUploadTaskErrorMsg, updateUploadChecksum, addToUploadQueue, updateUploadStatus, @@ -697,14 +718,25 @@ export const addDelegatedTasksToUploadQueue = }; export const setupUploadTaskErrorMsg = - ({ account, task, errorMsg }: { account: string; task: UploadObject; errorMsg: string }) => + ({ + account, + task, + errorMsg, + status, + }: { + account: string; + task: UploadObject; + errorMsg: string; + status?: UploadObjectStatus; + }) => async (dispatch: AppDispatch) => { // const isFolder = task.waitObject.name.endsWith('/'); dispatch( - updateUploadTaskMsg({ + updateUploadTaskErrorMsg({ account, id: task.id, msg: errorMsg || 'The object failed to be created.', + status, }), ); // isFolder && dispatch(cancelUploadFolder({ account, folderName: task.waitObject.name })); @@ -738,4 +770,15 @@ export const retryUploadTasks = }); }; +export const cancelUploadingRequests = + ({ ids }: { ids: number[] }) => + async (dispatch: AppDispatch, getState: GetState) => { + const { loginAccount } = getState().persist; + const uploadQueue = getState().global.objectUploadQueue[loginAccount] || _emptyUploadQueue; + const tasks = uploadQueue.filter((task) => ids.includes(task.id)); + tasks.forEach(async (task) => { + task.abortController && task.abortController.abort(); + }); + }; + export default globalSlice.reducer;