From f424a7c5bdb45771e1451f34f21fcecba7cdde4c Mon Sep 17 00:00:00 2001 From: aidencao Date: Wed, 26 Jul 2023 17:38:45 +0800 Subject: [PATCH 01/20] feat(dcellar-web-ui): add select object --- .../src/components/common/DCTable/index.tsx | 2 + apps/dcellar-web-ui/src/facade/bucket.ts | 2 +- .../src/modules/file/utils/file.ts | 29 ++++---- .../file/utils/generateGetObjectOptions.ts | 7 ++ .../src/modules/file/utils/index.tsx | 7 +- .../object/components/BatchOperations.tsx | 71 +++++++++++++++++++ .../modules/object/components/ObjectList.tsx | 43 +++++++++-- .../src/modules/object/index.tsx | 29 +++++--- .../dcellar-web-ui/src/store/slices/object.ts | 51 ++----------- 9 files changed, 163 insertions(+), 78 deletions(-) create mode 100644 apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx diff --git a/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx b/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx index 371a7992..849740d2 100644 --- a/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx +++ b/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx @@ -20,6 +20,7 @@ export type FixedType = 'left' | 'right' | boolean; const theme: ThemeConfig = { token: { + colorPrimary: '#00BA34', colorBorderSecondary: '#e6e8ea', colorLink: '#00BA34', colorLinkActive: '#00BA34', @@ -197,6 +198,7 @@ const Container = styled.div` padding-top: 13px; padding-bottom: 13px; } + .ant-table-tbody > tr.ant-table-row-selected > td, .ant-table-tbody > tr.ant-table-row:hover > td { background: rgba(0, 186, 52, 0.1); } diff --git a/apps/dcellar-web-ui/src/facade/bucket.ts b/apps/dcellar-web-ui/src/facade/bucket.ts index c986677b..c3127aa9 100644 --- a/apps/dcellar-web-ui/src/facade/bucket.ts +++ b/apps/dcellar-web-ui/src/facade/bucket.ts @@ -8,7 +8,7 @@ import { resolve } from '@/facade/common'; import { BucketProps } from '@bnb-chain/greenfield-chain-sdk/dist/cjs/types'; import { IObjectResultType } from '@bnb-chain/greenfield-chain-sdk'; -export const quotaRemains = (quota: IQuotaProps, payload: string) => { +export const quotaRemains = (quota: IQuotaProps, payload: string | number) => { const { freeQuota, readQuota, consumedQuota } = quota; return !BigNumber(freeQuota).plus(readQuota).minus(consumedQuota).minus(payload).isNegative(); }; diff --git a/apps/dcellar-web-ui/src/modules/file/utils/file.ts b/apps/dcellar-web-ui/src/modules/file/utils/file.ts index 7b1532ae..37532c52 100644 --- a/apps/dcellar-web-ui/src/modules/file/utils/file.ts +++ b/apps/dcellar-web-ui/src/modules/file/utils/file.ts @@ -1,10 +1,10 @@ -import { isValidUrl } from "@bnb-chain/greenfield-chain-sdk"; +import { isValidUrl } from '@bnb-chain/greenfield-chain-sdk'; const IP_REGEX = /^(\d+\.){3}\d+$/g; const ALLOW_REGEX = /^[a-z0-9][a-z0-9.\-]{1,61}[a-z0-9]$/g; -const dotdotComponent = ".." -const dotComponent = "." -const slashSeparator = "/"; +const dotdotComponent = '..'; +const dotComponent = '.'; +const slashSeparator = '/'; export interface getObjectPropsType { bucketName: string; @@ -20,6 +20,7 @@ export interface requestParamsType { url: string; headers: Headers; method: string; + params: URLSearchParams; } export interface putObjectPropsType { @@ -44,7 +45,7 @@ const hasBadPathComponent = (path: string): boolean => { } } return false; -} +}; const isUTF8 = (str: string): boolean => { try { @@ -53,7 +54,7 @@ const isUTF8 = (str: string): boolean => { } catch { return false; } -} +}; const validateBucketName = (bucketName?: string) => { if (!bucketName) { throw new Error('Bucket name is empty, please check.'); @@ -92,27 +93,27 @@ const validateObjectName = (objectName?: string) => { throw new Error('Object name is limited to 1024 at most, please check.'); } if (hasBadPathComponent(objectName)) { - throw new Error('Object name error, please check.') + throw new Error('Object name error, please check.'); } if (!isUTF8(objectName)) { - throw new Error('Object name is not in UTF-8 format, please check.') + throw new Error('Object name is not in UTF-8 format, please check.'); } if (objectName.includes(`//`)) { - throw new Error(`Object name that contains a "//" is not supported`) + throw new Error(`Object name that contains a "//" is not supported`); } }; -const generateUrlByBucketName = (endpoint='', bucketName: string) => { +const generateUrlByBucketName = (endpoint = '', bucketName: string) => { if (!isValidUrl(endpoint)) { - throw new Error('Invalid endpoint') + throw new Error('Invalid endpoint'); } validateBucketName(bucketName); - const {protocol} = new URL(endpoint); + const { protocol } = new URL(endpoint); return endpoint.replace(`${protocol}//`, `${protocol}//${bucketName}.`); -} +}; export { generateUrlByBucketName, validateBucketName, validateObjectName, isUTF8, hasBadPathComponent, -} \ No newline at end of file +}; diff --git a/apps/dcellar-web-ui/src/modules/file/utils/generateGetObjectOptions.ts b/apps/dcellar-web-ui/src/modules/file/utils/generateGetObjectOptions.ts index c06e4d0e..3cd3760d 100644 --- a/apps/dcellar-web-ui/src/modules/file/utils/generateGetObjectOptions.ts +++ b/apps/dcellar-web-ui/src/modules/file/utils/generateGetObjectOptions.ts @@ -28,9 +28,16 @@ export const generateGetObjectOptions = async ( 'X-Gnfd-User-Address': userAddress, 'X-Gnfd-App-Domain': domain, }); + + const params = new URLSearchParams(); + params.append('Authorization', body?.authorization || ''); + params.append('X-Gnfd-User-Address', userAddress); + params.append('X-Gnfd-App-Domain', domain); + return { url, headers, method: 'get', + params, }; }; diff --git a/apps/dcellar-web-ui/src/modules/file/utils/index.tsx b/apps/dcellar-web-ui/src/modules/file/utils/index.tsx index 04f49195..19eaf265 100644 --- a/apps/dcellar-web-ui/src/modules/file/utils/index.tsx +++ b/apps/dcellar-web-ui/src/modules/file/utils/index.tsx @@ -289,7 +289,7 @@ const renderInsufficientBalance = ( ); }; -const directlyDownload = (url: string) => { +const directlyDownload = (url: string, name?: string) => { if (!url) { toast.error({ description: 'Download url not existed. Please check.', @@ -297,7 +297,8 @@ const directlyDownload = (url: string) => { } const link = document.createElement('a'); link.href = url; - link.download = ''; + link.download = name || ''; + link.target = '_blank'; document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -414,5 +415,5 @@ export { saveFileByAxiosResponse, truncateFileName, renderPrelockedFeeValue, - getBuiltInLink + getBuiltInLink, }; diff --git a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx new file mode 100644 index 00000000..013d1310 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx @@ -0,0 +1,71 @@ +import React, { memo } from 'react'; +import { ActionButton } from '@/modules/file/components/FileTable'; +import { DeleteIcon, DownloadIcon } from '@totejs/icons'; +import { Text } from '@totejs/uikit'; +import { useAppDispatch, useAppSelector } from '@/store'; +import { E_NO_QUOTA, E_OFF_CHAIN_AUTH, E_UNKNOWN } from '@/facade/error'; +import { OBJECT_ERROR_TYPES, ObjectErrorType } from '@/modules/object/ObjectError'; +import { setStatusDetail } from '@/store/slices/object'; +import { useOffChainAuth } from '@/hooks/useOffChainAuth'; +import { useMount } from 'ahooks'; +import { setupBucketQuota } from '@/store/slices/bucket'; +import { quotaRemains } from '@/facade/bucket'; + +interface BatchOperationsProps {} + +export const BatchOperations = memo(function BatchOperations() { + const selectedRowKeys = useAppSelector((root) => root.object.selectedRowKeys); + const selected = selectedRowKeys.length; + const dispatch = useAppDispatch(); + const { setOpenAuthModal } = useOffChainAuth(); + const { loginAccount } = useAppSelector((root) => root.persist); + const { bucketName, objects, path, primarySp } = useAppSelector((root) => root.object); + const quotas = useAppSelector((root) => root.bucket.quotas); + const quotaData = quotas[bucketName]; + + useMount(() => { + dispatch(setupBucketQuota(bucketName)); + }); + + const onError = (type: string) => { + if (type === E_OFF_CHAIN_AUTH) { + return setOpenAuthModal(); + } + const errorData = OBJECT_ERROR_TYPES[type as ObjectErrorType] + ? OBJECT_ERROR_TYPES[type as ObjectErrorType] + : OBJECT_ERROR_TYPES[E_UNKNOWN]; + + dispatch(setStatusDetail(errorData)); + }; + + const onBatchDownload = async () => { + const items = objects[path].filter((i) => selectedRowKeys.includes(i.objectName)); + let remainQuota = quotaRemains( + quotaData, + items.reduce((x, y) => x + y.payloadSize, 0), + ); + if (!remainQuota) return onError(E_NO_QUOTA); + const operator = primarySp.operatorAddress; + // const { seedString } = await dispatch(getSpOffChainData(loginAccount, operator)); + // const domain = getDomain(); + // todo + }; + + return ( + <> + + {selected} File{selected > 1 && 's'} Selected{' '} + + + + + + + + + ); +}); diff --git a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx index ee1bdf20..04b3a31a 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx @@ -1,6 +1,7 @@ -import React, { memo, useState } from 'react'; +import React, { Key, memo, useState } from 'react'; import { useAppDispatch, useAppSelector } from '@/store'; import { + _getAllList, ObjectItem, selectObjectList, selectPathCurrent, @@ -12,21 +13,20 @@ import { setEditDownload, setEditShare, setRestoreCurrent, + setSelectedRowKeys, setStatusDetail, setupDummyFolder, setupListObjects, - _getAllList, } from '@/store/slices/object'; -import { chunk, reverse, sortBy } from 'lodash-es'; +import { chunk, reverse, sortBy, uniq, without, xor } from 'lodash-es'; import { ColumnProps } from 'antd/es/table'; import { getSpOffChainData, - setAccountConfig, SorterType, updateObjectPageSize, updateObjectSorter, } from '@/store/slices/persist'; -import { AlignType, DCTable, UploadStatus, SortIcon, SortItem } from '@/components/common/DCTable'; +import { AlignType, DCTable, SortIcon, SortItem, UploadStatus } from '@/components/common/DCTable'; import { formatTime, getMillisecond } from '@/utils/time'; import { Loading } from '@/components/common/Loading'; import { ListEmpty } from '@/modules/object/components/ListEmpty'; @@ -83,7 +83,9 @@ export const ObjectList = memo(function ObjectList() { const [rowIndex, setRowIndex] = useState(-1); const [deleteFolderNotEmpty, setDeleteFolderNotEmpty] = useState(false); - const { bucketName, prefix, path, objectsInfo } = useAppSelector((root) => root.object); + const { bucketName, prefix, path, objectsInfo, selectedRowKeys } = useAppSelector( + (root) => root.object, + ); const currentPage = useAppSelector(selectPathCurrent); const { bucketInfo, discontinue, owner } = useAppSelector((root) => root.bucket); const { spInfo } = useAppSelector((root) => root.sp); @@ -335,6 +337,34 @@ export const ObjectList = memo(function ObjectList() { dispatch(updateObjectPageSize(pageSize)); }; + const onSelectChange = (item: ObjectItem) => { + dispatch(setSelectedRowKeys(xor(selectedRowKeys, [item.objectName]))); + }; + + const onSelectAllChange = ( + selected: boolean, + selectedRows: ObjectItem[], + changeRows: ObjectItem[], + ) => { + const _changeRows = changeRows.filter(Boolean).map((i) => i.objectName); + if (selected) { + dispatch(setSelectedRowKeys(uniq(selectedRowKeys.concat(_changeRows)))); + } else { + dispatch(setSelectedRowKeys(without(selectedRowKeys, ..._changeRows))); + } + }; + + const rowSelection = { + checkStrictly: true, + selectedRowKeys, + onSelect: onSelectChange, + onSelectAll: onSelectAllChange, + getCheckboxProps: (record: ObjectItem) => ({ + disabled: record.folder || record.objectStatus !== 1, // Column configuration not to be checked + name: record.name, + }), + }; + const refetch = async (name?: string) => { if (!primarySpAddress) return; const { seedString } = await dispatch(getSpOffChainData(loginAccount, primarySpAddress)); @@ -374,6 +404,7 @@ export const ObjectList = memo(function ObjectList() { /> )} }} rowKey="objectName" columns={columns} diff --git a/apps/dcellar-web-ui/src/modules/object/index.tsx b/apps/dcellar-web-ui/src/modules/object/index.tsx index 79b91002..3f40523b 100644 --- a/apps/dcellar-web-ui/src/modules/object/index.tsx +++ b/apps/dcellar-web-ui/src/modules/object/index.tsx @@ -1,6 +1,6 @@ import { useRouter } from 'next/router'; import { useAppDispatch, useAppSelector } from '@/store'; -import { useAsyncEffect, useWhyDidYouUpdate } from 'ahooks'; +import { useAsyncEffect } from 'ahooks'; import { setBucketStatus, setupBucket } from '@/store/slices/bucket'; import Head from 'next/head'; import { @@ -12,17 +12,19 @@ import { import { ObjectBreadcrumb } from '@/modules/object/components/ObjectBreadcrumb'; import { isEmpty, last } from 'lodash-es'; import { NewObject } from '@/modules/object/components/NewObject'; -import { Tooltip } from '@totejs/uikit'; +import { Text, Tooltip } from '@totejs/uikit'; import { selectObjectList, setFolders, setPrimarySp } from '@/store/slices/object'; import { ObjectList } from '@/modules/object/components/ObjectList'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { SpItem } from '@/store/slices/sp'; +import { BatchOperations } from '@/modules/object/components/BatchOperations'; export const ObjectsPage = () => { const dispatch = useAppDispatch(); const { spInfo } = useAppSelector((root) => root.sp); const { bucketInfo } = useAppSelector((root) => root.bucket); const { loginAccount } = useAppSelector((root) => root.persist); + const selectedRowKeys = useAppSelector((root) => root.object.selectedRowKeys); const objectList = useAppSelector(selectObjectList); const router = useRouter(); const { path } = router.query; @@ -61,6 +63,8 @@ export const ObjectsPage = () => { await router.replace('/no-bucket?err=noBucket'); }, [bucketName, dispatch]); + const selected = selectedRowKeys.length; + return ( @@ -69,13 +73,18 @@ export const ObjectsPage = () => { - 40 ? 'visible' : 'hidden'} - > - {title} - + {selected > 0 ? ( + + ) : ( + 40 ? 'visible' : 'hidden'} + > + {title} + + )} + {!!objectList.length && ( void; - onClose: () => void; - onOpen: () => void; -}; export type TStatusDetail = { icon: string; @@ -36,28 +29,6 @@ export type TStatusDetail = { buttonOnClick?: () => void; }; -export type TFileItem = { - name: string; - size: string; - type: string; - calHash?: THashResult; - status: 'WAIT_CHECKING' | 'WAIT_UPLOAD' | 'UPLOADING' | 'UPLOAD_SUCCESS' | 'UPLOAD_FAIL'; - errorMsg?: string; - txnHash?: string; -}; - -export type TEditUpload = { - isOpen: boolean; - fileInfos: TFileItem[]; - visibility: VisibilityType; -}; -export type TUploading = { - isOpen: boolean; - isLoading: boolean; - fileInfos: TFileItem[]; - visibility: VisibilityType; -}; - export type ObjectActionType = 'view' | 'download' | ''; export interface ObjectState { @@ -79,7 +50,7 @@ export interface ObjectState { primarySp: SpItem; statusDetail: TStatusDetail; editUpload: number; - uploading: TUploading; + selectedRowKeys: Key[]; } const initialState: ObjectState = { @@ -101,12 +72,7 @@ const initialState: ObjectState = { statusDetail: {} as TStatusDetail, primarySp: {} as SpItem, editUpload: 0, - uploading: { - visibility: 2, - isOpen: false, - fileInfos: [], - isLoading: false, - }, + selectedRowKeys: [], }; export const SINGLE_FILE_MAX_SIZE = 256 * 1024 * 1024; @@ -114,6 +80,9 @@ export const objectSlice = createSlice({ name: 'object', initialState, reducers: { + setSelectedRowKeys(state, { payload }: PayloadAction) { + state.selectedRowKeys = payload; + }, updateObjectVisibility( state, { payload }: PayloadAction<{ object: ObjectItem; visibility: number }>, @@ -197,12 +166,6 @@ export const objectSlice = createSlice({ setEditCancel(state, { payload }: PayloadAction) { state.editCancel = payload; }, - setUploading(state, { payload }: PayloadAction>) { - state.uploading = { - ...state.uploading, - ...payload, - }; - }, setEditShare(state, { payload }: PayloadAction) { state.editShare = payload; }, @@ -361,10 +324,10 @@ export const { setEditShare, setEditUpload, setEditCancel, - setUploading, updateObjectStatus, setDummyFolder, updateObjectVisibility, + setSelectedRowKeys, } = objectSlice.actions; export default objectSlice.reducer; From 3d8c012306ffc5f3e1f2855919be618cca415dfb Mon Sep 17 00:00:00 2001 From: aidencao Date: Wed, 26 Jul 2023 17:47:04 +0800 Subject: [PATCH 02/20] feat(dcellar-web-ui): add select object --- apps/dcellar-web-ui/src/modules/file/utils/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dcellar-web-ui/src/modules/file/utils/index.tsx b/apps/dcellar-web-ui/src/modules/file/utils/index.tsx index 19eaf265..456540c9 100644 --- a/apps/dcellar-web-ui/src/modules/file/utils/index.tsx +++ b/apps/dcellar-web-ui/src/modules/file/utils/index.tsx @@ -298,7 +298,6 @@ const directlyDownload = (url: string, name?: string) => { const link = document.createElement('a'); link.href = url; link.download = name || ''; - link.target = '_blank'; document.body.appendChild(link); link.click(); document.body.removeChild(link); From 55d394bfac25b8132c43712127da49177addc771 Mon Sep 17 00:00:00 2001 From: devinxl Date: Tue, 25 Jul 2023 14:13:10 +0800 Subject: [PATCH 03/20] feat(dcellar-web-ui): add temp account --- apps/dcellar-web-ui/package.json | 6 +- .../src/components/common/DCDrawer/index.tsx | 10 +- .../Header/{GasList.tsx => GasObjects.tsx} | 8 +- .../components/layout/Header/GlobalTasks.tsx | 111 ++++++-- .../src/components/layout/Header/index.tsx | 4 +- apps/dcellar-web-ui/src/facade/account.ts | 69 +++++ apps/dcellar-web-ui/src/facade/error.ts | 14 + apps/dcellar-web-ui/src/facade/object.ts | 5 +- .../src/modules/file/constant.ts | 5 +- .../modules/file/utils/genCreateObjectTx.ts | 4 +- .../src/modules/object/ObjectError.tsx | 5 + .../object/components/CancelObject.tsx | 4 +- .../object/components/CreateFolder.tsx | 4 +- .../object/components/DeleteObject.tsx | 4 +- .../modules/object/components/NewObject.tsx | 167 +++++++---- .../src/modules/upload/ListItem.tsx | 66 +++++ .../src/modules/upload/SimulateFee.tsx | 104 +++++-- .../src/modules/upload/TaskManagement.tsx | 24 +- .../src/modules/upload/UploadObjects.tsx | 265 +++++++----------- .../src/modules/upload/UploadingObjects.tsx | 182 ++++++++---- .../modules/upload/useTaskManagementTab.tsx | 80 ++++++ .../src/modules/upload/useUploadTab.tsx | 49 ++++ .../dcellar-web-ui/src/store/slices/global.ts | 205 ++++++++++---- apps/dcellar-web-ui/src/utils/sp/index.ts | 28 ++ common/config/rush/pnpm-lock.yaml | 45 ++- 25 files changed, 1043 insertions(+), 425 deletions(-) rename apps/dcellar-web-ui/src/components/layout/Header/{GasList.tsx => GasObjects.tsx} (51%) create mode 100644 apps/dcellar-web-ui/src/modules/upload/ListItem.tsx create mode 100644 apps/dcellar-web-ui/src/modules/upload/useTaskManagementTab.tsx create mode 100644 apps/dcellar-web-ui/src/modules/upload/useUploadTab.tsx diff --git a/apps/dcellar-web-ui/package.json b/apps/dcellar-web-ui/package.json index 758f9ce6..a9197e49 100644 --- a/apps/dcellar-web-ui/package.json +++ b/apps/dcellar-web-ui/package.json @@ -18,15 +18,15 @@ "ahooks": "3.7.7", "hash-wasm": "4.9.0", "@babel/core": "^7.20.12", - "@bnb-chain/greenfield-chain-sdk": "0.0.0-snapshot-20230724100555", - "@bnb-chain/greenfield-cosmos-types": "0.4.0-alpha.15", + "@bnb-chain/greenfield-chain-sdk": "0.0.0-snapshot-20230725025153", + "@bnb-chain/greenfield-cosmos-types": "0.4.0-alpha.13", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@next/bundle-analyzer": "^13.1.6", "@tanstack/react-table": "^8.7.9", "@tanstack/react-virtual": "3.0.0-alpha.0", "@totejs/icons": "^2.10.0", - "@totejs/uikit": "~2.44.5", + "@totejs/uikit": "~2.49.1", "axios": "^1.3.2", "axios-retry": "^3.4.0", "bignumber.js": "^9.1.1", diff --git a/apps/dcellar-web-ui/src/components/common/DCDrawer/index.tsx b/apps/dcellar-web-ui/src/components/common/DCDrawer/index.tsx index f0995546..05a1e419 100644 --- a/apps/dcellar-web-ui/src/components/common/DCDrawer/index.tsx +++ b/apps/dcellar-web-ui/src/components/common/DCDrawer/index.tsx @@ -22,7 +22,15 @@ export const DCDrawer = (props: DCDrawerProps) => { return ( - + {children} diff --git a/apps/dcellar-web-ui/src/components/layout/Header/GasList.tsx b/apps/dcellar-web-ui/src/components/layout/Header/GasObjects.tsx similarity index 51% rename from apps/dcellar-web-ui/src/components/layout/Header/GasList.tsx rename to apps/dcellar-web-ui/src/components/layout/Header/GasObjects.tsx index 20781488..e522f10a 100644 --- a/apps/dcellar-web-ui/src/components/layout/Header/GasList.tsx +++ b/apps/dcellar-web-ui/src/components/layout/Header/GasObjects.tsx @@ -1,12 +1,12 @@ import { useAppDispatch } from "@/store"; -import { setupGasList } from "@/store/slices/global"; +import { setupGasObjects } from "@/store/slices/global"; import { useAsyncEffect } from "ahooks"; -export const GasList = () => { +export const GasObjects = () => { const dispatch = useAppDispatch(); useAsyncEffect(async () => { - dispatch(setupGasList()); - }, [dispatch, setupGasList]); + dispatch(setupGasObjects()); + }, [dispatch, setupGasObjects]); return <> } \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/components/layout/Header/GlobalTasks.tsx b/apps/dcellar-web-ui/src/components/layout/Header/GlobalTasks.tsx index 83dfbb8b..6e8991df 100644 --- a/apps/dcellar-web-ui/src/components/layout/Header/GlobalTasks.tsx +++ b/apps/dcellar-web-ui/src/components/layout/Header/GlobalTasks.tsx @@ -4,11 +4,13 @@ import { progressFetchList, selectHashTask, selectUploadQueue, - updateHashChecksum, updateHashStatus, + updateHashTaskMsg, + updateUploadChecksum, updateUploadMsg, updateUploadProgress, updateUploadStatus, + updateUploadTaskMsg, UploadFile, uploadQueueAndRefresh, } from '@/store/slices/global'; @@ -20,60 +22,123 @@ import { generatePutObjectOptions } from '@/modules/file/utils/generatePubObject import axios from 'axios'; import { headObject, queryLockFee } from '@/facade/object'; import Long from 'long'; -import { formatLockFee } from '@/utils/object'; +import { TCreateObject } from '@bnb-chain/greenfield-chain-sdk'; +import { reverseVisibilityType } from '@/utils/constant'; +import { genCreateObjectTx } from '@/modules/file/utils/genCreateObjectTx'; +import { resolve } from '@/facade/common'; +import { broadcastFault, createTxFault, simulateFault } from '@/facade/error'; +import { isEmpty } from 'lodash-es'; interface GlobalTasksProps {} export const GlobalTasks = memo(function GlobalTasks() { const dispatch = useAppDispatch(); const { loginAccount } = useAppSelector((root) => root.persist); - const { spInfo } = useAppSelector((root) => root.sp); - const { primarySp } = useAppSelector((root) => root.object); - const hashTask = useAppSelector(selectHashTask); + const { spInfo: spInfos } = useAppSelector((root) => root.sp); + const { primarySp, bucketName} = useAppSelector((root) => root.object); + const { tmpAccount } = useAppSelector((root) => root.global); + const { sps: globalSps } = useAppSelector((root) => root.sp); + const hashTask = useAppSelector(selectHashTask(loginAccount)); + console.log('hashTask', hashTask); const checksumApi = useChecksumApi(); const [counter, setCounter] = useState(0); const queue = useAppSelector(selectUploadQueue(loginAccount)); const upload = queue.filter((t) => t.status === 'UPLOAD'); - const wait = queue.filter((t) => t.status === 'WAIT'); + const ready = queue.filter((t) => t.status === 'READY'); const offset = 3 - upload.length; const select3Task = useMemo(() => { if (offset <= 0) return []; - return wait.slice(0, offset).map((p) => p.id); - }, [offset, wait]); + return ready.slice(0, offset).map((p) => p.id); + }, [offset, ready]); const sealQueue = queue.filter((q) => q.status === 'SEAL').map((s) => s.id); useAsyncEffect(async () => { if (!hashTask) return; - dispatch(updateHashStatus({ id: hashTask.id, status: 'HASH' })); - const res = await checksumApi?.generateCheckSumV2(hashTask.file); - const params = { - primarySpAddress: primarySp.operatorAddress, - createAt: Long.fromInt(Math.floor(hashTask.id / 1000)), - payloadSize: Long.fromInt(hashTask.file.size), - }; - const [data, error] = await queryLockFee(params); + dispatch(updateUploadStatus({ ids: [hashTask.id], status: 'HASH', account: loginAccount })); + const res = await checksumApi?.generateCheckSumV2(hashTask.file.file); + if (isEmpty(res)) { + dispatch(updateUploadMsg({ id: hashTask.id, msg: 'calculating hash error', account: loginAccount })); + return; + } const { expectCheckSums } = res!; dispatch( - updateHashChecksum({ + updateUploadChecksum({ + account: loginAccount, id: hashTask.id, checksum: expectCheckSums, - lockFee: formatLockFee(data?.amount), }), ); }, [hashTask, dispatch]); // todo refactor const runUploadTask = async (task: UploadFile) => { + // 1. get approval from sp + debugger; const domain = getDomain(); const { seedString } = await dispatch(getSpOffChainData(loginAccount, task.sp)); + const secondarySpAddresses = globalSps + .filter((item: any) => item.operator !== primarySp.operatorAddress) + .map((item: any) => item.operatorAddress); + const spInfo = { + endpoint: primarySp.endpoint, + primarySp: primarySp.operatorAddress, + sealAddress: primarySp.sealAddress, + secondarySpAddresses, + }; + const finalName = [...task.prefixFolders, task.file.name].join('/'); + const createObjectPayload: TCreateObject = { + bucketName, + objectName: finalName, + creator: tmpAccount.address, + visibility: reverseVisibilityType[task.visibility], + fileType: task.file.type || 'application/octet-stream', + contentLength: task.file.size, + expectCheckSums: task.checksum, + spInfo, + signType: 'authTypeV1', + privateKey: tmpAccount.privateKey, + }; + const [createObjectTx, _createError] = await genCreateObjectTx(createObjectPayload).then( + resolve, + createTxFault, + ); + if (_createError) { + return dispatch(updateUploadTaskMsg({ + account: loginAccount, + id: task.id, + msg: _createError, + })) + } + + const [simulateInfo, simulateError] = await createObjectTx! + .simulate({ + denom: 'BNB', + }) + .then(resolve, simulateFault); + + const broadcastPayload = { + denom: 'BNB', + gasLimit: Number(simulateInfo?.gasLimit), + gasPrice: simulateInfo?.gasPrice || '5000000000', + payer: tmpAccount.address, + granter: loginAccount, + privateKey: tmpAccount.privateKey, + }; + const [res, error] = await createObjectTx! + .broadcast(broadcastPayload) + .then(resolve, broadcastFault); + + if (error) { + console.log('error', error) + } const uploadOptions = await generatePutObjectOptions({ bucketName: task.bucketName, - objectName: [...task.folders, task.file.name].join('/'), + objectName: [...task.prefixFolders, task.file.name].join('/'), body: task.file.file, - endpoint: spInfo[task.sp].endpoint, - txnHash: task.createHash, + endpoint: spInfos[task.sp].endpoint, + txnHash: res?.transactionHash || '', userAddress: loginAccount, domain, seedString, @@ -107,8 +172,8 @@ export const GlobalTasks = memo(function GlobalTasks() { const _tasks = await Promise.all( tasks.map(async (task) => { - const { bucketName, folders, file } = task; - const objectName = [...folders, file.name].join('/'); + const { bucketName, prefixFolders, file } = task; + const objectName = [...prefixFolders, file.name].join('/'); const objectInfo = await headObject(bucketName, objectName); if (!objectInfo || ![0, 1].includes(objectInfo.objectStatus)) { dispatch( diff --git a/apps/dcellar-web-ui/src/components/layout/Header/index.tsx b/apps/dcellar-web-ui/src/components/layout/Header/index.tsx index 5c1e5bd4..d878a06f 100644 --- a/apps/dcellar-web-ui/src/components/layout/Header/index.tsx +++ b/apps/dcellar-web-ui/src/components/layout/Header/index.tsx @@ -16,7 +16,7 @@ import { useDebounceEffect, useMount } from 'ahooks'; import { setupBnbPrice, setupTmpAvailableBalance, setupTmpLockFee } from '@/store/slices/global'; import { useAppDispatch, useAppSelector } from '@/store'; import { useLogin } from '@/hooks/useLogin'; -import { GasList } from './GasList'; +import { GasObjects } from './GasObjects'; import { TaskManagement } from '@/modules/upload/TaskManagement'; import { GlobalTasks } from '@/components/layout/Header/GlobalTasks'; @@ -62,7 +62,7 @@ export const Header = ({ taskManagement = true }: { taskManagement?: boolean }) <> - + ({ balance: { amount: '0', denom } })); return balance!; }; + +export const createTmpAccount = async ({address, bucketName, amount}: any): Promise => { + // 1. create temporary account + const wallet = Wallet.createRandom(); + console.log('wallet', wallet.address, wallet.privateKey); + + // 2. allow temporary account to submit specified tx and amount + const client = await getClient(); + const grantAllowanceTx = await client.feegrant.grantAllowance({ + granter: address, + grantee: wallet.address, + allowedMessages: [MsgCreateObjectTypeUrl], + amount: parseEther(amount || '0.1').toString(), + denom: 'BNB', + }); + + // 3. Put bucket policy so that the temporary account can create objects within this bucket + const statement: PermissionTypes.Statement = { + effect: PermissionTypes.Effect.EFFECT_ALLOW, + actions: [PermissionTypes.ActionType.ACTION_CREATE_OBJECT], + resources: [GRNToString(newBucketGRN(bucketName))], + }; + const [putPolicyTx, putPolicyError] = await client.bucket.putBucketPolicy(bucketName, { + operator: address, + statements: [statement], + principal: { + type: PermissionTypes.PrincipalType.PRINCIPAL_TYPE_GNFD_ACCOUNT, + value: wallet.address, + }, + }).then(resolve, commonFault); + if (!putPolicyTx) { + return [null, putPolicyError]; + } + + // 4. broadcast txs include 2 msg + const txs = await client.basic.multiTx([grantAllowanceTx, putPolicyTx]); + const [simulateInfo, simulateError] = await txs.simulate({ + denom: 'BNB', + }).then(resolve, simulateFault); + if (simulateError) { + return [null, simulateError]; + } + + console.log('simuluateInfo', simulateInfo); + + const [res, error] = await txs.broadcast({ + denom: 'BNB', + gasLimit: Number(210000), + gasPrice: '5000000000', + payer: address, + granter: '', + }).then(resolve, broadcastFault); + + if (res && res.code !== 0 || error) { + return [null, error || UNKNOWN_ERROR]; + } + + return [{ + address: wallet.address, + privateKey: wallet.privateKey + }, null]; +} \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/facade/error.ts b/apps/dcellar-web-ui/src/facade/error.ts index 02159cc4..d1642718 100644 --- a/apps/dcellar-web-ui/src/facade/error.ts +++ b/apps/dcellar-web-ui/src/facade/error.ts @@ -4,6 +4,7 @@ export type ErrorMsg = string; export const E_GET_GAS_FEE_LACK_BALANCE_ERROR = `Current available balance is not enough for gas simulation, please check.`; export const E_UNKNOWN_ERROR = `Unknown error. Please try again later.`; +export const E_SP_PRICE_FAILED = `Get SP storage price failed.`; export const E_USER_REJECT_STATUS_NUM = '4001'; export const E_NOT_FOUND = 'NOT_FOUND'; export const E_PERMISSION_DENIED = 'PERMISSION_DENIED'; @@ -24,6 +25,7 @@ export const E_CAL_OBJECT_HASH = 'CAL_OBJECT_HASH'; export const E_OBJECT_NAME_EXISTS = 'OBJECT_NAME_EXISTS'; export const E_ACCOUNT_BALANCE_NOT_ENOUGH = 'ACCOUNT_BALANCE_NOT_ENOUGH'; export const E_NO_PERMISSION = 'NO_PERMISSION'; +export const E_SP_STORAGE_PRICE_FAILED = 'SP_STORAGE_PRICE_FAILED'; export declare class BroadcastTxError extends Error { readonly code: number; readonly codespace: string; @@ -81,3 +83,15 @@ export const commonFault = (e: any): ErrorResponse => { } return [null, E_UNKNOWN_ERROR]; }; + +export const queryLockFeeFault = (e: any): ErrorResponse => { + console.log('e', e); + if (e?.message.includes('storage price')) { + return [null, E_SP_PRICE_FAILED]; + } + if (e?.message) { + return [null, e?.message]; + } + + return [null, E_UNKNOWN_ERROR]; +} \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/facade/object.ts b/apps/dcellar-web-ui/src/facade/object.ts index d50d880f..d73bcd32 100644 --- a/apps/dcellar-web-ui/src/facade/object.ts +++ b/apps/dcellar-web-ui/src/facade/object.ts @@ -14,6 +14,7 @@ import { E_UNKNOWN, ErrorMsg, ErrorResponse, + queryLockFeeFault, simulateFault, } from '@/facade/error'; import { getObjectInfoAndBucketQuota, resolve } from '@/facade/common'; @@ -41,6 +42,7 @@ import { QueryLockFeeRequest, } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/query'; import { signTypedDataV4 } from '@/utils/signDataV4'; +import BigNumber from 'bignumber.js'; export type DeliverResponse = Awaited>; @@ -249,8 +251,7 @@ export const cancelCreateObject = async (params: any, Connector: any): Promise { const client = await getClient(); - const res = await client.storage.queryLockFee(params); - return await client.storage.queryLockFee(params).then(resolve, commonFault); + return await client.storage.queryLockFee(params).then(resolve, queryLockFeeFault); }; export const headObject = async (bucketName: string, objectName: string) => { diff --git a/apps/dcellar-web-ui/src/modules/file/constant.ts b/apps/dcellar-web-ui/src/modules/file/constant.ts index 69768819..8c7150b6 100644 --- a/apps/dcellar-web-ui/src/modules/file/constant.ts +++ b/apps/dcellar-web-ui/src/modules/file/constant.ts @@ -19,6 +19,7 @@ const UNKNOWN_ERROR_URL = `${assetPrefix}/images/files/unknown.svg`; // status_TITLE const FILE_TITLE_UPLOADING = 'Uploading File'; const OBJECT_TITLE_CREATING = 'Creating Object'; +const OBJECT_AUTH_TEMP_ACCOUNT_CREATING = 'Uploading'; const FILE_TITLE_DOWNLOADING = 'Downloading File'; const FILE_TITLE_DELETING = 'Deleting File'; const FOLDER_TITLE_DELETING = 'Deleting Folder'; @@ -125,7 +126,5 @@ export { UNKNOWN_ERROR_URL, FILE_UPLOAD_STATIC_URL, OBJECT_TITLE_CREATING, - FILE_ACCESS_URL, - FILE_STATUS_ACCESS, - FILE_ACCESS, + OBJECT_AUTH_TEMP_ACCOUNT_CREATING, }; diff --git a/apps/dcellar-web-ui/src/modules/file/utils/genCreateObjectTx.ts b/apps/dcellar-web-ui/src/modules/file/utils/genCreateObjectTx.ts index 8f63b3c5..38d66fd5 100644 --- a/apps/dcellar-web-ui/src/modules/file/utils/genCreateObjectTx.ts +++ b/apps/dcellar-web-ui/src/modules/file/utils/genCreateObjectTx.ts @@ -3,7 +3,7 @@ import { TCreateObject } from "@bnb-chain/greenfield-chain-sdk"; export const genCreateObjectTx = async (configParam: TCreateObject) => { const client = await getClient(); - const createBucketTx = await client.object.createObject(configParam); + const createObjectTx = await client.object.createObject(configParam); - return createBucketTx; + return createObjectTx; } \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/modules/object/ObjectError.tsx b/apps/dcellar-web-ui/src/modules/object/ObjectError.tsx index 25cd93e5..0ffda634 100644 --- a/apps/dcellar-web-ui/src/modules/object/ObjectError.tsx +++ b/apps/dcellar-web-ui/src/modules/object/ObjectError.tsx @@ -49,6 +49,11 @@ export const OBJECT_ERROR_TYPES = { title: 'You need Access', icon: FILE_FAILED_URL, desc: "You don't have permission to download. You can ask the person who shared the link to invite you directly.", + }, + SP_STORAGE_PRICE_FAILED: { + title: 'Get storage price failed', + icon: FILE_FAILED_URL, + desc: 'Get storage price failed, please select another SP.', } } diff --git a/apps/dcellar-web-ui/src/modules/object/components/CancelObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/CancelObject.tsx index 426e2beb..ebd3d4e8 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/CancelObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/CancelObject.tsx @@ -65,7 +65,7 @@ export const CancelObject = ({ refetch }: modalProps) => { const dispatch = useAppDispatch(); const [lockFee, setLockFee] = useState(''); const { loginAccount } = useAppSelector((root) => root.persist); - const { gasList } = useAppSelector((root) => root.global.gasHub); + const { gasObjects } = useAppSelector((root) => root.global.gasHub); const { bnb: { price: bnbPrice }, } = useAppSelector((root) => root.global); @@ -86,7 +86,7 @@ export const CancelObject = ({ refetch }: modalProps) => { document.documentElement.style.overflowY = ''; }; - const simulateGasFee = gasList[MsgCancelCreateObjectTypeUrl]?.gasFee + ''; + const simulateGasFee = gasObjects[MsgCancelCreateObjectTypeUrl]?.gasFee + ''; useEffect(() => { if (!isOpen) return; diff --git a/apps/dcellar-web-ui/src/modules/object/components/CreateFolder.tsx b/apps/dcellar-web-ui/src/modules/object/components/CreateFolder.tsx index 94ab787b..29abcdfd 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/CreateFolder.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/CreateFolder.tsx @@ -67,8 +67,8 @@ export const CreateFolder = memo(function CreateFolderDrawer({ refet const { connector } = useAccount(); const checksumWorkerApi = useChecksumApi(); const { bucketName, folders, objects, path, primarySp } = useAppSelector((root) => root.object); - const { gasList = {} } = useAppSelector((root) => root.global.gasHub); - const { gasFee } = gasList?.[MsgCreateObjectTypeUrl] || {}; + const { gasObjects = {} } = useAppSelector((root) => root.global.gasHub); + const { gasFee } = gasObjects?.[MsgCreateObjectTypeUrl] || {}; const { sps } = useAppSelector((root) => root.sp); const { loginAccount: address } = useAppSelector((root) => root.persist); const { _availableBalance: availableBalance } = useAppSelector((root) => root.global); diff --git a/apps/dcellar-web-ui/src/modules/object/components/DeleteObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/DeleteObject.tsx index 737a221b..7d8aaf48 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/DeleteObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/DeleteObject.tsx @@ -89,8 +89,8 @@ export const DeleteObject = ({ refetch }: modalProps) => { // todo fix it document.documentElement.style.overflowY = ''; }; - const { gasList } = useAppSelector((root) => root.global.gasHub); - const simulateGasFee = gasList[MsgDeleteObjectTypeUrl]?.gasFee ?? 0; + const { gasObjects } = useAppSelector((root) => root.global.gasHub); + const simulateGasFee = gasObjects[MsgDeleteObjectTypeUrl]?.gasFee ?? 0; const { connector } = useAccount(); useEffect(() => { diff --git a/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx index 4c1cc3cd..86324e07 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx @@ -1,11 +1,12 @@ import React, { ChangeEvent, memo } from 'react'; import { useAppDispatch, useAppSelector } from '@/store'; import { GAClick } from '@/components/common/GATracker'; -import { Flex, Text, Tooltip } from '@totejs/uikit'; +import { Button, Flex, Menu, MenuButton, MenuItem, MenuList, Text, Tooltip } from '@totejs/uikit'; import UploadIcon from '@/public/images/files/upload_transparency.svg'; import { setEditCreate, setEditUpload } from '@/store/slices/object'; import { addToHashQueue } from '@/store/slices/global'; import { getUtcZeroTimestamp } from '@bnb-chain/greenfield-chain-sdk'; +import { MenuCloseIcon, MenuOpenIcon } from '@totejs/icons'; interface NewObjectProps { gaFolderClickName?: string; @@ -26,14 +27,6 @@ export const NewObject = memo(function NewObject({ if (disabled) return; dispatch(setEditCreate(true)); }; - const handleFileChange = async (e: ChangeEvent) => { - const files = e.target.files || []; - if (!files.length) return; - const id = getUtcZeroTimestamp(); - dispatch(addToHashQueue({ id, file: files[0] })); - dispatch(setEditUpload(id)); - e.target.value = ''; - }; if (!owner) return <>; const folderExist = !prefix ? true : !!objectsInfo[path + '/']; @@ -45,6 +38,22 @@ export const NewObject = memo(function NewObject({ const disabled = maxFolderDepth || discontinue; const uploadDisabled = discontinue || invalidPath || folders.length > MAX_FOLDER_LEVEL; + const handleFilesChange = async (e: ChangeEvent) => { + console.log(e); + console.log('files', e.target.files, typeof e.target.files); + const files = e.target.files; + if (!files || !files.length) return; + const uploadIds: number[] = []; + Object.values(files).forEach((file: File) => { + const time = getUtcZeroTimestamp(); + const id = parseInt(String(time * Math.random())); + uploadIds.push(id); + dispatch(addToHashQueue({ id, file, time })); + }); + dispatch(setEditUpload(1)); + e.target.value = ''; + }; + return ( (function NewObject({ - - - + {!uploadDisabled && ( + + + + + + + )} - - - + + )} + ); }); diff --git a/apps/dcellar-web-ui/src/modules/upload/ListItem.tsx b/apps/dcellar-web-ui/src/modules/upload/ListItem.tsx new file mode 100644 index 00000000..cefba875 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/upload/ListItem.tsx @@ -0,0 +1,66 @@ +import { + Box, + Flex, + QListItem, +} from '@totejs/uikit'; +import React, { useMemo } from 'react'; +import { formatBytes } from '../file/utils'; +import { EllipsisText } from '@/components/common/EllipsisText'; +import { CloseIcon } from '@totejs/icons'; +import { removeFromHashQueue } from '@/store/slices/global'; +import { useAppDispatch, useAppSelector } from '@/store'; + +type ListItemProps = { path: string; type: 'ALL' | 'WAIT' | 'ERROR' }; + +export const ListItem = ({ path, type }: ListItemProps) => { + const dispatch = useAppDispatch(); + const { hashQueue: selectedFiles } = useAppSelector((root) => root.global); + const onRemoveClick = (id: number) => { + dispatch(removeFromHashQueue({ id })); + }; + const list = useMemo(() => { + switch (type) { + case 'ALL': + return selectedFiles; + case 'WAIT': + return selectedFiles.filter((file) => file.status === 'WAIT'); + case 'ERROR': + return selectedFiles.filter((file) => file.status === 'ERROR'); + default: + return selectedFiles; + } + }, [selectedFiles, type]); + + return ( + + {list && + list.map((selectedFile) => ( + onRemoveClick(selectedFile.id)} + marginLeft={'8px'} + cursor={'pointer'} + /> + } + > + + + {selectedFile.name} + {selectedFile.msg ? ( + {selectedFile.msg} + ) : ( + {formatBytes(selectedFile.size)} + )} + + {`${path}/`} + + + ))} + + ); +}; diff --git a/apps/dcellar-web-ui/src/modules/upload/SimulateFee.tsx b/apps/dcellar-web-ui/src/modules/upload/SimulateFee.tsx index faadb536..1fd1029d 100644 --- a/apps/dcellar-web-ui/src/modules/upload/SimulateFee.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/SimulateFee.tsx @@ -7,23 +7,57 @@ import { } from '@/modules/file/utils'; import { useAppDispatch, useAppSelector } from '@/store'; import { MsgCreateObjectTypeUrl } from '@bnb-chain/greenfield-chain-sdk'; -import { Box, Flex, Text } from '@totejs/uikit'; -import React from 'react'; -import { useMount } from 'ahooks'; -import { setupTmpAvailableBalance } from '@/store/slices/global'; +import { Box, Fade, Flex, Slide, Text, useDisclosure } from '@totejs/uikit'; +import React, { forwardRef, useImperativeHandle, useMemo } from 'react'; +import { useAsyncEffect, useMount } from 'ahooks'; +import { setupPreLockFeeObjects, setupTmpAvailableBalance } from '@/store/slices/global'; +import { isEmpty } from 'lodash-es'; +import { calPreLockFee } from '@/utils/sp'; +import { MenuCloseIcon } from '@totejs/icons'; -interface FeeProps { - lockFee: string; -} - -export const Fee = ({ lockFee }: FeeProps) => { +export const Fee = forwardRef((props, ref) => { const dispatch = useAppDispatch(); const { loginAccount } = useAppSelector((root) => root.persist); const { _availableBalance: availableBalance } = useAppSelector((root) => root.global); - const { gasList = {} } = useAppSelector((root) => root.global.gasHub); - const { gasFee } = gasList?.[MsgCreateObjectTypeUrl] || {}; + const { gasObjects = {} } = useAppSelector((root) => root.global.gasHub); + const { gasFee: singleTxGasFee } = gasObjects?.[MsgCreateObjectTypeUrl] || {}; const { price: exchangeRate } = useAppSelector((root) => root.global.bnb); + const { hashQueue, preLockFeeObjects } = useAppSelector((root) => root.global); + const { primarySp } = useAppSelector((root) => root.object); + const isChecking = + hashQueue.some((item) => item.status === 'CHECK') || isEmpty(preLockFeeObjects); + const { isOpen, onToggle } = useDisclosure(); + useAsyncEffect(async () => { + if (isEmpty(preLockFeeObjects[primarySp.operatorAddress])) { + return await dispatch(setupPreLockFeeObjects(primarySp.operatorAddress)); + } + }, [primarySp.operatorAddress]); + + const lockFee = useMemo(() => { + const preLockFeeObject = preLockFeeObjects[primarySp.operatorAddress]; + if (isEmpty(preLockFeeObject) || isChecking) { + return '-1'; + } + const size = hashQueue + .filter((item) => item.status !== 'ERROR') + .reduce((acc, cur) => acc + cur.size, 0); + const lockFee = calPreLockFee({ + size, + primarySpAddress: primarySp.operatorAddress, + preLockFeeObject: preLockFeeObject, + }); + return lockFee; + }, [hashQueue, isChecking, preLockFeeObjects, primarySp?.operatorAddress]); + + const gasFee = isChecking + ? -1 + : hashQueue.filter((item) => item.status !== 'ERROR').length * singleTxGasFee; + useImperativeHandle(ref, () => ({ + isBalanceAvailable: Number(availableBalance) >= Number(gasFee) + Number(lockFee), + amount: String(Number(gasFee) + Number(lockFee)), + balance: availableBalance, + })) useMount(() => { dispatch(setupTmpAvailableBalance(loginAccount)); }); @@ -39,7 +73,7 @@ export const Fee = ({ lockFee }: FeeProps) => { { )} - + {key === 'Pre-locked storage fee' ? renderPrelockedFeeValue(bnbValue, exchangeRate) : renderFeeValue(bnbValue, exchangeRate)} @@ -62,15 +96,27 @@ export const Fee = ({ lockFee }: FeeProps) => { }; return ( - <> + + Total Fees + + {renderFeeValue(String(Number(gasFee) + Number(lockFee)), exchangeRate)} + + + + + + {renderFee( 'Pre-locked storage fee', lockFee, @@ -98,20 +144,22 @@ export const Fee = ({ lockFee }: FeeProps) => { )} {renderFee('Gas fee', gasFee + '', +exchangeRate)} - + {/*todo correct the error showing logics*/} - {renderInsufficientBalance(gasFee + '', lockFee, availableBalance || '0', { - gaShowName: 'dc.file.upload_modal.transferin.show', - gaClickName: 'dc.file.upload_modal.transferin.click', - })} + {!isChecking && + renderInsufficientBalance(gasFee + '', lockFee, availableBalance || '0', { + gaShowName: 'dc.file.upload_modal.transferin.show', + gaClickName: 'dc.file.upload_modal.transferin.click', + })} Available balance: {renderBalanceNumber(availableBalance || '0')} - - + + + ); -}; +}); export default Fee; diff --git a/apps/dcellar-web-ui/src/modules/upload/TaskManagement.tsx b/apps/dcellar-web-ui/src/modules/upload/TaskManagement.tsx index a6c84e4b..b3c7d7e0 100644 --- a/apps/dcellar-web-ui/src/modules/upload/TaskManagement.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/TaskManagement.tsx @@ -1,7 +1,6 @@ -import { Box, Flex, QDrawer, Text } from '@totejs/uikit'; -import React, { useState } from 'react'; +import { Box, Text } from '@totejs/uikit'; +import React from 'react'; import { UploadingObjects } from './UploadingObjects'; -import { LoadingIcon } from '@/components/common/SvgIcon/LoadingIcon'; import { useAppDispatch, useAppSelector } from '@/store'; import { selectUploadQueue, setTaskManagement } from '@/store/slices/global'; import { DCButton } from '@/components/common/DCButton'; @@ -12,12 +11,15 @@ export const TaskManagement = () => { const dispatch = useAppDispatch(); const { taskManagement } = useAppSelector((root) => root.global); const { loginAccount } = useAppSelector((root) => root.persist); - const queue = useAppSelector(selectUploadQueue(loginAccount)); + const uploadQueue = useAppSelector(selectUploadQueue(loginAccount)); const isOpen = taskManagement; - const setOpen = (boo: boolean) => { - dispatch(setTaskManagement(boo)); + const onToggle = () => { + dispatch(setTaskManagement(!isOpen)); }; - const isUploading = queue.some((i) => i.status === 'UPLOAD'); + const setClose = () => { + dispatch(setTaskManagement(false)); + } + const isUploading = uploadQueue.some((i) => i.status === 'UPLOAD'); const renderButton = () => { if (isUploading) { @@ -25,7 +27,7 @@ export const TaskManagement = () => { setOpen(true)} + onClick={() => onToggle()} alignItems={'center'} justifyContent={'center'} > @@ -41,9 +43,7 @@ export const TaskManagement = () => { cursor={'pointer'} alignSelf={'center'} marginRight={'12px'} - onClick={() => { - setOpen(true); - }} + onClick={() => onToggle()} > Task Management @@ -55,7 +55,7 @@ export const TaskManagement = () => { return ( <> {renderButton()} - setOpen(false)}> + setClose()}> diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx index d8a0fbbe..289122e9 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { Box, Flex, @@ -6,13 +6,11 @@ import { QDrawerCloseButton, QDrawerFooter, QDrawerHeader, - QListItem, Tab, TabList, TabPanel, TabPanels, Tabs, - toast, } from '@totejs/uikit'; import { BUTTON_GOT_IT, @@ -20,6 +18,7 @@ import { FILE_STATUS_UPLOADING, FILE_TITLE_UPLOAD_FAILED, FILE_UPLOAD_URL, + OBJECT_AUTH_TEMP_ACCOUNT_CREATING, OBJECT_TITLE_CREATING, } from '@/modules/file/constant'; import Fee from './SimulateFee'; @@ -28,9 +27,6 @@ import { DotLoading } from '@/components/common/DotLoading'; import { WarningInfo } from '@/components/common/WarningInfo'; import AccessItem from './AccessItem'; import { - broadcastFault, - createTxFault, - E_ACCOUNT_BALANCE_NOT_ENOUGH, E_FILE_IS_EMPTY, E_FILE_TOO_LARGE, E_OBJECT_NAME_CONTAINS_SLASH, @@ -38,60 +34,59 @@ import { E_OBJECT_NAME_EXISTS, E_OBJECT_NAME_NOT_UTF8, E_OBJECT_NAME_TOO_LONG, - E_OFF_CHAIN_AUTH, E_UNKNOWN, - simulateFault, } from '@/facade/error'; import { isUTF8 } from '../file/utils/file'; - -import { genCreateObjectTx } from '../file/utils/genCreateObjectTx'; -import { signTypedDataCallback } from '@/facade/wallet'; -import { useAccount } from 'wagmi'; -import { resolve } from '@/facade/common'; -import { getDomain } from '@/utils/getDomain'; import { useAppDispatch, useAppSelector } from '@/store'; import { formatBytes } from '../file/utils'; import { DCDrawer } from '@/components/common/DCDrawer'; import { TStatusDetail, setEditUpload, setStatusDetail } from '@/store/slices/object'; -import { getSpOffChainData } from '@/store/slices/persist'; -import { TCreateObject } from '@bnb-chain/greenfield-chain-sdk'; import { - addTaskToUploadQueue, - selectHashFile, + addTasksToUploadQueue, + resetHashQueue, setTaskManagement, - updateHashQueue, + setTmpAccount, updateHashStatus, updateHashTaskMsg, } from '@/store/slices/global'; -import { useAsyncEffect } from 'ahooks'; +import { useAsyncEffect, useUpdateEffect } from 'ahooks'; import { VisibilityType } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/common'; -import { reverseVisibilityType } from '@/utils/constant'; import { OBJECT_ERROR_TYPES, ObjectErrorType } from '../object/ObjectError'; import { duplicateName } from '@/utils/object'; -import { useOffChainAuth } from '@/hooks/useOffChainAuth'; -import { EllipsisText } from '@/components/common/EllipsisText'; +import { isEmpty, round } from 'lodash-es'; +import { ListItem } from './ListItem'; +import { useTab } from './useUploadTab'; +import { createTmpAccount } from '@/facade/account'; +import { parseEther } from 'ethers/lib/utils.js'; const MAX_SIZE = 256; +type TSimulateFee = { + isBalanceAvailable: boolean; + amount: string; + balance: string; +}; + export const UploadObjects = () => { const dispatch = useAppDispatch(); - const { setOpenAuthModal } = useOffChainAuth(); + const feeRef = useRef(); const { editUpload, path, objects } = useAppSelector((root) => root.object); - const { connector } = useAccount(); - const { bucketName, primarySp, folders } = useAppSelector((root) => root.object); + const { bucketName, primarySp } = useAppSelector((root) => root.object); const { loginAccount } = useAppSelector((root) => root.persist); - const { sps: globalSps } = useAppSelector((root) => root.sp); - const selectedFile = useAppSelector(selectHashFile(editUpload)); - const { hashQueue } = useAppSelector((root) => root.global); + const { hashQueue, preLockFeeObjects } = useAppSelector((root) => root.global); const [visibility, setVisibility] = useState( VisibilityType.VISIBILITY_TYPE_PRIVATE, ); + const selectedFiles = hashQueue; const objectList = objects[path]?.filter((item) => !item.objectName.endsWith('/')); const [creating, setCreating] = useState(false); + const { tabOptions, activeKey, setActiveKey } = useTab(); const onClose = () => { dispatch(setEditUpload(0)); + dispatch(resetHashQueue()); }; + const getErrorMsg = (type: string) => { return OBJECT_ERROR_TYPES[type as ObjectErrorType] ? OBJECT_ERROR_TYPES[type as ObjectErrorType] @@ -128,15 +123,11 @@ export const UploadObjects = () => { const errorHandler = (error: string) => { setCreating(false); - if (error === E_OFF_CHAIN_AUTH) { - setOpenAuthModal(); - return; - } dispatch( setStatusDetail({ title: FILE_TITLE_UPLOAD_FAILED, icon: FILE_FAILED_URL, - desc: 'Sorry, there’s something wrong when uploading the file.', + desc: 'Sorry, there’s something wrong when signing with the wallet.', buttonText: BUTTON_GOT_IT, buttonOnClick: () => dispatch(setStatusDetail({} as TStatusDetail)), errorText: 'Error message: ' + error, @@ -145,176 +136,120 @@ export const UploadObjects = () => { }; const onUploadClick = async () => { - if (!selectedFile) return; setCreating(true); - const domain = getDomain(); - const secondarySpAddresses = globalSps - .filter((item: any) => item.operator !== primarySp.operatorAddress) - .map((item: any) => item.operatorAddress); - const spInfo = { - endpoint: primarySp.endpoint, - primarySp: primarySp.operatorAddress, - sealAddress: primarySp.sealAddress, - secondarySpAddresses, - }; - - const { seedString } = await dispatch( - getSpOffChainData(loginAccount, primarySp.operatorAddress), - ); - const finalName = [...folders, selectedFile.name].join('/'); - const createObjectPayload: TCreateObject = { - bucketName, - objectName: finalName, - creator: loginAccount, - visibility: reverseVisibilityType[visibility], - fileType: selectedFile.type || 'application/octet-stream', - contentLength: selectedFile.size, - expectCheckSums: selectedFile.checksum, - spInfo, - signType: 'offChainAuth', - domain, - seedString, - }; - const [createObjectTx, _createError] = await genCreateObjectTx(createObjectPayload).then( - resolve, - createTxFault, - ); - - if (_createError) { - return errorHandler(_createError); - } - dispatch( setStatusDetail({ icon: FILE_UPLOAD_URL, - title: OBJECT_TITLE_CREATING, + title: OBJECT_AUTH_TEMP_ACCOUNT_CREATING, desc: FILE_STATUS_UPLOADING, }), ); - - const [simulateInfo, simulateError] = await createObjectTx! - .simulate({ - denom: 'BNB', - }) - .then(resolve, simulateFault); - - if (simulateError) { - if ( - simulateError?.includes('lack of') || - simulateError?.includes('static balance is not enough') - ) { - dispatch(setStatusDetail(getErrorMsg(E_ACCOUNT_BALANCE_NOT_ENOUGH))); - } else if (simulateError?.includes('Object already exists')) { - dispatch(setStatusDetail(getErrorMsg(E_OBJECT_NAME_EXISTS))); - } else { - dispatch(setStatusDetail(getErrorMsg(E_UNKNOWN))); - } + const { amount, balance } = feeRef.current || {}; + if (!amount || !balance) { + console.error('get total fee error', feeRef.current); return; } - const broadcastPayload = { - denom: 'BNB', - gasLimit: Number(simulateInfo?.gasLimit), - gasPrice: simulateInfo?.gasPrice || '5000000000', - payer: loginAccount, - signTypedDataCallback: signTypedDataCallback(connector!), - granter: '', - }; - const [res, error] = await createObjectTx! - .broadcast(broadcastPayload) - .then(resolve, broadcastFault); - - const _ = res?.rawLog; - if (error) { - return errorHandler(error || _!); + const safeAmount = Number(amount) * 1.05 > Number(balance) ? round(Number(balance), 6) : round(Number(amount) * 1.05, 6); + const [tmpAccount, error] = await createTmpAccount({ + address: loginAccount, + bucketName, + amount: parseEther(String(safeAmount)).toString(), + }); + if (!tmpAccount) { + return errorHandler(error); } - toast.success({ description: 'Object created successfully!' }); - dispatch( - addTaskToUploadQueue(selectedFile.id, res!.transactionHash, primarySp.operatorAddress), - ); - dispatch(setEditUpload(0)); + + dispatch(setTmpAccount(tmpAccount)); + dispatch(addTasksToUploadQueue(primarySp.operatorAddress, visibility)); dispatch(setStatusDetail({} as TStatusDetail)); dispatch(setTaskManagement(true)); + onClose(); setCreating(false); }; useAsyncEffect(async () => { - if (!editUpload) { - setCreating(false); - dispatch(updateHashQueue()); - return; - } - if (!selectedFile) return; - const { file, id } = selectedFile; - const error = basicValidate(file); - if (!error) { - dispatch(updateHashStatus({ id, status: 'WAIT' })); - return; - } - dispatch(updateHashTaskMsg({ id, msg: getErrorMsg(error).title })); + if (isEmpty(selectedFiles)) return; + selectedFiles.forEach((item) => { + const { file, id } = item; + const error = basicValidate(file); + if (!error) { + dispatch(updateHashStatus({ id, status: 'WAIT' })); + return; + } + dispatch(updateHashTaskMsg({ id, msg: getErrorMsg(error).title })); + }); }, [editUpload]); - const loading = selectedFile?.status !== 'READY'; - const hasError = hashQueue.some((item) => item.msg !== ''); + useUpdateEffect(() => { + if (selectedFiles.length === 0) { + dispatch(setEditUpload(0)); + + } + }, [selectedFiles.length]); + + const loading = useMemo(() => { + return selectedFiles.some((item) => item.status === 'CHECK') || isEmpty(preLockFeeObjects); + }, [preLockFeeObjects, selectedFiles]); + + const hasSuccess = hashQueue.some((item) => item.status === 'WAIT'); return ( Upload Objects - {!!selectedFile && ( + {!isEmpty(selectedFiles) && ( - + setActiveKey(key)}> - - All Objects - + {tabOptions.map((item) => ( + + {item.icon} + {item.title}({item.len}) + + ))} - - - - - Total Upload: {formatBytes(selectedFile.size)} /{' '} - 1 Files - - - + {tabOptions.map((item) => ( + + + + ))} - - - - - {selectedFile.name} - {selectedFile.msg ? ( - {selectedFile.msg} - ) : ( - {formatBytes(selectedFile.size)} - )} - - {`${path}/`} - - - )} - - + + + + + Total Upload:{' '} + + {formatBytes( + selectedFiles.reduce( + (accumulator, currentValue) => accumulator + currentValue.size, + 0, + ), + )} + {' '} + / {selectedFiles.length} Files + + + - {(loading || creating) && !hasError ? ( + {(loading || creating) && !hasSuccess ? ( <> Loading diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadingObjects.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadingObjects.tsx index e9d6bc21..9624407c 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadingObjects.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadingObjects.tsx @@ -1,58 +1,66 @@ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; import { Box, + Empty, + EmptyDescription, + EmptyIcon, + EmptyTitle, Flex, Image, QDrawerBody, QDrawerCloseButton, QDrawerHeader, QListItem, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, Text, + CircularProgress, } from '@totejs/uikit'; import { FILE_UPLOAD_STATIC_URL } from '@/modules/file/constant'; import { useAppSelector } from '@/store'; import { formatBytes } from '../file/utils'; -import { sortBy } from 'lodash-es'; -import CircleProgress from '../file/components/CircleProgress'; -import { ColoredSuccessIcon } from '@totejs/icons'; +import { ColoredErrorIcon, ColoredSuccessIcon } from '@totejs/icons'; import { Loading } from '@/components/common/Loading'; import { UploadFile } from '@/store/slices/global'; import { EllipsisText } from '@/components/common/EllipsisText'; +import { useTaskManagementTab } from './useTaskManagementTab'; +import styled from '@emotion/styled'; export const UploadingObjects = () => { - const { objectsInfo } = useAppSelector((root) => root.object); - const { uploadQueue } = useAppSelector((root) => root.global); - const { loginAccount } = useAppSelector((root) => root.persist); - - const queue = sortBy(uploadQueue[loginAccount] || [], [ - (o) => { - switch (o.status) { - case 'SEAL': - return 0; - case 'UPLOAD': - return 1; - case 'WAIT': - return 2; - case 'FINISH': - return 3; - } - }, - ]); + const { bucketName } = useAppSelector((root) => root.object); + const { queue, tabOptions, activeKey, setActiveKey } = useTaskManagementTab(); const FileStatus = useCallback(({ task }: { task: UploadFile }) => { switch (task.status) { case 'WAIT': - return <>waiting; + return ( + <> + + waiting + + ); + case 'HASH': + return ( + <> + + hashing + + ); + case 'READY': + return ( + <> + + ready + + ); case 'UPLOAD': return ( - - - + <> + + Uploading + ); case 'SEAL': return ( @@ -63,6 +71,8 @@ export const UploadingObjects = () => { ); case 'FINISH': return ; + case 'ERROR': + return ; default: return null; } @@ -93,16 +103,85 @@ export const UploadingObjects = () => { Task Management - - Current Upload - - {queue.map((task) => { - const prefix = `${[task.bucketName, ...task.folders].join('/')}/`; + setActiveKey(key)}> + + {tabOptions.map((item) => ( + + {item.icon} + {item.title}({item.data.length}) + + ))} + + + {tabOptions.map((item) => ( + + {item.data.length === 0 && ( + + + {/* Title */} + There are no objects in the list + + )} + {item.data && + item.data.map((task) => ( + + + + + {task.file.name} + + {task.msg ? ( + + {task.msg} + + ) : ( + {formatBytes(task.file.size)} + )} + + + {[bucketName, task.prefixFolders].join('/')} + + + + + + + ))} + + ))} + + + {/* {queue.map((task) => { + const prefix = `${[task.bucketName, ...task.prefixFolders].join('/')}/`; return ( { {prefix} - {/* create hash: {task.createHash} - - seal hash:{' '} - { - objectsInfo[[task.bucketName, ...task.folders, task.file.name].join('/')] - ?.seal_tx_hash - } - */} ); - })} + })} */} ); }; + +const StyledTabList = styled(TabList)` + &::-webkit-scrollbar { + display: none; + } + scrollbar-width: none; /* firefox */ + -ms-overflow-style: none; /* IE 10+ */ + overflow-x: scroll; +`; diff --git a/apps/dcellar-web-ui/src/modules/upload/useTaskManagementTab.tsx b/apps/dcellar-web-ui/src/modules/upload/useTaskManagementTab.tsx new file mode 100644 index 00000000..fd34c297 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/upload/useTaskManagementTab.tsx @@ -0,0 +1,80 @@ +import { useAppSelector } from '@/store'; +import { TUploadStatus, UploadFile } from '@/store/slices/global'; +import { ColoredAlertIcon } from '@totejs/icons'; + +import { sortBy } from 'lodash-es'; +import { useMemo, useState } from 'react'; + +export type TTabKey = TUploadStatus; + +export const useTaskManagementTab = () => { + const { uploadQueue } = useAppSelector((root) => root.global); + const { loginAccount } = useAppSelector((root) => root.persist); + const queue = sortBy(uploadQueue[loginAccount] || [], [ + (o) => { + switch (o.status) { + case 'SEAL': + return 0; + case 'UPLOAD': + return 1; + case 'HASH': + return 1; + case 'READY': + return 1; + case 'WAIT': + return 2; + case 'FINISH': + return 3; + case 'ERROR': + return 4; + } + }, + ]); + const { uploadingQueue, completeQueue, errorQueue } = useMemo(() => { + const uploadingQueue = queue?.filter((i) => i.status === 'UPLOAD' || i.status === 'FINISH'); + const completeQueue = queue?.filter((i) => i.status === 'SEAL'); + const errorQueue = queue?.filter((i) => i.status === 'ERROR'); + return { + uploadingQueue, + completeQueue, + errorQueue, + }; + }, [queue]); + + const tabOptions: { + title: string; + key: TUploadStatus | 'ALL'; + icon?: React.ReactNode; + data: UploadFile[]; + }[] = [ + { + title: 'All Objects', + key: 'ALL', + data: queue, + }, + { + title: 'Uploading', + key: 'UPLOAD', + data: uploadingQueue, + }, + { + title: 'Complete', + key: 'SEAL', + data: completeQueue, + }, + { + title: 'Failed', + key: 'ERROR', + icon: , + data: errorQueue, + }, + ]; + const [activeKey, setActiveKey] = useState(tabOptions[0].key); + + return { + queue, + tabOptions, + activeKey, + setActiveKey, + }; +}; diff --git a/apps/dcellar-web-ui/src/modules/upload/useUploadTab.tsx b/apps/dcellar-web-ui/src/modules/upload/useUploadTab.tsx new file mode 100644 index 00000000..3df08460 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/upload/useUploadTab.tsx @@ -0,0 +1,49 @@ +import { useAppSelector } from '@/store'; +import { ColoredAlertIcon, ColoredErrorIcon } from '@totejs/icons'; +import { useMemo, useState } from 'react'; + +export type TTabKey = 'ALL' | 'WAIT' | 'ERROR'; + +export const useTab = () => { + const { hashQueue, preLockFeeObjects } = useAppSelector((root) => root.global); + const { allLen, waitLen, errorLen } = useMemo(() => { + const allLen = hashQueue.length; + const waitLen = hashQueue.filter((item) => item.status === 'WAIT').length; + const errorLen = hashQueue.filter((item) => item.status === 'ERROR').length; + return { + allLen, + waitLen, + errorLen + } + }, [hashQueue]) + const tabOptions: { + title: string; + key: TTabKey; + len: number; + icon?: React.ReactNode; + }[] = [ + { + title: 'All Objects', + key: 'ALL', + len: allLen, + }, + { + title: 'Awaiting Upload', + key: 'WAIT', + len: waitLen, + }, + { + title: 'Error', + key: 'ERROR', + len: errorLen, + icon: + }, + ]; + const [activeKey, setActiveKey] = useState(tabOptions[0].key); + + return { + tabOptions, + activeKey, + setActiveKey, + } +}; diff --git a/apps/dcellar-web-ui/src/store/slices/global.ts b/apps/dcellar-web-ui/src/store/slices/global.ts index 730d0a39..76970187 100644 --- a/apps/dcellar-web-ui/src/store/slices/global.ts +++ b/apps/dcellar-web-ui/src/store/slices/global.ts @@ -7,6 +7,8 @@ import { find, keyBy } from 'lodash-es'; import { setupListObjects, updateObjectStatus } from '@/store/slices/object'; import { getSpOffChainData } from '@/store/slices/persist'; import { defaultBalance } from '@/store/slices/balance'; +import Long from 'long'; +import { VisibilityType } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/common'; type TGasList = { [msgTypeUrl: string]: { @@ -18,30 +20,52 @@ type TGasList = { type TGas = { gasPrice: number; - gasList: TGasList; + gasObjects: TGasList; }; -export type TFileStatus = 'CHECK' | 'WAIT' | 'HASH' | 'READY' | 'SEAL' | 'FINISH' | 'UPLOAD'; +export type TPreLockFeeParams = { + spStorageStorePrice: string; + secondarySpStorePrice: string; + minChargeSize: number; + redundantDataChunkNum: number; + redundantParityChunkNum: number; + reserveTime: string; +} + +type TPreLockFeeObjects = { + [key: string]: TPreLockFeeParams +}; + +export type TFileStatus = 'CHECK' | 'WAIT' | 'ERROR'; + +export type TUploadStatus = 'WAIT' | 'HASH' | 'READY' | 'UPLOAD' | 'FINISH' | 'SEAL' | 'ERROR'; + +export type TTmpAccount = { + address: string; + privateKey: string; +}; export type HashFile = { file: File; - status: 'CHECK' | 'WAIT' | 'HASH' | 'READY'; + status: TFileStatus; id: number; + time: number; msg: string; type: string; size: number; name: string; - checksum: string[]; lockFee: string; }; export type UploadFile = { bucketName: string; - folders: string[]; + prefixFolders: string[]; id: number; sp: string; file: HashFile; - status: TFileStatus; + checksum: string[]; + status: TUploadStatus; + visibility: VisibilityType; createHash: string; msg: string; progress: number; @@ -50,24 +74,28 @@ export type UploadFile = { export interface GlobalState { bnb: BnbPriceInfo; gasHub: TGas; + preLockFeeObjects: TPreLockFeeObjects; hashQueue: HashFile[]; // max length two, share cross different accounts. uploadQueue: Record; _availableBalance: string; // using static value, avoid rerender _lockFee: string; taskManagement: boolean; + tmpAccount: TTmpAccount; } const initialState: GlobalState = { bnb: getDefaultBnbInfo(), gasHub: { gasPrice: 5e-9, - gasList: {}, + gasObjects: {}, }, + preLockFeeObjects: {}, hashQueue: [], uploadQueue: {}, _availableBalance: '0', _lockFee: '0', taskManagement: false, + tmpAccount: {} as TTmpAccount, }; export const globalSlice = createSlice({ @@ -114,27 +142,34 @@ export const globalSlice = createSlice({ const queue = state.uploadQueue[account] || []; state.uploadQueue[account] = queue.map((q) => (ids.includes(q.id) ? { ...q, status } : q)); }, - updateHashQueue(state) { - state.hashQueue = state.hashQueue.filter((task) => task.status === 'HASH'); - }, - updateHashChecksum( + updateUploadChecksum( state, - { payload }: PayloadAction<{ id: number; checksum: string[]; lockFee: string }>, + { payload }: PayloadAction<{ account: string; id: number; checksum: string[]; }>, ) { - const { id, checksum, lockFee } = payload; - const queue = state.hashQueue; - const task = find(queue, (t) => t.id === id); + const { account, id, checksum } = payload; + const queues = state.uploadQueue; + const queue = queues[account] + const task = find(queue, (t) => t.id === id); if (!task) return; task.status = 'READY'; task.checksum = checksum; - task.lockFee = lockFee; if (queue.length === 1) return; - queue.shift(); // shift first ready item + // 为什么要移除啊?先不要移除,一定要以数据流动和管理进行思考 + // queue.shift(); // shift first ready item }, - updateHashTaskMsg(state, { payload }: PayloadAction<{ id: number; msg: string }>) { + updateHashTaskMsg(state, { payload }: PayloadAction<{ id: number; msg: string, }>) { const { id, msg } = payload; const task = find(state.hashQueue, (t) => t.id === id); if (!task) return; + task.status = 'ERROR'; + task.msg = msg; + + }, + updateUploadTaskMsg(state, { payload }: PayloadAction<{ account: string, id: number, msg: string }>) { + const { id, msg } = payload; + const task = find(state.uploadQueue[payload.account], (t) => t.id === id); + if (!task) return; + task.status = 'ERROR'; task.msg = msg; }, updateHashStatus( @@ -146,35 +181,39 @@ export const globalSlice = createSlice({ if (!task) return; task.status = status; }, - addToHashQueue(state, { payload }: PayloadAction<{ id: number; file: File }>) { - const { id, file } = payload; + addToHashQueue(state, { payload }: PayloadAction<{ id: number; file: File; time: number; }>) { + const { id, file, time } = payload; const task: HashFile = { file, status: 'CHECK', id, + time, msg: '', type: file.type, size: file.size, name: file.name, - checksum: Array(), lockFee: '', }; - state.hashQueue = state.hashQueue.filter((task) => task.status === 'HASH'); - const queue = state.hashQueue; - // max length 2 - queue.length >= 2 ? (queue[2] = task) : queue.push(task); + state.hashQueue.push(task); }, - addToUploadQueue(state, { payload }: PayloadAction) { - const { account, ...task } = payload; - const tasks = state.uploadQueue[account] || []; - state.uploadQueue[account] = [...tasks, task]; + resetHashQueue(state) { + state.hashQueue = []; + }, + removeFromHashQueue(state, { payload }: PayloadAction<{ id: number }>) { + const { id } = payload; + state.hashQueue = state.hashQueue.filter((task) => task.id !== id); + }, + addToUploadQueue(state, { payload }: PayloadAction<{ account: string, tasks: UploadFile[] }>) { + const { account, tasks } = payload; + const existTasks = state.uploadQueue[account] || []; + state.uploadQueue[account] = [...existTasks, ...tasks]; }, setBnbInfo(state, { payload }: PayloadAction) { state.bnb = payload; }, - setGasList(state, { payload }: PayloadAction) { + setGasObjects(state, { payload }: PayloadAction) { const { gasPrice } = state.gasHub; - const gasList = keyBy( + const gasObjects = keyBy( payload.msgGasParams.map((item) => { const gasLimit = item.fixedType?.fixedGas.low || 0; const gasFee = gasPrice * gasLimit; @@ -187,11 +226,18 @@ export const globalSlice = createSlice({ 'msgTypeUrl', ); - state.gasHub.gasList = gasList; + state.gasHub.gasObjects = gasObjects; }, setTaskManagement(state, { payload }: PayloadAction) { state.taskManagement = payload; }, + setPreLockFeeObjects(state, { payload }: PayloadAction<{ primarySpAddress: string, lockFeeParams: TPreLockFeeParams }>) { + const { primarySpAddress, lockFeeParams } = payload; + state.preLockFeeObjects[primarySpAddress] = lockFeeParams; + }, + setTmpAccount(state, { payload }: PayloadAction) { + state.tmpAccount = payload; + } }, }); @@ -200,8 +246,8 @@ export const { updateHashStatus, addToHashQueue, updateHashTaskMsg, - updateHashChecksum, - updateHashQueue, + updateUploadTaskMsg, + updateUploadChecksum, addToUploadQueue, updateUploadStatus, updateUploadProgress, @@ -209,19 +255,26 @@ export const { setTmpAvailableBalance, setTmpLockFee, setTaskManagement, + removeFromHashQueue, + setTmpAccount, + resetHashQueue, } = globalSlice.actions; const _emptyUploadQueue = Array(); + export const selectUploadQueue = (address: string) => (root: AppState) => { return root.global.uploadQueue[address] || _emptyUploadQueue; }; -export const selectBnbPrice = (state: AppState) => state.global.bnb.price; +export const selectHashTask = (address: string) => (root: AppState) => { + const uploadQueue = root.global.uploadQueue[address] || _emptyUploadQueue; + const hashQueue = uploadQueue.filter((task) => task.status === 'HASH'); + const waitQueue = uploadQueue.filter((task) => task.status === 'WAIT'); -export const selectHashTask = (state: AppState) => { - const queue = state.global.hashQueue; - return !queue.length ? null : queue[0].status === 'WAIT' ? queue[0] : null; -}; + const res = !!hashQueue.length ? null : waitQueue[0] ? waitQueue[0] : null; + return res; +} +export const selectBnbPrice = (state: AppState) => state.global.bnb.price; export const selectHashFile = (id: number) => (state: AppState) => { return find(state.global.hashQueue, (f) => f.id === id); @@ -232,12 +285,38 @@ export const setupBnbPrice = () => async (dispatch: AppDispatch) => { dispatch(setBnbInfo(res)); }; -export const setupGasList = () => async (dispatch: AppDispatch) => { +export const setupGasObjects = () => async (dispatch: AppDispatch) => { const client = await getClient(); const res = await client.gashub.getMsgGasParams({ msgTypeUrls: [] }); - dispatch(globalSlice.actions.setGasList(res)); + dispatch(globalSlice.actions.setGasObjects(res)); }; +export const setupPreLockFeeObjects = (primarySpAddress: string) => async (dispatch: AppDispatch) => { + const client = await getClient(); + const spStoragePrice = await client.sp.getStoragePriceByTime(primarySpAddress); + const secondarySpStoragePrice = await client.sp.getSecondarySpStorePrice(); + const { params: storageParams } = await client.storage.params(); + const { + minChargeSize = new Long(0), + redundantDataChunkNum = 0, + redundantParityChunkNum = 0, + } = (storageParams && storageParams.versionedParams) || {}; + const { params: paymentParams } = await client.payment.params(); + const { reserveTime } = paymentParams || {}; + + const lockFeeParamsPayload = { + spStorageStorePrice: spStoragePrice?.storePrice || '', + secondarySpStorePrice: secondarySpStoragePrice?.storePrice || '', + minChargeSize: minChargeSize.toNumber(), + redundantDataChunkNum, + redundantParityChunkNum, + reserveTime: reserveTime?.toString() || '', + }; + dispatch(globalSlice.actions.setPreLockFeeObjects({ + primarySpAddress, lockFeeParams: lockFeeParamsPayload + })) +} + export const refreshTaskFolder = (task: UploadFile) => async (dispatch: AppDispatch, getState: GetState) => { const { spInfo } = getState().sp; @@ -253,7 +332,7 @@ export const refreshTaskFolder = endpoint: primarySp.endpoint, bucketName: task.bucketName, }; - await dispatch(setupListObjects(params, [task.bucketName, ...task.folders].join('/'))); + await dispatch(setupListObjects(params, [task.bucketName, ...task.prefixFolders].join('/'))); }; export const uploadQueueAndRefresh = @@ -264,7 +343,7 @@ export const uploadQueueAndRefresh = dispatch( updateObjectStatus({ bucketName: task.bucketName, - folders: task.folders, + folders: task.prefixFolders, name: task.file.name, objectStatus: 1, }), @@ -280,28 +359,30 @@ export const progressFetchList = fetchedList[task.id] = true; await dispatch(refreshTaskFolder(task)); }; - -export const addTaskToUploadQueue = - (id: number, hash: string, sp: string) => async (dispatch: AppDispatch, getState: GetState) => { +export const addTasksToUploadQueue = + (sp: string, visibility: VisibilityType) => async (dispatch: AppDispatch, getState: GetState) => { const { hashQueue } = getState().global; const { bucketName, folders } = getState().object; const { loginAccount } = getState().persist; - const task = find(hashQueue, (t) => t.id === id); - if (!task) return; - const _task: UploadFile & { account: string } = { - bucketName, - folders, - sp, - account: loginAccount, - id, - file: task, - createHash: hash, - msg: '', - status: 'WAIT', - progress: 0, - }; - dispatch(addToUploadQueue(_task)); - // dispatch(refreshTaskFolder(_task)); + const waitQueue = hashQueue.filter((t) => t.status === 'WAIT'); + if (!waitQueue || waitQueue.length === 0) return; + const newUploadQueue = waitQueue.map((task) => { + const uploadTask: UploadFile = { + bucketName, + prefixFolders: folders, + sp, + id: task.id, + file: task, + msg: '', + status: 'WAIT', + progress: 0, + checksum: [], + visibility, + createHash: '', + } + return uploadTask; + }); + dispatch(addToUploadQueue({ account: loginAccount, tasks: newUploadQueue })); }; export const setupTmpAvailableBalance = diff --git a/apps/dcellar-web-ui/src/utils/sp/index.ts b/apps/dcellar-web-ui/src/utils/sp/index.ts index 573ce778..540322c9 100644 --- a/apps/dcellar-web-ui/src/utils/sp/index.ts +++ b/apps/dcellar-web-ui/src/utils/sp/index.ts @@ -1,6 +1,8 @@ import { getClient } from '@/base/client'; import { GREENFIELD_CHAIN_ID } from '@/base/env'; +import { TPreLockFeeParams } from '@/store/slices/global'; import { IReturnOffChainAuthKeyPairAndUpload, getUtcZeroTimestamp } from '@bnb-chain/greenfield-chain-sdk'; +import { BigNumber } from 'bignumber.js'; const getStorageProviders = async () => { const client = await getClient(); @@ -44,4 +46,30 @@ const filterAuthSps = ({ address, sps }: { address: string; sps: any[]; }) => { return filterSps; } +export const calPreLockFee = ({ size, preLockFeeObject }: { size: number; primarySpAddress: string; preLockFeeObject: TPreLockFeeParams }) => { + const { + spStorageStorePrice, + secondarySpStorePrice, + redundantDataChunkNum, + redundantParityChunkNum, + minChargeSize, + reserveTime + } = preLockFeeObject; + + const chargeSize = size >= minChargeSize ? size : minChargeSize; + const lockedFeeRate = BigNumber(spStorageStorePrice) + .plus( + BigNumber(secondarySpStorePrice).times( + redundantDataChunkNum + redundantParityChunkNum, + ), + ) + .times(BigNumber(chargeSize)).dividedBy(Math.pow(10, 18)); + const lockFeeInBNB = lockedFeeRate + .times(BigNumber(reserveTime || 0)) + .dividedBy(Math.pow(10, 18)); + + return lockFeeInBNB.toString() + +} + export { getStorageProviders, getBucketInfo, getObjectInfo, getSpInfo, filterAuthSps }; diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 0549fc4d..392aaccc 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -10,8 +10,8 @@ importers: '@babel/core': ^7.20.12 '@babel/plugin-syntax-flow': ^7.14.5 '@babel/plugin-transform-react-jsx': ^7.14.9 - '@bnb-chain/greenfield-chain-sdk': 0.0.0-snapshot-20230724100555 - '@bnb-chain/greenfield-cosmos-types': 0.4.0-alpha.15 + '@bnb-chain/greenfield-chain-sdk': 0.0.0-snapshot-20230725025153 + '@bnb-chain/greenfield-cosmos-types': 0.4.0-alpha.13 '@builder.io/partytown': ^0.7.6 '@commitlint/cli': ^17.4.3 '@commitlint/config-conventional': ^17.4.3 @@ -27,7 +27,7 @@ importers: '@totejs/eslint-config': ^1.5.2 '@totejs/icons': ^2.10.0 '@totejs/prettier-config': ^0.1.0 - '@totejs/uikit': ~2.44.5 + '@totejs/uikit': ~2.49.1 '@types/lodash-es': ^4.17.6 '@types/node': 18.16.0 '@types/react': 18.0.38 @@ -62,8 +62,8 @@ importers: wagmi: ^0.12.9 dependencies: '@babel/core': 7.22.5 - '@bnb-chain/greenfield-chain-sdk': 0.0.0-snapshot-20230724100555 - '@bnb-chain/greenfield-cosmos-types': 0.4.0-alpha.15 + '@bnb-chain/greenfield-chain-sdk': 0.0.0-snapshot-20230725025153 + '@bnb-chain/greenfield-cosmos-types': 0.4.0-alpha.13 '@emotion/react': 11.11.1_627697682086d325a0e273fee4549116 '@emotion/styled': 11.11.0_1e8dacba4d8e6343e430b8486686f015 '@next/bundle-analyzer': 13.4.5 @@ -72,7 +72,7 @@ importers: '@tanstack/react-table': 8.9.2_react-dom@18.2.0+react@18.2.0 '@tanstack/react-virtual': 3.0.0-alpha.0_react@18.2.0 '@totejs/icons': 2.13.0_aa3274991927adc2766d9259998fdd18 - '@totejs/uikit': 2.44.5_aa3274991927adc2766d9259998fdd18 + '@totejs/uikit': 2.49.1_aa3274991927adc2766d9259998fdd18 ahooks: 3.7.7_react@18.2.0 antd: 5.6.3_react-dom@18.2.0+react@18.2.0 apollo-node-client: 1.4.3 @@ -1622,8 +1622,8 @@ packages: '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 - /@bnb-chain/greenfield-chain-sdk/0.0.0-snapshot-20230724100555: - resolution: {integrity: sha512-1f4NMpMvgpYq0f9e3WwC4IlLEssOPUdZKO+1kIJEbm+JRyq4gmpqBkMUYQi8Jkp2gRb2WlVBzL5mDfJUO+LTgw==} + /@bnb-chain/greenfield-chain-sdk/0.0.0-snapshot-20230725025153: + resolution: {integrity: sha512-qw2rX5bhrbqlAVgcvGA3hoC7oA6oM38bT+lVsa91tAnWhNgFiJSk1sPzfxme+LRBfwkYFIasdw0ILx7VI+uG2A==} engines: {npm: please use pnpm, yarn: please use pnpm} dependencies: '@bnb-chain/greenfield-cosmos-types': 0.4.0-alpha.15 @@ -3711,6 +3711,21 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false + /@totejs/icons/2.14.0_aa3274991927adc2766d9259998fdd18: + resolution: {integrity: sha512-k2rr3SJD4zqjjMYIroNvdSxoJyqlq8TajFiV3llQXEDXKaZEiC8oShmIUcdfSm+4jVUa5mAob2sTptNKQqZ0og==} + peerDependencies: + '@emotion/react': '>=11' + '@emotion/styled': '>=11' + react: '>=16.9.0' + react-dom: '>=16.9.0' + dependencies: + '@emotion/react': 11.11.1_627697682086d325a0e273fee4549116 + '@emotion/styled': 11.11.0_1e8dacba4d8e6343e430b8486686f015 + '@totejs/styled-system': 2.12.0_react-dom@18.2.0+react@18.2.0 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + /@totejs/prettier-config/0.1.0: resolution: {integrity: sha512-N7ayi2uD5BUV44XDNHqHPQ3kWkCa73gTTLRDX0Doz42iSVszTne2ZtFppGIx/FDXwJfehnJiyaM1ZOrUzgn7QQ==} dependencies: @@ -3740,8 +3755,8 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /@totejs/uikit/2.44.5_aa3274991927adc2766d9259998fdd18: - resolution: {integrity: sha512-C3kNFdGBv62ghsmy+14SqjUACFylvSJY1AKEocbI5uylBZUJTO0xnIGHudXHyBUmlyvdkdxqjMrWSAYqiKZo/w==} + /@totejs/uikit/2.49.1_aa3274991927adc2766d9259998fdd18: + resolution: {integrity: sha512-ghOG/69Hjx0HjZSrBmK+N3VJAkqml9Ij9d6K3+mjRFOf3vyBsXElA7FzvHUEwo6PpbFFAzpw4yiouSJqhsWYPA==} peerDependencies: '@emotion/react': '>=11' '@emotion/styled': '>=11' @@ -3751,7 +3766,7 @@ packages: '@emotion/react': 11.11.1_627697682086d325a0e273fee4549116 '@emotion/styled': 11.11.0_1e8dacba4d8e6343e430b8486686f015 '@popperjs/core': 2.11.8 - '@totejs/icons': 2.13.0_aa3274991927adc2766d9259998fdd18 + '@totejs/icons': 2.14.0_aa3274991927adc2766d9259998fdd18 '@totejs/styled-system': 2.12.0_react-dom@18.2.0+react@18.2.0 '@xobotyi/scrollbar-width': 1.9.5 react: 18.2.0 @@ -5463,6 +5478,14 @@ packages: - encoding dev: false + /cross-fetch/4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + dependencies: + node-fetch: 2.6.12 + transitivePeerDependencies: + - encoding + dev: false + /cross-spawn/7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} From 511c5d2c2af5f7c0929d06c0f1cf22b4ae8f9f5b Mon Sep 17 00:00:00 2001 From: "Miya.ww" Date: Fri, 28 Jul 2023 14:24:32 +0800 Subject: [PATCH 04/20] fix(dcellar-web-ui): add batch delete --- apps/dcellar-web-ui/package.json | 2 +- .../object/components/BatchOperations.tsx | 21 +- .../modules/object/components/ObjectList.tsx | 2 +- .../batch-delete/BatchDeleteObject.tsx | 341 ++++++++++++++++++ .../src/modules/object/index.tsx | 9 +- .../src/pages/buckets/[...path].tsx | 8 +- 6 files changed, 376 insertions(+), 7 deletions(-) create mode 100644 apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx diff --git a/apps/dcellar-web-ui/package.json b/apps/dcellar-web-ui/package.json index a9197e49..5a78e4d5 100644 --- a/apps/dcellar-web-ui/package.json +++ b/apps/dcellar-web-ui/package.json @@ -18,7 +18,7 @@ "ahooks": "3.7.7", "hash-wasm": "4.9.0", "@babel/core": "^7.20.12", - "@bnb-chain/greenfield-chain-sdk": "0.0.0-snapshot-20230725025153", + "@bnb-chain/greenfield-chain-sdk": "0.2.2-alpha.15", "@bnb-chain/greenfield-cosmos-types": "0.4.0-alpha.13", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", diff --git a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx index 013d1310..f34fd22b 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx @@ -10,6 +10,7 @@ import { useOffChainAuth } from '@/hooks/useOffChainAuth'; import { useMount } from 'ahooks'; import { setupBucketQuota } from '@/store/slices/bucket'; import { quotaRemains } from '@/facade/bucket'; +import { BatchDeleteObject } from '@/modules/object/components/batch-delete/BatchDeleteObject'; interface BatchOperationsProps {} @@ -22,6 +23,7 @@ export const BatchOperations = memo(function BatchOperatio const { bucketName, objects, path, primarySp } = useAppSelector((root) => root.object); const quotas = useAppSelector((root) => root.bucket.quotas); const quotaData = quotas[bucketName]; + const [isBatchDeleteOpen, setBatchDeleteOpen] = React.useState(false); useMount(() => { dispatch(setupBucketQuota(bucketName)); @@ -50,9 +52,24 @@ export const BatchOperations = memo(function BatchOperatio // const domain = getDomain(); // todo }; - + const onBatchDelete = async () => { + const items = objects[path].filter((i) => selectedRowKeys.includes(i.objectName)); + let remainQuota = quotaRemains( + quotaData, + items.reduce((x, y) => x + y.payloadSize, 0), + ); + if (!remainQuota) return onError(E_NO_QUOTA); + console.log(isBatchDeleteOpen, 'onBatchDelete'); + setBatchDeleteOpen(true); + }; + const refetch = async (name?: string) => {}; return ( <> + setBatchDeleteOpen(false)} + /> {selected} File{selected > 1 && 's'} Selected{' '} (function BatchOperatio > - + diff --git a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx index 04b3a31a..6513a29b 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx @@ -1,4 +1,4 @@ -import React, { Key, memo, useState } from 'react'; +import React, { Key, memo, useState, useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '@/store'; import { _getAllList, diff --git a/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx new file mode 100644 index 00000000..25b19bd6 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx @@ -0,0 +1,341 @@ +import { ModalCloseButton, ModalHeader, ModalFooter, Text, Flex, toast, Box } from '@totejs/uikit'; +import { useAccount } from 'wagmi'; +import React, { useEffect, useState } from 'react'; +import { + renderBalanceNumber, + renderFeeValue, + renderInsufficientBalance, +} from '@/modules/file/utils'; +import { + BUTTON_GOT_IT, + FILE_DELETE_GIF, + FILE_DESCRIPTION_DELETE_ERROR, + FILE_FAILED_URL, + FILE_STATUS_DELETING, + FILE_TITLE_DELETE_FAILED, + FILE_TITLE_DELETING, + FOLDER_TITLE_DELETING, +} from '@/modules/file/constant'; +import { DCModal } from '@/components/common/DCModal'; +import { Tips } from '@/components/common/Tips'; +import { DCButton } from '@/components/common/DCButton'; +import { reportEvent } from '@/utils/reportEvent'; +import { getClient } from '@/base/client'; +import { signTypedDataV4 } from '@/utils/signDataV4'; +import { E_USER_REJECT_STATUS_NUM, broadcastFault } from '@/facade/error'; +import { useAppDispatch, useAppSelector } from '@/store'; +import { + ObjectItem, + TStatusDetail, + setSelectedRowKeys, + setStatusDetail, +} from '@/store/slices/object'; +import { MsgDeleteObjectTypeUrl } from '@bnb-chain/greenfield-chain-sdk'; +import { useAsyncEffect } from 'ahooks'; +import { getLockFee } from '@/utils/wallet'; +import { setupTmpAvailableBalance, setTmpAccount, TTmpAccount } from '@/store/slices/global'; +import { resolve } from '@/facade/common'; +import { createTmpAccount } from '@/facade/account'; +import { parseEther } from 'ethers/lib/utils.js'; +import { round } from 'lodash-es'; + +interface modalProps { + refetch: () => void; + isOpen: boolean; + cancelFn: () => void; +} + +const renderQuota = (key: string, value: string) => { + return ( + + + {key} + + + {value} + + + ); +}; + +const renderFee = ( + key: string, + bnbValue: string, + exchangeRate: number, + keyIcon?: React.ReactNode, +) => { + return ( + + + + {key} + + {keyIcon && ( + + {keyIcon} + + )} + + + {renderFeeValue(bnbValue, exchangeRate)} + + + ); +}; + +export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => { + const dispatch = useAppDispatch(); + const [lockFee, setLockFee] = useState(''); + const { loginAccount } = useAppSelector((root) => root.persist); + const { price: bnbPrice } = useAppSelector((root) => root.global.bnb); + const selectedRowKeys = useAppSelector((root) => root.object.selectedRowKeys); + const { editDelete, bucketName, primarySp, objectsInfo, path } = useAppSelector( + (root) => root.object, + ); + const exchangeRate = +bnbPrice ?? 0; + const [loading, setLoading] = useState(false); + const [buttonDisabled, setButtonDisabled] = useState(false); + const { _availableBalance: availableBalance } = useAppSelector((root) => root.global); + const [isModalOpen, setModalOpen] = useState(isOpen); + + const deleteObjects = selectedRowKeys.map((key) => { + return objectsInfo[path + '/' + key]; + }); + console.log(deleteObjects, 'deleteObjects'); + + const onClose = () => { + document.documentElement.style.overflowY = ''; + setModalOpen(false); + cancelFn(); + }; + const { gasObjects } = useAppSelector((root) => root.global.gasHub); + const simulateGasFee = gasObjects[MsgDeleteObjectTypeUrl]?.gasFee ?? 0; + const { connector } = useAccount(); + + useEffect(() => { + if (!isOpen) return; + setModalOpen(isOpen); + dispatch(setupTmpAvailableBalance(loginAccount)); + }, [isOpen, dispatch, loginAccount]); + + useAsyncEffect(async () => { + const totalPayloadSize = deleteObjects.reduce((acc, cur) => { + return acc + Number(cur.object_info.payload_size); + }, 0); + let lockFeeInBNB = await getLockFee(totalPayloadSize, primarySp.operatorAddress); + setLockFee(lockFeeInBNB); + }, [isOpen]); + + useEffect(() => { + if (!simulateGasFee || Number(simulateGasFee) < 0 || !lockFee || Number(lockFee) < 0) { + setButtonDisabled(false); + return; + } + const currentBalance = Number(availableBalance); + if (currentBalance >= Number(simulateGasFee) + Number(lockFee)) { + setButtonDisabled(false); + return; + } + setButtonDisabled(true); + }, [simulateGasFee, availableBalance, lockFee]); + const description = 'Are you sure you want to delete these objects?'; + + const setFailedStatusModal = (description: string, error: any) => { + dispatch( + setStatusDetail({ + icon: FILE_FAILED_URL, + title: FILE_TITLE_DELETE_FAILED, + desc: description, + buttonText: BUTTON_GOT_IT, + errorText: 'Error message: ' + error?.message ?? '', + buttonOnClick: () => { + dispatch(setStatusDetail({} as TStatusDetail)); + }, + }), + ); + }; + const errorHandler = (error: string) => { + setLoading(false); + dispatch( + setStatusDetail({ + title: FILE_TITLE_DELETE_FAILED, + icon: FILE_FAILED_URL, + desc: 'Sorry, there’s something wrong when signing with the wallet.', + buttonText: BUTTON_GOT_IT, + errorText: 'Error message: ' + error, + buttonOnClick: () => dispatch(setStatusDetail({} as TStatusDetail)), + }), + ); + }; + const deleteObject = async (objectName: string, tmpAccount: TTmpAccount) => { + const client = await getClient(); + const delObjTx = await client.object.deleteObject({ + bucketName, + objectName: objectName, + operator: tmpAccount.address, + }); + const simulateInfo = await delObjTx.simulate({ + denom: 'BNB', + }); + const [txRes, error] = await delObjTx + .broadcast({ + denom: 'BNB', + gasLimit: Number(simulateInfo?.gasLimit), + gasPrice: simulateInfo?.gasPrice || '5000000000', + payer: tmpAccount.address, + granter: '', + privateKey: tmpAccount.privateKey, + // signTypedDataCallback: async (addr: string, message: string) => { + // const provider = await connector?.getProvider(); + // return await signTypedDataV4(provider, addr, message); + // }, + }) + .then(resolve, broadcastFault); + if (txRes === null) { + dispatch(setStatusDetail({} as TStatusDetail)); + return toast.error({ description: error || 'Delete file error.' }); + } + if (txRes.code === 0) { + toast.success({ + description: 'File deleted successfully.', + }); + reportEvent({ + name: 'dc.toast.file_delete.success.show', + }); + } else { + toast.error({ description: 'Delete file error.' }); + } + }; + + const onConfirmDelete = async () => { + try { + setLoading(true); + onClose(); + dispatch( + setStatusDetail({ + icon: FILE_DELETE_GIF, + title: FILE_TITLE_DELETING, + desc: FILE_STATUS_DELETING, + }), + ); + console.log(lockFee, 'parseEther(lockFee)'); + const [tmpAccount, err] = await createTmpAccount({ + address: loginAccount, + bucketName, + amount: parseEther(round(Number(lockFee), 6).toString()).toString(), + }); + if (!tmpAccount) { + return errorHandler(err); + } + + dispatch(setTmpAccount(tmpAccount)); + deleteObjects.map((obj) => { + deleteObject(obj.object_info.object_name, tmpAccount); + }); + refetch(); + onClose(); + dispatch(setSelectedRowKeys([])); + dispatch(setStatusDetail({} as TStatusDetail)); + setLoading(false); + } catch (error: any) { + setLoading(false); + const { code = '' } = error; + if (code && String(code) === E_USER_REJECT_STATUS_NUM) { + dispatch(setStatusDetail({} as TStatusDetail)); + onClose(); + return; + } + // eslint-disable-next-line no-console + console.error('Delete file error.', error); + setFailedStatusModal(FILE_DESCRIPTION_DELETE_ERROR, error); + } + }; + + return ( + + Confirm Delete + + + {description} + + + {renderFee( + 'Unlocked storage fee', + lockFee, + exchangeRate, + + + We will unlock the storage fee after you delete the file. + + + } + />, + )} + {renderFee('Gas Fee', simulateGasFee + '', exchangeRate)} + + + + {renderInsufficientBalance(simulateGasFee + '', lockFee, availableBalance || '0', { + gaShowName: 'dc.file.delete_confirm.depost.show', + gaClickName: 'dc.file.delete_confirm.transferin.click', + })} + + + Available balance: {renderBalanceNumber(availableBalance || '0')} + + + + + + Cancel + + + Delete + + + + ); +}; diff --git a/apps/dcellar-web-ui/src/modules/object/index.tsx b/apps/dcellar-web-ui/src/modules/object/index.tsx index 3f40523b..2a0c4cad 100644 --- a/apps/dcellar-web-ui/src/modules/object/index.tsx +++ b/apps/dcellar-web-ui/src/modules/object/index.tsx @@ -13,7 +13,12 @@ import { ObjectBreadcrumb } from '@/modules/object/components/ObjectBreadcrumb'; import { isEmpty, last } from 'lodash-es'; import { NewObject } from '@/modules/object/components/NewObject'; import { Text, Tooltip } from '@totejs/uikit'; -import { selectObjectList, setFolders, setPrimarySp } from '@/store/slices/object'; +import { + selectObjectList, + setFolders, + setPrimarySp, + setSelectedRowKeys, +} from '@/store/slices/object'; import { ObjectList } from '@/modules/object/components/ObjectList'; import React, { useEffect } from 'react'; import { SpItem } from '@/store/slices/sp'; @@ -31,9 +36,9 @@ export const ObjectsPage = () => { const items = path as string[]; const title = last(items)!; const [bucketName, ...folders] = items; + useEffect(() => { dispatch(setFolders({ bucketName, folders })); - return () => { dispatch(setFolders({ bucketName: '', folders: [] })); }; diff --git a/apps/dcellar-web-ui/src/pages/buckets/[...path].tsx b/apps/dcellar-web-ui/src/pages/buckets/[...path].tsx index dd8da714..0ee65591 100644 --- a/apps/dcellar-web-ui/src/pages/buckets/[...path].tsx +++ b/apps/dcellar-web-ui/src/pages/buckets/[...path].tsx @@ -1,5 +1,11 @@ import { ObjectsPage } from '@/modules/object'; - +import React, { useEffect } from 'react'; +import { useAppDispatch } from '@/store'; +import { setSelectedRowKeys } from '@/store/slices/object'; export default function Objects() { + const dispatch = useAppDispatch(); + useEffect(() => { + dispatch(setSelectedRowKeys([])); + }, [dispatch]); return ; } From 0aca0e3dd6981e51bc228e7d05b938ff0ddffa97 Mon Sep 17 00:00:00 2001 From: "Miya.ww" Date: Fri, 28 Jul 2023 23:10:51 +0800 Subject: [PATCH 05/20] fix(dcellar-web-ui): batch delete commit --- apps/dcellar-web-ui/src/facade/account.ts | 99 ++++++++++++------- .../batch-delete/BatchDeleteObject.tsx | 96 +++++++++++++----- 2 files changed, 137 insertions(+), 58 deletions(-) diff --git a/apps/dcellar-web-ui/src/facade/account.ts b/apps/dcellar-web-ui/src/facade/account.ts index d52cf342..de924fad 100644 --- a/apps/dcellar-web-ui/src/facade/account.ts +++ b/apps/dcellar-web-ui/src/facade/account.ts @@ -1,5 +1,12 @@ import { getClient } from '@/base/client'; -import { GRNToString, MsgCreateObjectTypeUrl, MsgDeleteObjectTypeUrl, PermissionTypes, newBucketGRN } from '@bnb-chain/greenfield-chain-sdk'; +import { + GRNToString, + MsgCreateObjectTypeUrl, + MsgDeleteObjectTypeUrl, + PermissionTypes, + newBucketGRN, + newObjectGRN, +} from '@bnb-chain/greenfield-chain-sdk'; import { Coin } from '@bnb-chain/greenfield-cosmos-types/cosmos/base/v1beta1/coin'; import { Wallet } from 'ethers'; import { parseEther } from 'ethers/lib/utils.js'; @@ -7,8 +14,10 @@ import { resolve } from './common'; import { ErrorResponse, broadcastFault, commonFault, simulateFault } from './error'; import { UNKNOWN_ERROR } from '@/modules/file/constant'; import { TTmpAccount } from '@/store/slices/global'; +import { signTypedDataV4 } from '@/utils/signDataV4'; export type QueryBalanceRequest = { address: string; denom?: string }; +type ActionType = 'delete' | 'create'; export const getAccountBalance = async ({ address, @@ -21,7 +30,22 @@ export const getAccountBalance = async ({ return balance!; }; -export const createTmpAccount = async ({address, bucketName, amount}: any): Promise => { +export const createTmpAccount = async ({ + address, + bucketName, + amount, + connector, + actionType, + objectList, +}: any): Promise => { + //messages and resources are different for create and delete + const isDelete = actionType === 'delete'; + + const grantAllowedMessage = isDelete ? [MsgDeleteObjectTypeUrl] : [MsgCreateObjectTypeUrl]; + const statementAction = isDelete + ? [PermissionTypes.ActionType.ACTION_DELETE_OBJECT] + : [PermissionTypes.ActionType.ACTION_CREATE_OBJECT]; + // 1. create temporary account const wallet = Wallet.createRandom(); console.log('wallet', wallet.address, wallet.privateKey); @@ -29,57 +53,66 @@ export const createTmpAccount = async ({address, bucketName, amount}: any): Prom // 2. allow temporary account to submit specified tx and amount const client = await getClient(); const grantAllowanceTx = await client.feegrant.grantAllowance({ - granter: address, - grantee: wallet.address, - allowedMessages: [MsgCreateObjectTypeUrl], - amount: parseEther(amount || '0.1').toString(), - denom: 'BNB', + granter: address, + grantee: wallet.address, + allowedMessages: grantAllowedMessage, + amount: parseEther(amount <= 0 ? '0.1' : amount).toString(), + denom: 'BNB', }); - + const resources = isDelete + ? objectList.map((objectName: string) => { + return GRNToString(newObjectGRN(bucketName, objectName)); + }) + : [GRNToString(newBucketGRN(bucketName))]; // 3. Put bucket policy so that the temporary account can create objects within this bucket const statement: PermissionTypes.Statement = { effect: PermissionTypes.Effect.EFFECT_ALLOW, - actions: [PermissionTypes.ActionType.ACTION_CREATE_OBJECT], - resources: [GRNToString(newBucketGRN(bucketName))], + actions: statementAction, + resources: resources, }; - const [putPolicyTx, putPolicyError] = await client.bucket.putBucketPolicy(bucketName, { + const putPolicyTx = await client.bucket.putBucketPolicy(bucketName, { operator: address, statements: [statement], principal: { type: PermissionTypes.PrincipalType.PRINCIPAL_TYPE_GNFD_ACCOUNT, value: wallet.address, }, - }).then(resolve, commonFault); - if (!putPolicyTx) { - return [null, putPolicyError]; - } + }); - // 4. broadcast txs include 2 msg - const txs = await client.basic.multiTx([grantAllowanceTx, putPolicyTx]); - const [simulateInfo, simulateError] = await txs.simulate({ - denom: 'BNB', - }).then(resolve, simulateFault); - if (simulateError) { - return [null, simulateError]; - } + // 4. broadcast txs include 2 msg + const txs = await client.basic.multiTx([grantAllowanceTx, putPolicyTx]); - console.log('simuluateInfo', simulateInfo); + const simulateInfo = await txs.simulate({ + denom: 'BNB', + }); const payload = { denom: 'BNB', gasLimit: Number(210000), gasPrice: '5000000000', payer: address, granter: '', - } - console.log('payload', payload) - const [res, error] = await txs.broadcast(payload).then(resolve, broadcastFault); + }; + const payloadParam = isDelete + ? { + ...payload, + signTypedDataCallback: async (addr: string, message: string) => { + const provider = await connector?.getProvider(); + return await signTypedDataV4(provider, addr, message); + }, + } + : payload; + console.log('payload', payload); + const [res, error] = await txs.broadcast(payloadParam).then(resolve, broadcastFault); - if (res && res.code !== 0 || error) { + if ((res && res.code !== 0) || error) { return [null, error || UNKNOWN_ERROR]; } - return [{ - address: wallet.address, - privateKey: wallet.privateKey - }, null]; -} \ No newline at end of file + return [ + { + address: wallet.address, + privateKey: wallet.privateKey, + }, + null, + ]; +}; diff --git a/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx index 16624078..01cf5c06 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx @@ -1,6 +1,6 @@ import { ModalCloseButton, ModalHeader, ModalFooter, Text, Flex, toast, Box } from '@totejs/uikit'; import { useAccount } from 'wagmi'; -import React, { useEffect, useState } from 'react'; +import React, { use, useEffect, useState } from 'react'; import { renderBalanceNumber, renderFeeValue, @@ -38,6 +38,7 @@ import { resolve } from '@/facade/common'; import { createTmpAccount } from '@/facade/account'; import { parseEther } from 'ethers/lib/utils.js'; import { round } from 'lodash-es'; +import { ColoredWaitingIcon } from '@totejs/icons'; interface modalProps { refetch: () => void; @@ -89,10 +90,8 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => const { loginAccount } = useAppSelector((root) => root.persist); const { price: bnbPrice } = useAppSelector((root) => root.global.bnb); const selectedRowKeys = useAppSelector((root) => root.object.selectedRowKeys); - const { editDelete, bucketName, objectsInfo, path } = useAppSelector( - (root) => root.object, - ); - const {primarySpInfo}= useAppSelector((root) => root.sp); + const { bucketName, objectsInfo, path } = useAppSelector((root) => root.object); + const { primarySpInfo } = useAppSelector((root) => root.sp); const primarySp = primarySpInfo[bucketName]; const exchangeRate = +bnbPrice ?? 0; const [loading, setLoading] = useState(false); @@ -103,7 +102,6 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => const deleteObjects = selectedRowKeys.map((key) => { return objectsInfo[path + '/' + key]; }); - console.log(deleteObjects, 'deleteObjects'); const onClose = () => { document.documentElement.style.overflowY = ''; @@ -173,33 +171,78 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => const client = await getClient(); const delObjTx = await client.object.deleteObject({ bucketName, - objectName: objectName, + objectName, operator: tmpAccount.address, }); + const simulateInfo = await delObjTx.simulate({ denom: 'BNB', }); - const [txRes, error] = await delObjTx - .broadcast({ - denom: 'BNB', - gasLimit: Number(simulateInfo?.gasLimit), - gasPrice: simulateInfo?.gasPrice || '5000000000', - payer: tmpAccount.address, - granter: '', - privateKey: tmpAccount.privateKey, - // signTypedDataCallback: async (addr: string, message: string) => { - // const provider = await connector?.getProvider(); - // return await signTypedDataV4(provider, addr, message); - // }, - }) - .then(resolve, broadcastFault); + + const txRes = await delObjTx.broadcast({ + denom: 'BNB', + gasLimit: Number(simulateInfo?.gasLimit), + gasPrice: simulateInfo?.gasPrice || '5000000000', + payer: tmpAccount.address, + granter: loginAccount, + privateKey: tmpAccount.privateKey, + }); + console.log(txRes, 'txRes'); + + if (txRes === null) { + dispatch(setStatusDetail({} as TStatusDetail)); + return toast.error({ description: 'Delete object error.' }); + } + if (txRes.code === 0) { + toast.success({ + description: 'object deleted successfully.', + }); + reportEvent({ + name: 'dc.toast.file_delete.success.show', + }); + } else { + toast.error({ description: 'Delete file error.' }); + } + }; + const deleteObjectUseMulti = async (objectList: any[], tmpAccount: TTmpAccount) => { + const client = await getClient(); + const multiMsg: any[] = await Promise.all( + objectList.map(async (objectName) => { + const delTx = await client.object.deleteObject({ + bucketName, + objectName, + operator: tmpAccount.address, + }); + return delTx; + }), + ); + + console.log(multiMsg, 'multiMsg'); + const delObjTxs = await client.basic.multiTx(multiMsg); + + console.log(delObjTxs, 'delObjTxs'); + + const simulateInfo = await delObjTxs.simulate({ + denom: 'BNB', + }); + + const txRes = await delObjTxs.broadcast({ + denom: 'BNB', + gasLimit: Number(simulateInfo?.gasLimit), + gasPrice: simulateInfo?.gasPrice || '5000000000', + payer: tmpAccount.address, + granter: loginAccount, + privateKey: tmpAccount.privateKey, + }); + console.log(txRes, 'txRes'); + if (txRes === null) { dispatch(setStatusDetail({} as TStatusDetail)); - return toast.error({ description: error || 'Delete file error.' }); + return toast.error({ description: 'Delete object error.' }); } if (txRes.code === 0) { toast.success({ - description: 'File deleted successfully.', + description: 'object deleted successfully.', }); reportEvent({ name: 'dc.toast.file_delete.success.show', @@ -220,20 +263,23 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => desc: FILE_STATUS_DELETING, }), ); - console.log(lockFee, 'parseEther(lockFee)'); const [tmpAccount, err] = await createTmpAccount({ address: loginAccount, bucketName, amount: parseEther(round(Number(lockFee), 6).toString()).toString(), + connector, + actionType: 'delete', + objectList: selectedRowKeys, }); if (!tmpAccount) { return errorHandler(err); } - dispatch(setTmpAccount(tmpAccount)); + // deleteObjectUseMulti(selectedRowKeys, tmpAccount); deleteObjects.map((obj) => { deleteObject(obj.object_info.object_name, tmpAccount); }); + toast.info({ description: 'Objects deleting', icon: }); refetch(); onClose(); dispatch(setSelectedRowKeys([])); From fe0019556d6d517f4659ea3d7d6051abe3578050 Mon Sep 17 00:00:00 2001 From: "miya@nodereal" Date: Tue, 1 Aug 2023 12:58:53 +0800 Subject: [PATCH 06/20] fix: batch delete --- apps/dcellar-web-ui/package.json | 2 +- .../components/layout/Header/NewBalance.tsx | 2 +- .../src/components/layout/Header/index.tsx | 2 +- apps/dcellar-web-ui/src/constants/links.ts | 2 +- apps/dcellar-web-ui/src/facade/bucket.ts | 19 +++++- .../modules/bucket/components/NameItem.tsx | 2 +- .../List/components/BucketNameItem.tsx | 2 +- .../src/modules/file/constant.ts | 2 + .../object/components/BatchOperations.tsx | 16 +++-- .../object/components/DeleteObject.tsx | 48 +++++++++++++- .../object/components/FolderNotEmpty.tsx | 2 +- .../modules/object/components/ObjectList.tsx | 54 +++++----------- .../batch-delete/BatchDeleteObject.tsx | 62 ++++--------------- .../src/pages/buckets/index.tsx | 9 ++- 14 files changed, 110 insertions(+), 114 deletions(-) diff --git a/apps/dcellar-web-ui/package.json b/apps/dcellar-web-ui/package.json index 1cc711a0..23c6055e 100644 --- a/apps/dcellar-web-ui/package.json +++ b/apps/dcellar-web-ui/package.json @@ -18,7 +18,7 @@ "ahooks": "3.7.7", "hash-wasm": "4.9.0", "@babel/core": "^7.20.12", - "@bnb-chain/greenfield-chain-sdk": "0.2.2-alpha.15", + "@bnb-chain/greenfield-chain-sdk": "0.2.2-alpha.17", "@bnb-chain/greenfield-cosmos-types": "0.4.0-alpha.18", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", diff --git a/apps/dcellar-web-ui/src/components/layout/Header/NewBalance.tsx b/apps/dcellar-web-ui/src/components/layout/Header/NewBalance.tsx index fd5377f8..4572ba6c 100644 --- a/apps/dcellar-web-ui/src/components/layout/Header/NewBalance.tsx +++ b/apps/dcellar-web-ui/src/components/layout/Header/NewBalance.tsx @@ -82,7 +82,7 @@ const NewBalance = (props: any) => { flow rate based on your file size. => { + const client = await getClient(); + const deleteBucketTx = await client.bucket.deleteBucket({ + bucketName, + operator: address, + }); + const [data, error] = await deleteBucketTx.simulate({ + denom: 'BNB', + }).then(resolve, simulateFault); + + if (error) return [null, error]; + return [data!, null]; +}; \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/modules/bucket/components/NameItem.tsx b/apps/dcellar-web-ui/src/modules/bucket/components/NameItem.tsx index 5439a34d..e9bc9226 100644 --- a/apps/dcellar-web-ui/src/modules/bucket/components/NameItem.tsx +++ b/apps/dcellar-web-ui/src/modules/bucket/components/NameItem.tsx @@ -20,7 +20,7 @@ export const NameItem = memo(function NameItem({ item }) { +delete_at * 1000 + 7 * 24 * 60 * 60 * 1000, 'YYYY-MM-DD HH:mm:ss', ); - const more = 'https://docs.nodereal.io/docs/faq-1#question-what-is-discontinue'; + const more = 'https://docs.nodereal.io/docs/dcellar-faq#question-what-is-discontinue'; const content = `This item will be deleted by SP with an estimated time of ${estimateTime}. Please backup your data in time.`; return ( diff --git a/apps/dcellar-web-ui/src/modules/buckets/List/components/BucketNameItem.tsx b/apps/dcellar-web-ui/src/modules/buckets/List/components/BucketNameItem.tsx index e2253343..03ef880b 100644 --- a/apps/dcellar-web-ui/src/modules/buckets/List/components/BucketNameItem.tsx +++ b/apps/dcellar-web-ui/src/modules/buckets/List/components/BucketNameItem.tsx @@ -34,7 +34,7 @@ export const BucketNameItem = ({ info }: any) => { > {info.getValue()} - {isContinued && } + {isContinued && } ) } diff --git a/apps/dcellar-web-ui/src/modules/file/constant.ts b/apps/dcellar-web-ui/src/modules/file/constant.ts index ec56c34b..c2e92261 100644 --- a/apps/dcellar-web-ui/src/modules/file/constant.ts +++ b/apps/dcellar-web-ui/src/modules/file/constant.ts @@ -15,6 +15,7 @@ const DELETE_ICON_URL = `${assetPrefix}/images/icons/delete.gif`; const UPLOAD_IMAGE_URL = `${assetPrefix}/images/files/upload.svg`; const FILE_INFO_IMAGE_URL = `${assetPrefix}/images/files/upload_file.svg`; const UNKNOWN_ERROR_URL = `${assetPrefix}/images/files/unknown.svg`; +const NOT_EMPTY = `${assetPrefix}/images/buckets/bucket-not-empty.svg`; // status_TITLE const FILE_TITLE_UPLOADING = 'Uploading File'; @@ -130,4 +131,5 @@ export { FILE_ACCESS_URL, FILE_STATUS_ACCESS, FILE_ACCESS, + NOT_EMPTY }; diff --git a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx index f34fd22b..365c7e07 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx @@ -20,11 +20,12 @@ export const BatchOperations = memo(function BatchOperatio const dispatch = useAppDispatch(); const { setOpenAuthModal } = useOffChainAuth(); const { loginAccount } = useAppSelector((root) => root.persist); - const { bucketName, objects, path, primarySp } = useAppSelector((root) => root.object); - const quotas = useAppSelector((root) => root.bucket.quotas); + const { bucketName, objects, path } = useAppSelector((root) => root.object); + const { quotas } = useAppSelector((root) => root.bucket); const quotaData = quotas[bucketName]; const [isBatchDeleteOpen, setBatchDeleteOpen] = React.useState(false); - + const { primarySpInfo } = useAppSelector((root) => root.sp); + const primarySp = primarySpInfo[bucketName]; useMount(() => { dispatch(setupBucketQuota(bucketName)); }); @@ -42,10 +43,7 @@ export const BatchOperations = memo(function BatchOperatio const onBatchDownload = async () => { const items = objects[path].filter((i) => selectedRowKeys.includes(i.objectName)); - let remainQuota = quotaRemains( - quotaData, - items.reduce((x, y) => x + y.payloadSize, 0), - ); + let remainQuota = quotaRemains(quotaData, String(items.reduce((x, y) => x + y.payloadSize, 0))); if (!remainQuota) return onError(E_NO_QUOTA); const operator = primarySp.operatorAddress; // const { seedString } = await dispatch(getSpOffChainData(loginAccount, operator)); @@ -55,8 +53,8 @@ export const BatchOperations = memo(function BatchOperatio const onBatchDelete = async () => { const items = objects[path].filter((i) => selectedRowKeys.includes(i.objectName)); let remainQuota = quotaRemains( - quotaData, - items.reduce((x, y) => x + y.payloadSize, 0), + { readQuota: 2000000, freeQuota: 2000000, consumedQuota: 2000000 }, + String(items.reduce((x, y) => x + y.payloadSize, 0)), ); if (!remainQuota) return onError(E_NO_QUOTA); console.log(isBatchDeleteOpen, 'onBatchDelete'); diff --git a/apps/dcellar-web-ui/src/modules/object/components/DeleteObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/DeleteObject.tsx index 6e0031ce..aabe5da6 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/DeleteObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/DeleteObject.tsx @@ -24,6 +24,7 @@ import { FILE_TITLE_DELETE_FAILED, FILE_TITLE_DELETING, FOLDER_TITLE_DELETING, + NOT_EMPTY, } from '@/modules/file/constant'; import { DCModal } from '@/components/common/DCModal'; import { Tips } from '@/components/common/Tips'; @@ -39,6 +40,7 @@ import { useAsyncEffect } from 'ahooks'; import { getLockFee } from '@/utils/wallet'; import { setupTmpAvailableBalance } from '@/store/slices/global'; import { resolve } from '@/facade/common'; +import { getListObjects } from '@/facade/object'; interface modalProps { refetch: () => void; @@ -87,7 +89,7 @@ export const DeleteObject = ({ refetch }: modalProps) => { const [lockFee, setLockFee] = useState(''); const { loginAccount: address } = useAppSelector((root) => root.persist); const { price: bnbPrice } = useAppSelector((root) => root.global.bnb); - const {primarySpInfo}= useAppSelector((root) => root.sp); + const { primarySpInfo } = useAppSelector((root) => root.sp); const { editDelete, bucketName } = useAppSelector((root) => root.object); const primarySp = primarySpInfo[bucketName]; const exchangeRate = +bnbPrice ?? 0; @@ -128,13 +130,33 @@ export const DeleteObject = ({ refetch }: modalProps) => { }, [simulateGasFee, availableBalance, lockFee]); const filePath = editDelete.objectName.split('/'); const isFolder = editDelete.objectName.endsWith('/'); - const isSavedSixMonths = getUtcZeroTimestamp() - editDelete.createAt * 1000 > 6 * 30 * 24 * 60 * 60 * 1000; + const isSavedSixMonths = + getUtcZeroTimestamp() - editDelete.createAt * 1000 > 6 * 30 * 24 * 60 * 60 * 1000; const showName = filePath[filePath.length - 1]; const folderName = filePath[filePath.length - 2]; const description = isFolder ? `Are you sure you want to delete folder "${folderName}"?` : `Are you sure you want to delete file "${showName}"?`; + const isFolderEmpty = async (record: ObjectItem) => { + const _query = new URLSearchParams(); + _query.append('delimiter', '/'); + _query.append('maxKeys', '2'); + _query.append('prefix', `${record.objectName}`); + + const params = { + address: primarySp.operatorAddress, + bucketName: bucketName, + prefix: record.objectName, + query: _query, + endpoint: primarySp.endpoint, + seedString: '', + }; + const [res, error] = await getListObjects(params); + if (error || !res || res.code !== 0) return [null, String(error || res?.message)]; + const list = res.body!; + return list.key_count === '1' && list.objects[0].object_info.object_name === record.objectName; + }; const setFailedStatusModal = (description: string, error: any) => { dispatch( setStatusDetail({ @@ -160,6 +182,8 @@ export const DeleteObject = ({ refetch }: modalProps) => { > Confirm Delete + {/* {deleteFolderNotEmpty && } */} + {!isFolder && isSavedSixMonths && ( { textDecoration={'underline'} cursor={'pointer'} href="https://docs.nodereal.io/docs/dcellar-faq#fee-related " - target='_blank' + target="_blank" > Learn more @@ -254,6 +278,24 @@ export const DeleteObject = ({ refetch }: modalProps) => { try { setLoading(true); onClose(); + if (!isFolderEmpty) { + dispatch( + setStatusDetail({ + icon: NOT_EMPTY, + title: 'Folder Not Empty', + desc: '', + buttonText: BUTTON_GOT_IT, + errorText: + 'Only empty folder can be deleted. Please delete all objects in this folder first', + buttonOnClick: () => { + dispatch(setStatusDetail({} as TStatusDetail)); + }, + }), + ); + setLoading(false); + dispatch(setStatusDetail({} as TStatusDetail)); + onClose(); + } dispatch( setStatusDetail({ icon: FILE_DELETE_GIF, diff --git a/apps/dcellar-web-ui/src/modules/object/components/FolderNotEmpty.tsx b/apps/dcellar-web-ui/src/modules/object/components/FolderNotEmpty.tsx index b9abbc75..735dfca6 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/FolderNotEmpty.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/FolderNotEmpty.tsx @@ -34,7 +34,7 @@ export const FolderNotEmpty = () => { lineHeight="150%" marginY={'16px'} > - Folder not Empty + Folder Not Empty Only empty folder can be deleted. Please delete all objects in this folder first. diff --git a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx index 02a0178e..80f179b0 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx @@ -44,7 +44,6 @@ import { DownloadObject } from './DownloadObject'; import { setupBucketQuota } from '@/store/slices/bucket'; import { quotaRemains } from '@/facade/bucket'; import { OBJECT_ERROR_TYPES, ObjectErrorType } from '../ObjectError'; -import { FolderNotEmpty } from '@/modules/object/components/FolderNotEmpty'; import { E_GET_QUOTA_FAILED, @@ -53,7 +52,7 @@ import { E_OFF_CHAIN_AUTH, E_UNKNOWN, } from '@/facade/error'; -import { downloadObject } from '@/facade/object'; +import { downloadObject, getListObjects } from '@/facade/object'; import { getObjectInfoAndBucketQuota } from '@/facade/common'; import { UploadObjects } from '@/modules/upload/UploadObjects'; import { OBJECT_SEALED_STATUS } from '@/modules/file/constant'; @@ -83,13 +82,13 @@ export const ObjectList = memo(function ObjectList() { } = useAppSelector((root) => root.persist); const [rowIndex, setRowIndex] = useState(-1); - const [deleteFolderNotEmpty, setDeleteFolderNotEmpty] = useState(false); + // const [deleteFolderNotEmpty, setDeleteFolderNotEmpty] = useState(false); const { bucketName, prefix, path, objectsInfo, selectedRowKeys } = useAppSelector( (root) => root.object, ); const currentPage = useAppSelector(selectPathCurrent); const { discontinue, owner } = useAppSelector((root) => root.bucket); - const { spInfo, primarySpInfo} = useAppSelector((root) => root.sp); + const { spInfo, primarySpInfo } = useAppSelector((root) => root.sp); const loading = useAppSelector(selectPathLoading); const objectList = useAppSelector(selectObjectList); const { setOpenAuthModal } = useOffChainAuth(); @@ -102,7 +101,9 @@ export const ObjectList = memo(function ObjectList() { useAsyncEffect(async () => { if (!primarySp) return; - const { seedString } = await dispatch(getSpOffChainData(loginAccount, primarySp.operatorAddress)); + const { seedString } = await dispatch( + getSpOffChainData(loginAccount, primarySp.operatorAddress), + ); const query = new URLSearchParams(); const params = { seedString, @@ -135,14 +136,16 @@ export const ObjectList = memo(function ObjectList() { const config = accounts[loginAccount] || {}; if (config.directDownload) { - const { seedString } = await dispatch(getSpOffChainData(loginAccount, primarySp.operatorAddress)); + const { seedString } = await dispatch( + getSpOffChainData(loginAccount, primarySp.operatorAddress), + ); const gParams = { bucketName, objectName: object.objectName, endpoint: primarySp.endpoint, seedString, address: loginAccount, - } + }; const [objectInfo, quotaData] = await getObjectInfoAndBucketQuota(gParams); if (objectInfo === null) { return onError(E_UNKNOWN); @@ -177,17 +180,7 @@ export const ObjectList = memo(function ObjectList() { case 'detail': return dispatch(setEditDetail(record)); case 'delete': - let isFolder = record.objectName.endsWith('/'); - setDeleteFolderNotEmpty(false); - if (isFolder) { - let res = await isFolderEmpty(record); - if (!res) { - setDeleteFolderNotEmpty(true); - } - return dispatch(setEditDelete(record)); - } else { - return dispatch(setEditDelete(record)); - } + return dispatch(setEditDelete(record)); case 'share': return dispatch(setEditShare(record)); case 'download': @@ -196,24 +189,6 @@ export const ObjectList = memo(function ObjectList() { return dispatch(setEditCancel(record)); } }; - const isFolderEmpty = async (record: ObjectItem) => { - const _query = new URLSearchParams(); - _query.append('delimiter', '/'); - _query.append('maxKeys', '1000'); - _query.append('prefix', `${record.objectName}`); - - const params = { - address: primarySp.operatorAddress, - bucketName: bucketName, - prefix: record.objectName, - query: _query, - endpoint: primarySp.endpoint, - seedString: '', - maxKeys: 1000, - }; - const [res] = await _getAllList(params); - return res?.objects?.length === 1; - }; const columns: ColumnProps[] = [ { key: 'objectName', @@ -371,7 +346,9 @@ export const ObjectList = memo(function ObjectList() { const refetch = async (name?: string) => { if (!primarySp) return; - const { seedString } = await dispatch(getSpOffChainData(loginAccount, primarySp.operatorAddress)); + const { seedString } = await dispatch( + getSpOffChainData(loginAccount, primarySp.operatorAddress), + ); const query = new URLSearchParams(); const params = { seedString, @@ -392,8 +369,7 @@ export const ObjectList = memo(function ObjectList() { return ( <> {editCreate && } - {editDelete?.objectName && !deleteFolderNotEmpty &&} - {deleteFolderNotEmpty && } + {editDelete?.objectName && } {statusDetail.title && } {editDetail?.objectName && } {editShare?.objectName && } diff --git a/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx index 01cf5c06..70bac0b5 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx @@ -204,53 +204,6 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => toast.error({ description: 'Delete file error.' }); } }; - const deleteObjectUseMulti = async (objectList: any[], tmpAccount: TTmpAccount) => { - const client = await getClient(); - const multiMsg: any[] = await Promise.all( - objectList.map(async (objectName) => { - const delTx = await client.object.deleteObject({ - bucketName, - objectName, - operator: tmpAccount.address, - }); - return delTx; - }), - ); - - console.log(multiMsg, 'multiMsg'); - const delObjTxs = await client.basic.multiTx(multiMsg); - - console.log(delObjTxs, 'delObjTxs'); - - const simulateInfo = await delObjTxs.simulate({ - denom: 'BNB', - }); - - const txRes = await delObjTxs.broadcast({ - denom: 'BNB', - gasLimit: Number(simulateInfo?.gasLimit), - gasPrice: simulateInfo?.gasPrice || '5000000000', - payer: tmpAccount.address, - granter: loginAccount, - privateKey: tmpAccount.privateKey, - }); - console.log(txRes, 'txRes'); - - if (txRes === null) { - dispatch(setStatusDetail({} as TStatusDetail)); - return toast.error({ description: 'Delete object error.' }); - } - if (txRes.code === 0) { - toast.success({ - description: 'object deleted successfully.', - }); - reportEvent({ - name: 'dc.toast.file_delete.success.show', - }); - } else { - toast.error({ description: 'Delete file error.' }); - } - }; const onConfirmDelete = async () => { try { @@ -275,10 +228,17 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => return errorHandler(err); } dispatch(setTmpAccount(tmpAccount)); - // deleteObjectUseMulti(selectedRowKeys, tmpAccount); - deleteObjects.map((obj) => { - deleteObject(obj.object_info.object_name, tmpAccount); - }); + + async function deleteInRow() { + if (!tmpAccount) return; + for (let obj of deleteObjects) { + await deleteObject(obj.object_info.object_name, tmpAccount); + } + } + deleteInRow(); + // deleteObjects.map((obj) => { + // deleteObject(obj.object_info.object_name, tmpAccount); + // }); toast.info({ description: 'Objects deleting', icon: }); refetch(); onClose(); diff --git a/apps/dcellar-web-ui/src/pages/buckets/index.tsx b/apps/dcellar-web-ui/src/pages/buckets/index.tsx index e766f36d..ba7564b3 100644 --- a/apps/dcellar-web-ui/src/pages/buckets/index.tsx +++ b/apps/dcellar-web-ui/src/pages/buckets/index.tsx @@ -1,6 +1,11 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { BucketPage } from '@/modules/bucket'; - +import { useAppDispatch } from '@/store'; +import { setSelectedRowKeys } from '@/store/slices/object'; export default function Bucket() { + const dispatch = useAppDispatch(); + useEffect(() => { + dispatch(setSelectedRowKeys([])); + }, [dispatch]); return ; } From 8e43e6d9679a15ee0fd9aad38961a88d66d01299 Mon Sep 17 00:00:00 2001 From: "miya@nodereal" Date: Tue, 1 Aug 2023 12:59:10 +0800 Subject: [PATCH 07/20] fix: batch delete --- common/config/rush/pnpm-lock.yaml | 242 ++++++++++++------------------ 1 file changed, 98 insertions(+), 144 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index e720d1c1..09023562 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -61,43 +61,42 @@ importers: typescript: 5.0.4 wagmi: ^0.12.9 dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.22.9 '@bnb-chain/greenfield-chain-sdk': 0.2.2-alpha.15 '@bnb-chain/greenfield-cosmos-types': 0.4.0-alpha.18 '@emotion/react': 11.11.1_627697682086d325a0e273fee4549116 '@emotion/styled': 11.11.0_1e8dacba4d8e6343e430b8486686f015 '@next/bundle-analyzer': 13.4.12 - '@reduxjs/toolkit': 1.9.5_react-redux@8.1.1+react@18.2.0 + '@reduxjs/toolkit': 1.9.5_react-redux@8.1.2+react@18.2.0 '@sentry/nextjs': 7.52.1_next@13.3.4+react@18.2.0 - '@tanstack/react-table': 8.9.2_react-dom@18.2.0+react@18.2.0 + '@tanstack/react-table': 8.9.3_react-dom@18.2.0+react@18.2.0 '@tanstack/react-virtual': 3.0.0-alpha.0_react@18.2.0 - '@totejs/icons': 2.13.0_aa3274991927adc2766d9259998fdd18 - '@totejs/uikit': 2.49.1_aa3274991927adc2766d9259998fdd18 + '@totejs/icons': 2.15.0_aa3274991927adc2766d9259998fdd18 '@totejs/uikit': 2.49.1_aa3274991927adc2766d9259998fdd18 ahooks: 3.7.7_react@18.2.0 antd: 5.6.3_react-dom@18.2.0+react@18.2.0 apollo-node-client: 1.4.3 axios: 1.4.0 - axios-retry: 3.5.1 + axios-retry: 3.6.0 bignumber.js: 9.1.1 comlink: 4.4.1 - dayjs: 1.11.8 + dayjs: 1.11.9 eslint-config-next: 13.3.4_eslint@8.39.0+typescript@5.0.4 ethers: 5.7.2 hash-wasm: 4.9.0 lodash-es: 4.17.21 long: 5.2.3 - next: 13.3.4_707e2fb8cf1226853cfc0154ceb98fa9 - next-redux-wrapper: 8.1.0_4c4b79cf759926e5fa0d169c6be1cf7a + next: 13.3.4_293636dd5805fb635204b0a21172c2c8 + next-redux-wrapper: 8.1.0_577768ed59b92efaefa9152a8510b434 query-string: 8.1.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-hook-form: 7.45.2_react@18.2.0 - react-redux: 8.1.1_8e70b46749da2f914bfa002f2c2020cc + react-redux: 8.1.2_8e70b46749da2f914bfa002f2c2020cc react-use: 17.4.0_react-dom@18.2.0+react@18.2.0 redux-persist: 6.0.0 typescript: 5.0.4 - wagmi: 0.12.19_d42511e4b2c276371e4a5b1d9ba84f65 + wagmi: 0.12.19_66ef97441d5706a665436ba432bb4d01 devDependencies: '@babel/plugin-syntax-flow': 7.22.5_@babel+core@7.22.9 '@babel/plugin-transform-react-jsx': 7.22.5_@babel+core@7.22.9 @@ -137,8 +136,8 @@ packages: '@ctrl/tinycolor': 3.6.0 dev: false - /@ant-design/cssinjs/1.16.0_react-dom@18.2.0+react@18.2.0: - resolution: {integrity: sha512-T+6btUa3VoiAM1TORZmY4KxsG/L6foUNN22GmSKTt4Jcy/ydxSCTE91Kkz8bWPDBTBZ0sIvJQ+F9EkFM6TInQw==} + /@ant-design/cssinjs/1.16.1_react-dom@18.2.0+react@18.2.0: + resolution: {integrity: sha512-KKVB5Or6BDC1Bo3Y4KMlOkyQU0P+6GTodubrQ9YfrtXG1TgO4wpaEfg9I4ZA49R7M+Ij2KKNwb+5abvmXy6K8w==} peerDependencies: react: '>=16.0.0' react-dom: '>=16.0.0' @@ -265,7 +264,7 @@ packages: '@babel/compat-data': 7.22.9 '@babel/core': 7.22.9 '@babel/helper-validator-option': 7.22.5 - browserslist: 4.21.9 + browserslist: 4.21.10 lru-cache: 5.1.1 semver: 6.3.1 @@ -1583,8 +1582,6 @@ packages: /@bnb-chain/greenfield-chain-sdk/0.2.2-alpha.15: resolution: {integrity: sha512-KiIbUd1FhrrzEBtvU719BsGdONgR2vQQv2oUqHEjA7XlZ7JEdGKPoti/qnwkA1KFJjmX0wJZ8FEpGNjcbPyyNQ==} - /@bnb-chain/greenfield-chain-sdk/0.0.0-snapshot-20230725025153: - resolution: {integrity: sha512-qw2rX5bhrbqlAVgcvGA3hoC7oA6oM38bT+lVsa91tAnWhNgFiJSk1sPzfxme+LRBfwkYFIasdw0ILx7VI+uG2A==} engines: {npm: please use pnpm, yarn: please use pnpm} dependencies: '@bnb-chain/greenfield-cosmos-types': 0.4.0-alpha.18 @@ -1597,10 +1594,10 @@ packages: '@ethersproject/strings': 5.7.0 '@ethersproject/units': 5.7.0 '@metamask/eth-sig-util': 5.1.0 - cross-fetch: 3.1.6 - dayjs: 1.11.8 + cross-fetch: 3.1.8 + dayjs: 1.11.9 dotenv: 16.3.1 - ethereum-cryptography: 2.0.0 + ethereum-cryptography: 2.1.2 lodash.mapvalues: 4.6.0 lodash.sortby: 4.7.0 long: 5.2.3 @@ -1630,7 +1627,7 @@ packages: engines: {node: '>= 10.0.0'} dependencies: '@metamask/safe-event-emitter': 2.0.0 - '@solana/web3.js': 1.78.0 + '@solana/web3.js': 1.78.2 bind-decorator: 1.0.11 bn.js: 5.2.1 buffer: 6.0.3 @@ -2087,7 +2084,7 @@ packages: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: eslint: 8.39.0 - eslint-visitor-keys: 3.4.1 + eslint-visitor-keys: 3.4.2 dev: true /@eslint-community/regexpp/4.6.2: @@ -2095,8 +2092,8 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint/eslintrc/2.1.0: - resolution: {integrity: sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==} + /@eslint/eslintrc/2.1.1: + resolution: {integrity: sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 @@ -2906,15 +2903,15 @@ packages: dependencies: '@babel/runtime': 7.22.6 '@rc-component/portal': 1.1.2_react-dom@18.2.0+react@18.2.0 - '@rc-component/trigger': 1.15.0_react-dom@18.2.0+react@18.2.0 + '@rc-component/trigger': 1.15.1_react-dom@18.2.0+react@18.2.0 classnames: 2.3.2 rc-util: 5.35.0_react-dom@18.2.0+react@18.2.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 dev: false - /@rc-component/trigger/1.15.0_react-dom@18.2.0+react@18.2.0: - resolution: {integrity: sha512-s9mDnu3/2WB8nMSdop/3Jxfwtk7iXvWaFbvpN7NrkmiBA2BMwI7IwWauWbOHhUKLf0QGM6SkRwgm/0MPuU/pew==} + /@rc-component/trigger/1.15.1_react-dom@18.2.0+react@18.2.0: + resolution: {integrity: sha512-U1F9WsIMLXB2JLjLSEa6uWifmTX2vxQ1r0RQCLnor8d/83e3U7TuclNbcWcM/eGcgrT2YUZid3TLDDKbDOHmLg==} engines: {node: '>=8.x'} peerDependencies: react: '>=16.9.0' @@ -2935,7 +2932,7 @@ packages: resolution: {integrity: sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==} dev: false - /@reduxjs/toolkit/1.9.5_react-redux@8.1.1+react@18.2.0: + /@reduxjs/toolkit/1.9.5_react-redux@8.1.2+react@18.2.0: resolution: {integrity: sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 @@ -2948,7 +2945,7 @@ packages: dependencies: immer: 9.0.21 react: 18.2.0 - react-redux: 8.1.1_8e70b46749da2f914bfa002f2c2020cc + react-redux: 8.1.2_8e70b46749da2f914bfa002f2c2020cc redux: 4.2.1 redux-thunk: 2.4.2_redux@4.2.1 reselect: 4.1.8 @@ -3004,7 +3001,7 @@ packages: /@safe-global/safe-apps-sdk/7.11.0: resolution: {integrity: sha512-RDamzPM1Lhhiiz0O+Dn6FkFqIh47jmZX+HCV/BBnBBOSKfBJE//IGD3+02zMgojXHTikQAburdPes9qmH1SA1A==} dependencies: - '@safe-global/safe-gateway-typescript-sdk': 3.7.3 + '@safe-global/safe-gateway-typescript-sdk': 3.8.0 ethers: 5.7.2 transitivePeerDependencies: - bufferutil @@ -3015,7 +3012,7 @@ packages: /@safe-global/safe-apps-sdk/7.9.0: resolution: {integrity: sha512-S2EI+JL8ocSgE3uGNaDZCzKmwfhtxXZFDUP76vN0FeaY35itFMyi8F0Vhxu0XnZm3yLzJE3tp5px6GhuQFLU6w==} dependencies: - '@safe-global/safe-gateway-typescript-sdk': 3.7.3 + '@safe-global/safe-gateway-typescript-sdk': 3.8.0 ethers: 5.7.2 transitivePeerDependencies: - bufferutil @@ -3023,8 +3020,8 @@ packages: - utf-8-validate dev: false - /@safe-global/safe-gateway-typescript-sdk/3.7.3: - resolution: {integrity: sha512-O6JCgXNZWG0Vv8FnOEjKfcbsP0WxGvoPJk5ufqUrsyBlHup16It6oaLnn+25nXFLBZOHI1bz8429JlqAc2t2hg==} + /@safe-global/safe-gateway-typescript-sdk/3.8.0: + resolution: {integrity: sha512-CiGWIHgIaOdICpDxp05Jw3OPslWTu8AnL0PhrCT1xZgIO86NlMMLzkGbeycJ4FHpTjA999O791Oxp4bZPIjgHA==} dependencies: cross-fetch: 3.1.8 transitivePeerDependencies: @@ -3128,7 +3125,7 @@ packages: '@sentry/utils': 7.52.1 '@sentry/webpack-plugin': 1.20.0 chalk: 3.0.0 - next: 13.3.4_707e2fb8cf1226853cfc0154ceb98fa9 + next: 13.3.4_293636dd5805fb635204b0a21172c2c8 react: 18.2.0 rollup: 2.78.0 stacktrace-parser: 0.1.10 @@ -3208,8 +3205,8 @@ packages: buffer: 6.0.3 dev: false - /@solana/web3.js/1.78.0: - resolution: {integrity: sha512-CSjCjo+RELJ5puoZALfznN5EF0YvL1V8NQrQYovsdjE1lCV6SqbKAIZD0+9LlqCBoa1ibuUaR7G2SooYzvzmug==} + /@solana/web3.js/1.78.2: + resolution: {integrity: sha512-oF+TmBZCt3eAEl4Meu3GO2p6G8wdyoKgXgTKzQpIUIhpMGA/dVQzyMFpKjCgoTU1Kx+/UF3gXUdsZOxQukGbvQ==} dependencies: '@babel/runtime': 7.22.6 '@noble/curves': 1.1.0 @@ -3622,36 +3619,6 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /@totejs/icons/2.14.0_aa3274991927adc2766d9259998fdd18: - resolution: {integrity: sha512-k2rr3SJD4zqjjMYIroNvdSxoJyqlq8TajFiV3llQXEDXKaZEiC8oShmIUcdfSm+4jVUa5mAob2sTptNKQqZ0og==} - peerDependencies: - '@emotion/react': '>=11' - '@emotion/styled': '>=11' - react: '>=16.9.0' - react-dom: '>=16.9.0' - dependencies: - '@emotion/react': 11.11.1_627697682086d325a0e273fee4549116 - '@emotion/styled': 11.11.0_1e8dacba4d8e6343e430b8486686f015 - '@totejs/styled-system': 2.12.0_react-dom@18.2.0+react@18.2.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: false - - /@totejs/icons/2.14.0_aa3274991927adc2766d9259998fdd18: - resolution: {integrity: sha512-k2rr3SJD4zqjjMYIroNvdSxoJyqlq8TajFiV3llQXEDXKaZEiC8oShmIUcdfSm+4jVUa5mAob2sTptNKQqZ0og==} - peerDependencies: - '@emotion/react': '>=11' - '@emotion/styled': '>=11' - react: '>=16.9.0' - react-dom: '>=16.9.0' - dependencies: - '@emotion/react': 11.11.1_627697682086d325a0e273fee4549116 - '@emotion/styled': 11.11.0_1e8dacba4d8e6343e430b8486686f015 - '@totejs/styled-system': 2.12.0_react-dom@18.2.0+react@18.2.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: false - /@totejs/prettier-config/0.1.0: resolution: {integrity: sha512-N7ayi2uD5BUV44XDNHqHPQ3kWkCa73gTTLRDX0Doz42iSVszTne2ZtFppGIx/FDXwJfehnJiyaM1ZOrUzgn7QQ==} dependencies: @@ -3671,8 +3638,6 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /@totejs/uikit/2.49.1_aa3274991927adc2766d9259998fdd18: - resolution: {integrity: sha512-ghOG/69Hjx0HjZSrBmK+N3VJAkqml9Ij9d6K3+mjRFOf3vyBsXElA7FzvHUEwo6PpbFFAzpw4yiouSJqhsWYPA==} /@totejs/uikit/2.49.1_aa3274991927adc2766d9259998fdd18: resolution: {integrity: sha512-ghOG/69Hjx0HjZSrBmK+N3VJAkqml9Ij9d6K3+mjRFOf3vyBsXElA7FzvHUEwo6PpbFFAzpw4yiouSJqhsWYPA==} peerDependencies: @@ -3685,7 +3650,6 @@ packages: '@emotion/styled': 11.11.0_1e8dacba4d8e6343e430b8486686f015 '@popperjs/core': 2.11.8 '@totejs/icons': 2.14.0_aa3274991927adc2766d9259998fdd18 - '@totejs/icons': 2.14.0_aa3274991927adc2766d9259998fdd18 '@totejs/styled-system': 2.12.0_react-dom@18.2.0+react@18.2.0 '@xobotyi/scrollbar-width': 1.9.5 react: 18.2.0 @@ -4008,7 +3972,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: '@typescript-eslint/types': 5.62.0 - eslint-visitor-keys: 3.4.1 + eslint-visitor-keys: 3.4.2 /@wagmi/chains/0.2.22_typescript@5.0.4: resolution: {integrity: sha512-TdiOzJT6TO1JrztRNjTA5Quz+UmQlbvWFG8N41u9tta0boHA1JCAzGGvU6KuIcOmJfRJkKOUIt67wlbopCpVHg==} @@ -4037,7 +4001,7 @@ packages: '@ledgerhq/connect-kit-loader': 1.1.0 '@safe-global/safe-apps-provider': 0.15.2 '@safe-global/safe-apps-sdk': 7.11.0 - '@wagmi/core': 0.10.17_ffa9e4b7c978105c4cdc523f3dd0562c + '@wagmi/core': 0.10.17_01c1674540fb06277196a21a8a2e0b3e '@walletconnect/ethereum-provider': 2.9.0_@walletconnect+modal@2.6.1 '@walletconnect/legacy-provider': 2.0.0 '@walletconnect/modal': 2.6.1_react@18.2.0 @@ -4056,7 +4020,7 @@ packages: - zod dev: false - /@wagmi/core/0.10.17_ffa9e4b7c978105c4cdc523f3dd0562c: + /@wagmi/core/0.10.17_01c1674540fb06277196a21a8a2e0b3e: resolution: {integrity: sha512-qud45y3IlHp7gYWzoFeyysmhyokRie59Xa5tcx5F1E/v4moD5BY0kzD26mZW/ZQ3WZuVK/lZwiiPRqpqWH52Gw==} peerDependencies: ethers: '>=5.5.1 <6' @@ -4071,9 +4035,10 @@ packages: ethers: 5.7.2 eventemitter3: 4.0.7 typescript: 5.0.4 - zustand: 4.3.9_react@18.2.0 + zustand: 4.4.0_627697682086d325a0e273fee4549116 transitivePeerDependencies: - '@react-native-async-storage/async-storage' + - '@types/react' - bufferutil - encoding - immer @@ -4635,7 +4600,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@ant-design/colors': 7.0.0 - '@ant-design/cssinjs': 1.16.0_react-dom@18.2.0+react@18.2.0 + '@ant-design/cssinjs': 1.16.1_react-dom@18.2.0+react@18.2.0 '@ant-design/icons': 5.1.4_react-dom@18.2.0+react@18.2.0 '@ant-design/react-slick': 1.0.2_react@18.2.0 '@babel/runtime': 7.22.6 @@ -4643,14 +4608,14 @@ packages: '@rc-component/color-picker': 1.2.0_react-dom@18.2.0+react@18.2.0 '@rc-component/mutate-observer': 1.0.0_react-dom@18.2.0+react@18.2.0 '@rc-component/tour': 1.8.1_react-dom@18.2.0+react@18.2.0 - '@rc-component/trigger': 1.15.0_react-dom@18.2.0+react@18.2.0 + '@rc-component/trigger': 1.15.1_react-dom@18.2.0+react@18.2.0 classnames: 2.3.2 copy-to-clipboard: 3.3.3 dayjs: 1.11.9 qrcode.react: 3.1.0_react@18.2.0 rc-cascader: 3.12.1_react-dom@18.2.0+react@18.2.0 rc-checkbox: 3.1.0_react-dom@18.2.0+react@18.2.0 - rc-collapse: 3.7.0_react-dom@18.2.0+react@18.2.0 + rc-collapse: 3.7.1_react-dom@18.2.0+react@18.2.0 rc-dialog: 9.1.0_react-dom@18.2.0+react@18.2.0 rc-drawer: 6.2.0_react-dom@18.2.0+react@18.2.0 rc-dropdown: 4.1.0_react-dom@18.2.0+react@18.2.0 @@ -4833,8 +4798,8 @@ packages: resolution: {integrity: sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==} engines: {node: '>=4'} - /axios-retry/3.5.1: - resolution: {integrity: sha512-mQRJ4IyAUnYig14BQ4MnnNHHuH1cNH7NW4JxEUD6mNJwK6pwOY66wKLCwZ6Y0o3POpfStalqRC+J4+Hnn6Om7w==} + /axios-retry/3.6.0: + resolution: {integrity: sha512-jtH4qWTKZ2a17dH6tjq52Y1ssNV0lKge6/Z9Lw67s9Wt01nGTg4hg7/LJBGYfDci44NTANJQlCPHPOT/TSFm9w==} dependencies: '@babel/runtime': 7.22.6 is-retry-allowed: 2.2.0 @@ -5031,15 +4996,15 @@ packages: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} dev: false - /browserslist/4.21.9: - resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==} + /browserslist/4.21.10: + resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001517 - electron-to-chromium: 1.4.475 + caniuse-lite: 1.0.30001518 + electron-to-chromium: 1.4.478 node-releases: 2.0.13 - update-browserslist-db: 1.0.11_browserslist@4.21.9 + update-browserslist-db: 1.0.11_browserslist@4.21.10 /bs58/4.0.1: resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} @@ -5115,8 +5080,8 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite/1.0.30001517: - resolution: {integrity: sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==} + /caniuse-lite/1.0.30001518: + resolution: {integrity: sha512-rup09/e3I0BKjncL+FesTayKtPrdwKhUufQFd3riFw1hHg8JmIFoInYfB102cFcY/pPgGmdyl/iy+jgiDi2vdA==} /chalk/1.1.3: resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} @@ -5342,7 +5307,7 @@ packages: /core-js-compat/3.32.0: resolution: {integrity: sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==} dependencies: - browserslist: 4.21.9 + browserslist: 4.21.10 dev: true /core-js/3.32.0: @@ -5404,22 +5369,6 @@ packages: - encoding dev: false - /cross-fetch/4.0.0: - resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} - dependencies: - node-fetch: 2.6.12 - transitivePeerDependencies: - - encoding - dev: false - - /cross-fetch/4.0.0: - resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} - dependencies: - node-fetch: 2.6.12 - transitivePeerDependencies: - - encoding - dev: false - /cross-spawn/7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -5670,8 +5619,8 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true - /electron-to-chromium/1.4.475: - resolution: {integrity: sha512-mTye5u5P98kSJO2n7zYALhpJDmoSQejIGya0iR01GpoRady8eK3bw7YHHnjA1Rfi4ZSLdpuzlAC7Zw+1Zu7Z6A==} + /electron-to-chromium/1.4.478: + resolution: {integrity: sha512-qjTA8djMXd+ruoODDFGnRCRBpID+AAfYWCyGtYTNhsuwxI19s8q19gbjKTwRS5z/LyVf5wICaIiPQGLekmbJbA==} /elliptic/6.5.4: resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} @@ -5830,14 +5779,14 @@ packages: optional: true dependencies: '@next/eslint-plugin-next': 13.3.4 - '@rushstack/eslint-patch': 1.3.1 - '@typescript-eslint/parser': 5.59.11_eslint@8.39.0+typescript@5.0.4 + '@rushstack/eslint-patch': 1.3.2 + '@typescript-eslint/parser': 5.62.0_eslint@8.39.0+typescript@5.0.4 eslint: 8.39.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-typescript: 3.5.5_88a97c99d7215110fff572394f7f7f35 eslint-plugin-import: 2.28.0_eslint@8.39.0 eslint-plugin-jsx-a11y: 6.7.1_eslint@8.39.0 - eslint-plugin-react: 7.33.0_eslint@8.39.0 + eslint-plugin-react: 7.33.1_eslint@8.39.0 eslint-plugin-react-hooks: 4.6.0_eslint@8.39.0 typescript: 5.0.4 transitivePeerDependencies: @@ -5862,7 +5811,7 @@ packages: eslint-plugin-import: 2.28.0_eslint@8.39.0 eslint-plugin-jest: 25.7.0_9cbd9d3f657acde097221c50b7cddab0 eslint-plugin-jsx-a11y: 6.7.1_eslint@8.39.0 - eslint-plugin-react: 7.33.0_eslint@8.39.0 + eslint-plugin-react: 7.33.1_eslint@8.39.0 eslint-plugin-react-hooks: 4.6.0_eslint@8.39.0 eslint-plugin-testing-library: 5.11.0_eslint@8.39.0+typescript@5.0.4 transitivePeerDependencies: @@ -5992,7 +5941,7 @@ packages: emoji-regex: 9.2.2 eslint: 8.39.0 has: 1.0.3 - jsx-ast-utils: 3.3.4 + jsx-ast-utils: 3.3.5 language-tags: 1.0.5 minimatch: 3.1.2 object.entries: 1.1.6 @@ -6007,8 +5956,8 @@ packages: dependencies: eslint: 8.39.0 - /eslint-plugin-react/7.33.0_eslint@8.39.0: - resolution: {integrity: sha512-qewL/8P34WkY8jAqdQxsiL82pDUeT7nhs8IsuXgfgnsEloKCT4miAV9N9kGtx7/KM9NH/NCGUE7Edt9iGxLXFw==} + /eslint-plugin-react/7.33.1_eslint@8.39.0: + resolution: {integrity: sha512-L093k0WAMvr6VhNwReB8VgOq5s2LesZmrpPdKz/kZElQDzqS7G7+DnKoqT+w4JwuiGeAhAvHO0fvy0Eyk4ejDA==} engines: {node: '>=4'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 @@ -6019,7 +5968,7 @@ packages: doctrine: 2.1.0 eslint: 8.39.0 estraverse: 5.3.0 - jsx-ast-utils: 3.3.4 + jsx-ast-utils: 3.3.5 minimatch: 3.1.2 object.entries: 1.1.6 object.fromentries: 2.0.6 @@ -6051,8 +6000,8 @@ packages: estraverse: 4.3.0 dev: true - /eslint-scope/7.2.1: - resolution: {integrity: sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==} + /eslint-scope/7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: esrecurse: 4.3.0 @@ -6064,8 +6013,8 @@ packages: engines: {node: '>=10'} dev: true - /eslint-visitor-keys/3.4.1: - resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} + /eslint-visitor-keys/3.4.2: + resolution: {integrity: sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} /eslint/8.39.0: @@ -6075,7 +6024,7 @@ packages: dependencies: '@eslint-community/eslint-utils': 4.4.0_eslint@8.39.0 '@eslint-community/regexpp': 4.6.2 - '@eslint/eslintrc': 2.1.0 + '@eslint/eslintrc': 2.1.1 '@eslint/js': 8.39.0 '@humanwhocodes/config-array': 0.11.10 '@humanwhocodes/module-importer': 1.0.1 @@ -6086,8 +6035,8 @@ packages: debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.1 - eslint-visitor-keys: 3.4.1 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.2 espree: 9.6.1 esquery: 1.5.0 esutils: 2.0.3 @@ -6123,7 +6072,7 @@ packages: dependencies: acorn: 8.10.0 acorn-jsx: 5.3.2_acorn@8.10.0 - eslint-visitor-keys: 3.4.1 + eslint-visitor-keys: 3.4.2 dev: true /esquery/1.5.0: @@ -7145,8 +7094,8 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} - /jsx-ast-utils/3.3.4: - resolution: {integrity: sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==} + /jsx-ast-utils/3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} dependencies: array-includes: 3.1.6 @@ -7611,19 +7560,19 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true - /next-redux-wrapper/8.1.0_4c4b79cf759926e5fa0d169c6be1cf7a: + /next-redux-wrapper/8.1.0_577768ed59b92efaefa9152a8510b434: resolution: {integrity: sha512-2hIau0hcI6uQszOtrvAFqgc0NkZegKYhBB7ZAKiG3jk7zfuQb4E7OV9jfxViqqojh3SEHdnFfPkN9KErttUKuw==} peerDependencies: next: '>=9' react: '*' react-redux: '*' dependencies: - next: 13.3.4_707e2fb8cf1226853cfc0154ceb98fa9 + next: 13.3.4_293636dd5805fb635204b0a21172c2c8 react: 18.2.0 - react-redux: 8.1.1_8e70b46749da2f914bfa002f2c2020cc + react-redux: 8.1.2_8e70b46749da2f914bfa002f2c2020cc dev: false - /next/13.3.4_707e2fb8cf1226853cfc0154ceb98fa9: + /next/13.3.4_293636dd5805fb635204b0a21172c2c8: resolution: {integrity: sha512-sod7HeokBSvH5QV0KB+pXeLfcXUlLrGnVUXxHpmhilQ+nQYT3Im2O8DswD5e4uqbR8Pvdu9pcWgb1CbXZQZlmQ==} engines: {node: '>=16.8.0'} hasBin: true @@ -7647,7 +7596,7 @@ packages: '@next/env': 13.3.4 '@swc/helpers': 0.5.1 busboy: 1.6.0 - caniuse-lite: 1.0.30001517 + caniuse-lite: 1.0.30001518 postcss: 8.4.14 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 @@ -8243,8 +8192,8 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /rc-collapse/3.7.0_react-dom@18.2.0+react@18.2.0: - resolution: {integrity: sha512-Cir1c89cENiK5wryd9ut+XltrIfx/+KH1/63uJIVjuXkgfrIvIy6W1fYGgEYtttbHW2fEfxg1s31W+Vm98fSRw==} + /rc-collapse/3.7.1_react-dom@18.2.0+react@18.2.0: + resolution: {integrity: sha512-N/7ejyiTf3XElNJBBpxqnZBUuMsQWEOPjB2QkfNvZ/Ca54eAvJXuOD1EGbCWCk2m7v/MSxku7mRpdeaLOCd4Gg==} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' @@ -8294,7 +8243,7 @@ packages: react-dom: '>=16.11.0' dependencies: '@babel/runtime': 7.22.6 - '@rc-component/trigger': 1.15.0_react-dom@18.2.0+react@18.2.0 + '@rc-component/trigger': 1.15.1_react-dom@18.2.0+react@18.2.0 classnames: 2.3.2 rc-util: 5.35.0_react-dom@18.2.0+react@18.2.0 react: 18.2.0 @@ -8365,7 +8314,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.22.6 - '@rc-component/trigger': 1.15.0_react-dom@18.2.0+react@18.2.0 + '@rc-component/trigger': 1.15.1_react-dom@18.2.0+react@18.2.0 classnames: 2.3.2 rc-input: 1.0.4_react-dom@18.2.0+react@18.2.0 rc-menu: 9.9.2_react-dom@18.2.0+react@18.2.0 @@ -8382,7 +8331,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.22.6 - '@rc-component/trigger': 1.15.0_react-dom@18.2.0+react@18.2.0 + '@rc-component/trigger': 1.15.1_react-dom@18.2.0+react@18.2.0 classnames: 2.3.2 rc-motion: 2.7.3_react-dom@18.2.0+react@18.2.0 rc-overflow: 1.3.1_react-dom@18.2.0+react@18.2.0 @@ -8467,7 +8416,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.22.6 - '@rc-component/trigger': 1.15.0_react-dom@18.2.0+react@18.2.0 + '@rc-component/trigger': 1.15.1_react-dom@18.2.0+react@18.2.0 classnames: 2.3.2 dayjs: 1.11.9 rc-util: 5.35.0_react-dom@18.2.0+react@18.2.0 @@ -8538,7 +8487,7 @@ packages: react-dom: '*' dependencies: '@babel/runtime': 7.22.6 - '@rc-component/trigger': 1.15.0_react-dom@18.2.0+react@18.2.0 + '@rc-component/trigger': 1.15.1_react-dom@18.2.0+react@18.2.0 classnames: 2.3.2 rc-motion: 2.7.3_react-dom@18.2.0+react@18.2.0 rc-overflow: 1.3.1_react-dom@18.2.0+react@18.2.0 @@ -8645,7 +8594,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.22.6 - '@rc-component/trigger': 1.15.0_react-dom@18.2.0+react@18.2.0 + '@rc-component/trigger': 1.15.1_react-dom@18.2.0+react@18.2.0 classnames: 2.3.2 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 @@ -8752,8 +8701,8 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: false - /react-redux/8.1.1_8e70b46749da2f914bfa002f2c2020cc: - resolution: {integrity: sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==} + /react-redux/8.1.2_8e70b46749da2f914bfa002f2c2020cc: + resolution: {integrity: sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==} peerDependencies: '@types/react': ^16.8 || ^17.0 || ^18.0 '@types/react-dom': ^16.8 || ^17.0 || ^18.0 @@ -9893,13 +9842,13 @@ packages: engines: {node: '>=8'} dev: false - /update-browserslist-db/1.0.11_browserslist@4.21.9: + /update-browserslist-db/1.0.11_browserslist@4.21.10: resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.21.9 + browserslist: 4.21.10 escalade: 3.1.1 picocolors: 1.0.0 @@ -9977,8 +9926,8 @@ packages: dependencies: debug: 4.3.4 eslint: 8.39.0 - eslint-scope: 7.2.1 - eslint-visitor-keys: 3.4.1 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.2 espree: 9.6.1 esquery: 1.5.0 lodash: 4.17.21 @@ -9987,7 +9936,7 @@ packages: - supports-color dev: true - /wagmi/0.12.19_d42511e4b2c276371e4a5b1d9ba84f65: + /wagmi/0.12.19_66ef97441d5706a665436ba432bb4d01: resolution: {integrity: sha512-S/el9BDb/HNeQWh1v8TvntMPX/CgKLDAoJqDb8i7jifLfWPqFL7gor3vnI1Vs6ZlB8uh7m+K1Qyg+mKhbITuDQ==} peerDependencies: ethers: '>=5.5.1 <6' @@ -10000,7 +9949,7 @@ packages: '@tanstack/query-sync-storage-persister': 4.32.0 '@tanstack/react-query': 4.32.0_react-dom@18.2.0+react@18.2.0 '@tanstack/react-query-persist-client': 4.32.0_@tanstack+react-query@4.32.0 - '@wagmi/core': 0.10.17_ffa9e4b7c978105c4cdc523f3dd0562c + '@wagmi/core': 0.10.17_01c1674540fb06277196a21a8a2e0b3e abitype: 0.3.0_typescript@5.0.4 ethers: 5.7.2 react: 18.2.0 @@ -10008,6 +9957,7 @@ packages: use-sync-external-store: 1.2.0_react@18.2.0 transitivePeerDependencies: - '@react-native-async-storage/async-storage' + - '@types/react' - bufferutil - encoding - immer @@ -10269,18 +10219,22 @@ packages: engines: {node: '>=10'} dev: true - /zustand/4.3.9_react@18.2.0: - resolution: {integrity: sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==} + /zustand/4.4.0_627697682086d325a0e273fee4549116: + resolution: {integrity: sha512-2dq6wq4dSxbiPTamGar0NlIG/av0wpyWZJGeQYtUOLegIUvhM2Bf86ekPlmgpUtS5uR7HyetSiktYrGsdsyZgQ==} engines: {node: '>=12.7.0'} peerDependencies: + '@types/react': '>=16.8' immer: '>=9.0' react: '>=16.8' peerDependenciesMeta: + '@types/react': + optional: true immer: optional: true react: optional: true dependencies: + '@types/react': 18.0.38 react: 18.2.0 use-sync-external-store: 1.2.0_react@18.2.0 dev: false From f4c18316d3a656084b72dca733bcbd96fdc47f93 Mon Sep 17 00:00:00 2001 From: aidencao Date: Fri, 4 Aug 2023 16:38:26 +0800 Subject: [PATCH 08/20] feat(dcellar-web-ui): add multi download --- apps/dcellar-web-ui/src/facade/object.ts | 55 +++++++++--------- .../file/utils/generateGetObjectOptions.ts | 7 +-- .../src/modules/file/utils/index.tsx | 13 ++++- .../object/components/BatchOperations.tsx | 22 +++++-- .../modules/object/components/ObjectList.tsx | 10 +++- .../modules/object/components/ShareObject.tsx | 7 +-- .../object/components/SharePermission.tsx | 6 +- .../src/modules/object/index.tsx | 14 +++-- .../src/modules/share/SharedFile.tsx | 2 +- .../src/modules/upload/UploadObjects.tsx | 45 +++++++++------ .../dcellar-web-ui/src/store/slices/object.ts | 57 ++++++++++--------- apps/dcellar-web-ui/src/utils/string.ts | 29 ++++++++++ 12 files changed, 168 insertions(+), 99 deletions(-) diff --git a/apps/dcellar-web-ui/src/facade/object.ts b/apps/dcellar-web-ui/src/facade/object.ts index fb36da15..dec869be 100644 --- a/apps/dcellar-web-ui/src/facade/object.ts +++ b/apps/dcellar-web-ui/src/facade/object.ts @@ -10,7 +10,6 @@ import { import { broadcastFault, commonFault, - downloadPreviewFault, E_NO_QUOTA, E_NOT_FOUND, E_PERMISSION_DENIED, @@ -32,19 +31,16 @@ import { VisibilityType } from '@bnb-chain/greenfield-cosmos-types/greenfield/st import { quotaRemains } from '@/facade/bucket'; import { ObjectInfo } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/types'; import { encodeObjectName } from '@/utils/string'; -import { - downloadWithProgress, - saveFileByAxiosResponse, - viewFileByAxiosResponse, -} from '@/modules/file/utils'; -import axios, { AxiosResponse } from 'axios'; +import axios from 'axios'; import { SpItem } from '@/store/slices/sp'; import { QueryHeadObjectResponse, QueryLockFeeRequest, } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/query'; import { signTypedDataV4 } from '@/utils/signDataV4'; -import BigNumber from 'bignumber.js'; +import { getDomain } from '@/utils/getDomain'; +import { generateGetObjectOptions } from '@/modules/file/utils/generateGetObjectOptions'; +import { batchDownload, directlyDownload } from '@/modules/file/utils'; export type DeliverResponse = Awaited>; @@ -101,29 +97,30 @@ export const getCanObjectAccess = async ( }; export type DownloadPreviewParams = { - objectInfo: ObjectInfo; + objectInfo: { bucketName: string; objectName: string; visibility: number }; primarySp: SpItem; address: string; }; -const getObjectBytes = async ( +export const getAuthorizedLink = async ( params: DownloadPreviewParams, seedString: string, -): Promise<[AxiosResponse | null, ErrorMsg?]> => { + view = 1, +): Promise<[null, ErrorMsg] | [string]> => { const { address, primarySp, objectInfo } = params; - const { bucketName, objectName, payloadSize } = objectInfo; - - const [result, error] = await downloadWithProgress({ + const { bucketName, objectName } = objectInfo; + const domain = getDomain(); + const [options, error] = await generateGetObjectOptions({ bucketName, objectName, - primarySp, - payloadSize: payloadSize.toNumber(), - address, + endpoint: primarySp.endpoint, + userAddress: address, + domain, seedString, - }).then(resolve, downloadPreviewFault); - if (!result) return [null, error]; - - return [result]; + }).then(resolve, commonFault); + if (error) return [null, error]; + const { url, params: _params } = options!; + return [`${url}?${_params}&view=${view}`]; }; export const downloadObject = async ( @@ -137,14 +134,14 @@ export const downloadObject = async ( const isPrivate = visibility === VisibilityType.VISIBILITY_TYPE_PRIVATE; const link = `${endpoint}/download/${bucketName}/${encodeObjectName(objectName)}`; if (!isPrivate) { - window.location.href = link; + batchDownload(link); return [true]; } - const [result, error] = await getObjectBytes(params, seedString); - if (!result) return [false, error]; + const [url, error] = await getAuthorizedLink(params, seedString, 0); + if (!url) return [false, error]; - saveFileByAxiosResponse(result, objectName); + batchDownload(url); return [true]; }; @@ -159,14 +156,14 @@ export const previewObject = async ( const isPrivate = visibility === VisibilityType.VISIBILITY_TYPE_PRIVATE; const link = `${endpoint}/view/${bucketName}/${encodeObjectName(objectName)}`; if (!isPrivate) { - window.open(link, '_blank'); + directlyDownload(link, '_blank'); return [true]; } - const [result, error] = await getObjectBytes(params, seedString); - if (!result) return [false, error]; + const [url, error] = await getAuthorizedLink(params, seedString); + if (!url) return [false, error]; - viewFileByAxiosResponse(result); + directlyDownload(url, '_blank'); return [true]; }; diff --git a/apps/dcellar-web-ui/src/modules/file/utils/generateGetObjectOptions.ts b/apps/dcellar-web-ui/src/modules/file/utils/generateGetObjectOptions.ts index 009795f4..5c4b5b9e 100644 --- a/apps/dcellar-web-ui/src/modules/file/utils/generateGetObjectOptions.ts +++ b/apps/dcellar-web-ui/src/modules/file/utils/generateGetObjectOptions.ts @@ -30,10 +30,9 @@ export const generateGetObjectOptions = async ( }); const params = new URLSearchParams(); - // params.append('authorization', body?.authorization || ''); - // params.append('user-address', userAddress); - // params.append('app-domain', domain); - // params.append('view', '1'); + params.append('authorization', body?.authorization || ''); + params.append('user-address', userAddress); + params.append('app-domain', domain); return { url, diff --git a/apps/dcellar-web-ui/src/modules/file/utils/index.tsx b/apps/dcellar-web-ui/src/modules/file/utils/index.tsx index 5a9d1391..3286d3d1 100644 --- a/apps/dcellar-web-ui/src/modules/file/utils/index.tsx +++ b/apps/dcellar-web-ui/src/modules/file/utils/index.tsx @@ -293,7 +293,7 @@ const renderInsufficientBalance = ( ); }; -const directlyDownload = (url: string, name?: string) => { +const directlyDownload = (url: string, target = '_self', name?: string) => { if (!url) { toast.error({ description: 'Download url not existed. Please check.', @@ -302,11 +302,22 @@ const directlyDownload = (url: string, name?: string) => { const link = document.createElement('a'); link.href = url; link.download = name || ''; + link.target = target; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; +export const batchDownload = (url: string | string[]) => { + const urls = Array().concat(url); + urls.forEach((url) => { + const iframe = document.createElement('iframe'); + iframe.src = url; + iframe.style.display = 'none'; + document.body.appendChild(iframe); + }); +}; + const getQuota = async ( bucketName: string, endpoint: string, diff --git a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx index 9f573f99..70debd9c 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx @@ -5,11 +5,13 @@ import { Text } from '@totejs/uikit'; import { useAppDispatch, useAppSelector } from '@/store'; import { E_NO_QUOTA, E_OFF_CHAIN_AUTH, E_UNKNOWN } from '@/facade/error'; import { OBJECT_ERROR_TYPES, ObjectErrorType } from '@/modules/object/ObjectError'; -import { setStatusDetail } from '@/store/slices/object'; +import { setSelectedRowKeys, setStatusDetail } from '@/store/slices/object'; import { useOffChainAuth } from '@/hooks/useOffChainAuth'; import { useMount } from 'ahooks'; import { setupBucketQuota } from '@/store/slices/bucket'; import { quotaRemains } from '@/facade/bucket'; +import { getSpOffChainData } from '@/store/slices/persist'; +import { downloadObject } from '@/facade/object'; interface BatchOperationsProps {} @@ -20,8 +22,10 @@ export const BatchOperations = memo(function BatchOperatio const { setOpenAuthModal } = useOffChainAuth(); const { loginAccount } = useAppSelector((root) => root.persist); const { bucketName, objects, path } = useAppSelector((root) => root.object); + const { primarySpInfo } = useAppSelector((root) => root.sp); const quotas = useAppSelector((root) => root.bucket.quotas); const quotaData = quotas[bucketName]; + const primarySp = primarySpInfo[bucketName]; useMount(() => { dispatch(setupBucketQuota(bucketName)); @@ -45,10 +49,18 @@ export const BatchOperations = memo(function BatchOperatio items.reduce((x, y) => x + y.payloadSize, 0), ); if (!remainQuota) return onError(E_NO_QUOTA); - // const operator = primarySp.operatorAddress; - // const { seedString } = await dispatch(getSpOffChainData(loginAccount, operator)); - // const domain = getDomain(); - // todo + const operator = primarySp.operatorAddress; + const { seedString } = await dispatch(getSpOffChainData(loginAccount, operator)); + + for (const item of items) { + const payload = { + primarySp, + objectInfo: item, + address: loginAccount, + }; + await downloadObject(payload, seedString); + } + dispatch(setSelectedRowKeys([])); }; return ( diff --git a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx index 87fca572..9d235f92 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx @@ -59,6 +59,8 @@ import { CreateFolder } from './CreateFolder'; import { useOffChainAuth } from '@/hooks/useOffChainAuth'; import { StyledRow } from '@/modules/object/objects.style'; import { UploadFile, selectUploadQueue } from '@/store/slices/global'; +import { copy, encodeObjectName, getShareLink } from '@/utils/string'; +import { toast } from '@totejs/uikit'; const Actions: ActionMenuItem[] = [ { label: 'View Details', value: 'detail' }, @@ -86,7 +88,7 @@ export const ObjectList = memo(function ObjectList() { ); const currentPage = useAppSelector(selectPathCurrent); const { discontinue, owner } = useAppSelector((root) => root.bucket); - const { primarySpInfo} = useAppSelector((root) => root.sp); + const { primarySpInfo } = useAppSelector((root) => root.sp); const loading = useAppSelector(selectPathLoading); const objectList = useAppSelector(selectObjectList); const { setOpenAuthModal } = useOffChainAuth(); @@ -181,7 +183,9 @@ export const ObjectList = memo(function ObjectList() { case 'delete': return dispatch(setEditDelete(record)); case 'share': - return dispatch(setEditShare(record)); + copy(getShareLink(bucketName, record.objectName)); + toast.success({ description: 'Successfully copied to your clipboard.' }); + return; case 'download': return download(record); case 'cancel': @@ -275,7 +279,7 @@ export const ObjectList = memo(function ObjectList() { // It is not allowed to cancel when the chain is sealed, but the SP is not synchronized. const file = find( uploadQueue, - (q) => [...q.prefixFolders, q.file.name].join('/') === record.objectName + (q) => [...q.prefixFolders, q.file.name].join('/') === record.objectName, ); if (file) { fitActions = fitActions.filter((a) => a.value !== 'cancel'); diff --git a/apps/dcellar-web-ui/src/modules/object/components/ShareObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/ShareObject.tsx index 28663d8c..ab1e7ff2 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/ShareObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/ShareObject.tsx @@ -16,7 +16,7 @@ import { GAClick } from '@/components/common/GATracker'; // import { AccessItem } from '@/modules/file/components/AccessItem'; import { useAppDispatch, useAppSelector } from '@/store'; import { ObjectItem, setEditShare } from '@/store/slices/object'; -import { encodeObjectName } from '@/utils/string'; +import { getShareLink } from '@/utils/string'; interface modalProps {} @@ -24,7 +24,6 @@ export const ShareObject = (props: modalProps) => { const dispatch = useAppDispatch(); const { hasCopied, onCopy, setValue } = useClipboard(''); const { editShare, bucketName } = useAppSelector((root) => root.object); - const params = [bucketName, encodeObjectName(editShare.objectName)].join('/'); const isOpen = !!editShare.objectName; const onClose = () => { dispatch(setEditShare({} as ObjectItem)); @@ -33,8 +32,8 @@ export const ShareObject = (props: modalProps) => { }; useEffect(() => { - setValue(`${location.origin}/share?file=${encodeURIComponent(params)}`); - }, [setValue, params]); + setValue(getShareLink(bucketName, editShare.objectName)); + }, [setValue, bucketName, editShare.objectName]); return ( <> diff --git a/apps/dcellar-web-ui/src/modules/object/components/SharePermission.tsx b/apps/dcellar-web-ui/src/modules/object/components/SharePermission.tsx index f8d9792a..7f46fb91 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/SharePermission.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/SharePermission.tsx @@ -30,7 +30,7 @@ import { import { useOffChainAuth } from '@/hooks/useOffChainAuth'; import { ViewerList } from '@/modules/object/components/ViewerList'; import { CopyButton } from '@/modules/object/components/CopyButton'; -import { encodeObjectName } from '@/utils/string'; +import { encodeObjectName, getShareLink } from '@/utils/string'; interface SharePermissionProps {} @@ -109,8 +109,6 @@ export const SharePermission = memo(function SharePermissi ); }; - const params = [bucketName, encodeObjectName(editDetail.objectName)].join('/'); - return ( <> (function SharePermissi /> )} - + Copy Link diff --git a/apps/dcellar-web-ui/src/modules/object/index.tsx b/apps/dcellar-web-ui/src/modules/object/index.tsx index 172a2a1c..1372e313 100644 --- a/apps/dcellar-web-ui/src/modules/object/index.tsx +++ b/apps/dcellar-web-ui/src/modules/object/index.tsx @@ -22,7 +22,7 @@ import { BatchOperations } from '@/modules/object/components/BatchOperations'; export const ObjectsPage = () => { const dispatch = useAppDispatch(); - const { allSps, primarySpInfo} = useAppSelector((root) => root.sp); + const { allSps, primarySpInfo } = useAppSelector((root) => root.sp); const { bucketInfo } = useAppSelector((root) => root.bucket); const { loginAccount } = useAppSelector((root) => root.persist); const selectedRowKeys = useAppSelector((root) => root.object.selectedRowKeys); @@ -45,11 +45,15 @@ export const ObjectsPage = () => { if (!bucket) return; const primarySp = primarySpInfo[bucketName]; if (!primarySp) { - const [data, error] = await getVirtualGroupFamily({ familyId: bucket.global_virtual_group_family_id }); - const sp = allSps.find((item) => item.id === data?.globalVirtualGroupFamily?.primarySpId) as SpItem; - dispatch(setPrimarySpInfo({ bucketName, sp})); + const [data, error] = await getVirtualGroupFamily({ + familyId: bucket.global_virtual_group_family_id, + }); + const sp = allSps.find( + (item) => item.id === data?.globalVirtualGroupFamily?.primarySpId, + ) as SpItem; + dispatch(setPrimarySpInfo({ bucketName, sp })); } - }, [bucketInfo, bucketName]) + }, [bucketInfo, bucketName]); useAsyncEffect(async () => { const bucket = bucketInfo[bucketName]; diff --git a/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx b/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx index d79813b3..39ab30a4 100644 --- a/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx +++ b/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx @@ -17,7 +17,7 @@ import { useAppDispatch, useAppSelector } from '@/store'; import { getSpOffChainData } from '@/store/slices/persist'; import { setupBucketQuota } from '@/store/slices/bucket'; import { useOffChainAuth } from '@/hooks/useOffChainAuth'; -import { getSpUrlByBucketName, getVirtualGroupFamily } from '@/facade/virtual-group'; +import { getSpUrlByBucketName } from '@/facade/virtual-group'; import { SpItem } from '@/store/slices/sp'; import { VisibilityType } from '../file/type'; diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx index f3ddc0e8..b300daa5 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { memo, useMemo, useState } from 'react'; import { Box, Flex, @@ -65,7 +65,10 @@ import { createTmpAccount } from '@/facade/account'; import { parseEther } from 'ethers/lib/utils.js'; import { useAccount } from 'wagmi'; -export const UploadObjects = () => { +interface UploadObjectsProps {} + +// add memo avoid parent state change rerender +export const UploadObjects = memo(function UploadObjects() { const dispatch = useAppDispatch(); const { _availableBalance: availableBalance } = useAppSelector((root) => root.global); const { editUpload, bucketName, path, objects, folders } = useAppSelector((root) => root.object); @@ -127,7 +130,7 @@ export const UploadObjects = () => { .filter((item) => item); const isExistObjectList = objectListNames.includes(file.name); const isExistUploadList = uploadingNames.includes(file.name); - if ( isExistObjectList || isExistUploadList) { + if (isExistObjectList || isExistUploadList) { return E_OBJECT_NAME_EXISTS; } return ''; @@ -161,11 +164,14 @@ export const UploadObjects = () => { Number(amount) * 1.05 > Number(availableBalance) ? round(Number(availableBalance), 6) : round(Number(amount) * 1.05, 6); - const [tmpAccount, error] = await createTmpAccount({ - address: loginAccount, - bucketName, - amount: parseEther(String(safeAmount)).toString(), - }, connector); + const [tmpAccount, error] = await createTmpAccount( + { + address: loginAccount, + bucketName, + amount: parseEther(String(safeAmount)).toString(), + }, + connector, + ); if (!tmpAccount) { return errorHandler(error); } @@ -203,7 +209,7 @@ export const UploadObjects = () => { }, [preLockFeeObjects, selectedFiles]); const checkedQueue = selectedFiles.filter((item) => item.status === 'WAIT'); - console.log(loading, creating, !checkedQueue?.length, !editUpload.isBalanceAvailable, editUpload) + // console.log(loading, creating, !checkedQueue?.length, !editUpload.isBalanceAvailable, editUpload); return ( @@ -213,7 +219,13 @@ export const UploadObjects = () => { setActiveKey(key)}> {tabOptions.map((item) => ( - + {item.icon} {item.title}({item.len}) @@ -241,10 +253,9 @@ export const UploadObjects = () => { Total Upload:{' '} {formatBytes( - checkedQueue.filter(item => item.status === 'WAIT').reduce( - (accumulator, currentValue) => accumulator + currentValue.size, - 0, - ), + checkedQueue + .filter((item) => item.status === 'WAIT') + .reduce((accumulator, currentValue) => accumulator + currentValue.size, 0), )} {' '} / {checkedQueue.length} Objects @@ -256,7 +267,9 @@ export const UploadObjects = () => { w="100%" variant={'dcPrimary'} onClick={onUploadClick} - isDisabled={loading || creating || !checkedQueue?.length || !editUpload.isBalanceAvailable} + isDisabled={ + loading || creating || !checkedQueue?.length || !editUpload.isBalanceAvailable + } justifyContent={'center'} gaClickName="dc.file.upload_modal.confirm.click" > @@ -274,4 +287,4 @@ export const UploadObjects = () => { ); -}; +}); diff --git a/apps/dcellar-web-ui/src/store/slices/object.ts b/apps/dcellar-web-ui/src/store/slices/object.ts index bda68c29..b68d5e88 100644 --- a/apps/dcellar-web-ui/src/store/slices/object.ts +++ b/apps/dcellar-web-ui/src/store/slices/object.ts @@ -11,6 +11,7 @@ export const SINGLE_OBJECT_MAX_SIZE = 128 * 1024 * 1024; export const SELECT_OBJECT_NUM_LIMIT = 10; export type ObjectItem = { + bucketName: string; objectName: string; name: string; payloadSize: number; @@ -38,8 +39,8 @@ export type TEditUploadContent = { preLockFee: string; totalFee: string; isBalanceAvailable: boolean; -} -export type TEditUpload = TEditUploadContent & { isOpen: boolean; } +}; +export type TEditUpload = TEditUploadContent & { isOpen: boolean }; export interface ObjectState { bucketName: string; folders: string[]; @@ -180,7 +181,7 @@ export const objectSlice = createSlice({ setEditUpload(state, { payload }: PayloadAction) { state.editUpload = { ...state.editUpload, - ...payload + ...payload, }; }, setEditCancel(state, { payload }: PayloadAction) { @@ -199,6 +200,7 @@ export const objectSlice = createSlice({ const folders = list.common_prefixes .reverse() .map((i, index) => ({ + bucketName, objectName: i, name: last(trimEnd(i, '/').split('/'))!, payloadSize: 0, @@ -232,6 +234,7 @@ export const objectSlice = createSlice({ state.objectsInfo[path] = i; return { + bucketName: bucket_name, objectName: object_name, name: last(object_name.split('/'))!, payloadSize: Number(payload_size), @@ -255,7 +258,7 @@ export const objectSlice = createSlice({ }, setListRefreshing(state, { payload }: PayloadAction) { state.refreshing = payload; - } + }, }, }); @@ -291,6 +294,7 @@ export const setupDummyFolder = setDummyFolder({ path, folder: { + bucketName, objectName: prefix + name + '/', name: last(trimEnd(name, '/').split('/'))!, payloadSize: 0, @@ -306,29 +310,28 @@ export const setupDummyFolder = }; export const setupListObjects = (params: Partial, _path?: string) => - async (dispatch: AppDispatch, getState: GetState) => { - - const { prefix, bucketName, path, restoreCurrent } = getState().object; - const { loginAccount: address } = getState().persist; - const _query = new URLSearchParams(params.query?.toString() || ''); - _query.append('max-keys', '1000'); - _query.append('delimiter', '/'); - if (prefix) _query.append('prefix', prefix); - // support any path list objects, bucketName & _path - const payload = { bucketName, ...params, query: _query, address } as ListObjectsParams; - // fix refresh then nav to other pages. - if (!bucketName) return; - const [res, error] = await _getAllList(payload); - if (error) { - toast.error({ description: error }); - return; - } - dispatch(setObjectList({ path: _path || path, list: res! })); - dispatch(setRestoreCurrent(true)); - if (!restoreCurrent) { - dispatch(setCurrentObjectPage({ path, current: 0 })); - } - }; + async (dispatch: AppDispatch, getState: GetState) => { + const { prefix, bucketName, path, restoreCurrent } = getState().object; + const { loginAccount: address } = getState().persist; + const _query = new URLSearchParams(params.query?.toString() || ''); + _query.append('max-keys', '1000'); + _query.append('delimiter', '/'); + if (prefix) _query.append('prefix', prefix); + // support any path list objects, bucketName & _path + const payload = { bucketName, ...params, query: _query, address } as ListObjectsParams; + // fix refresh then nav to other pages. + if (!bucketName) return; + const [res, error] = await _getAllList(payload); + if (error) { + toast.error({ description: error }); + return; + } + dispatch(setObjectList({ path: _path || path, list: res! })); + dispatch(setRestoreCurrent(true)); + if (!restoreCurrent) { + dispatch(setCurrentObjectPage({ path, current: 0 })); + } + }; export const closeStatusDetail = () => async (dispatch: AppDispatch) => { dispatch(setStatusDetail({} as TStatusDetail)); diff --git a/apps/dcellar-web-ui/src/utils/string.ts b/apps/dcellar-web-ui/src/utils/string.ts index 934e7e42..174939b9 100644 --- a/apps/dcellar-web-ui/src/utils/string.ts +++ b/apps/dcellar-web-ui/src/utils/string.ts @@ -46,3 +46,32 @@ export const formatId = (id: number) => { const value = `0x${hex.padStart(64, '0')}`; return value; }; + +export const copy = (text: string) => { + const range = document.createRange(); + const div = document.createElement('div'); + div.innerText = text; + div.style.position = 'absolute'; + div.style.left = '-99999px'; + div.style.top = '-99999px'; + document.body.appendChild(div); + range.selectNode(div); + + const selection = document.getSelection()!; + selection.removeAllRanges(); + selection.addRange(range); + + document.execCommand('copy'); + range.detach(); + document.body.removeChild(div); +}; + +const getObjectPath = (bucketName: string, objectName: string) => { + return [bucketName, encodeObjectName(objectName)].join('/'); +}; + +export const getShareLink = (bucketName: string, objectName: string) => { + return `${location.origin}/share?file=${encodeURIComponent( + getObjectPath(bucketName, objectName), + )}`; +}; From 08fd23742cd87b30d659da314922cfeb4bf70b02 Mon Sep 17 00:00:00 2001 From: aidencao Date: Tue, 8 Aug 2023 15:15:13 +0800 Subject: [PATCH 09/20] feat(dcellar-web-ui): update multi download ui --- .../src/components/common/DCTable/index.tsx | 6 +- .../object/components/BatchOperations.tsx | 78 ++++++++++++------- .../modules/object/components/NewObject.tsx | 36 +++++---- .../modules/object/components/ObjectList.tsx | 4 +- .../src/modules/object/index.tsx | 44 +++++++---- .../src/modules/object/objects.style.ts | 44 +++++++++-- 6 files changed, 144 insertions(+), 68 deletions(-) diff --git a/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx b/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx index 6d866b28..65704fe7 100644 --- a/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx +++ b/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx @@ -80,7 +80,7 @@ export const SealLoading = () => { }; export const UploadProgress = (props: { progress: number }) => { - let { progress = 0} = props; + let { progress = 0 } = props; if (progress < 0) { progress = 0; } @@ -206,4 +206,8 @@ const Container = styled.div` .ant-table-ping-right:not(.ant-table-has-fix-right) .ant-table-container::after { display: none; } + + .ant-checkbox-checked:after { + display: none; + } `; diff --git a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx index 70debd9c..023a1fe9 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx @@ -1,7 +1,5 @@ -import React, { memo } from 'react'; -import { ActionButton } from '@/modules/file/components/FileTable'; -import { DeleteIcon, DownloadIcon } from '@totejs/icons'; -import { Text } from '@totejs/uikit'; +import React, { memo, useMemo } from 'react'; +import { Box, Text, Tooltip } from '@totejs/uikit'; import { useAppDispatch, useAppSelector } from '@/store'; import { E_NO_QUOTA, E_OFF_CHAIN_AUTH, E_UNKNOWN } from '@/facade/error'; import { OBJECT_ERROR_TYPES, ObjectErrorType } from '@/modules/object/ObjectError'; @@ -12,19 +10,20 @@ import { setupBucketQuota } from '@/store/slices/bucket'; import { quotaRemains } from '@/facade/bucket'; import { getSpOffChainData } from '@/store/slices/persist'; import { downloadObject } from '@/facade/object'; +import { formatBytes } from '@/modules/file/utils'; +import { GhostButton } from '@/modules/object/objects.style'; interface BatchOperationsProps {} export const BatchOperations = memo(function BatchOperations() { const selectedRowKeys = useAppSelector((root) => root.object.selectedRowKeys); - const selected = selectedRowKeys.length; const dispatch = useAppDispatch(); const { setOpenAuthModal } = useOffChainAuth(); const { loginAccount } = useAppSelector((root) => root.persist); const { bucketName, objects, path } = useAppSelector((root) => root.object); const { primarySpInfo } = useAppSelector((root) => root.sp); const quotas = useAppSelector((root) => root.bucket.quotas); - const quotaData = quotas[bucketName]; + const quotaData = quotas[bucketName] || {}; const primarySp = primarySpInfo[bucketName]; useMount(() => { @@ -42,41 +41,64 @@ export const BatchOperations = memo(function BatchOperatio dispatch(setStatusDetail(errorData)); }; + const items = useMemo( + () => objects[path].filter((i) => selectedRowKeys.includes(i.objectName)), + [objects, path, selectedRowKeys], + ); + + const remainQuota = useMemo( + () => + quotaRemains( + quotaData, + items.reduce((x, y) => x + y.payloadSize, 0), + ), + [quotaData, items], + ); + const onBatchDownload = async () => { - const items = objects[path].filter((i) => selectedRowKeys.includes(i.objectName)); - let remainQuota = quotaRemains( - quotaData, - items.reduce((x, y) => x + y.payloadSize, 0), - ); if (!remainQuota) return onError(E_NO_QUOTA); const operator = primarySp.operatorAddress; const { seedString } = await dispatch(getSpOffChainData(loginAccount, operator)); for (const item of items) { - const payload = { - primarySp, - objectInfo: item, - address: loginAccount, - }; + const payload = { primarySp, objectInfo: item, address: loginAccount }; await downloadObject(payload, seedString); } dispatch(setSelectedRowKeys([])); + dispatch(setupBucketQuota(bucketName)); }; + const showDownload = items.every((i) => i.objectStatus === 1) || !items.length; + const downloadable = remainQuota && showDownload && !!items.length; + const remainQuotaBytes = formatBytes( + quotaData.freeQuota + quotaData.readQuota - quotaData.consumedQuota, + ); + return ( <> - - {selected} File{selected > 1 && 's'} Selected{' '} - - - - - - + + {showDownload && ( + + No enough quota to download your selected objects. Please reduce the number of + objects or increase quota. + + Remaining Quota: {remainQuotaBytes} + + + } + > +
+ + Download + +
+
+ )} + Delete
); diff --git a/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx index 4e07f051..bc277e43 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx @@ -1,12 +1,7 @@ import React, { ChangeEvent, memo } from 'react'; import { useAppDispatch, useAppSelector } from '@/store'; import { GAClick } from '@/components/common/GATracker'; -import { - Flex, - Text, - Tooltip, - toast, -} from '@totejs/uikit'; +import { Flex, Text, Tooltip, toast } from '@totejs/uikit'; import UploadIcon from '@/public/images/files/upload_transparency.svg'; import { SELECT_OBJECT_NUM_LIMIT, @@ -14,13 +9,15 @@ import { setEditUploadStatus, setListRefreshing, setRestoreCurrent, + setSelectedRowKeys, setupListObjects, } from '@/store/slices/object'; import { addToWaitQueue } from '@/store/slices/global'; import { getUtcZeroTimestamp } from '@bnb-chain/greenfield-chain-sdk'; -import { MenuCloseIcon, MenuOpenIcon } from '@totejs/icons'; import RefreshIcon from '@/public/images/icons/refresh.svg'; import { getSpOffChainData } from '@/store/slices/persist'; +import { BatchOperations } from '@/modules/object/components/BatchOperations'; +import { setupBucketQuota } from '@/store/slices/bucket'; interface NewObjectProps { showRefresh?: boolean; gaFolderClickName?: string; @@ -64,7 +61,7 @@ export const NewObject = memo(function NewObject({ return toast.error({ description: `You can only upload a maximum of ${SELECT_OBJECT_NUM_LIMIT} objects at a time.`, isClosable: true, - }) + }); } const uploadIds: number[] = []; Object.values(files).forEach((file: File) => { @@ -87,6 +84,8 @@ export const NewObject = memo(function NewObject({ query, endpoint: primarySp.endpoint, }; + dispatch(setSelectedRowKeys([])); + dispatch(setupBucketQuota(bucketName)); dispatch(setListRefreshing(true)); dispatch(setRestoreCurrent(false)); await dispatch(setupListObjects(params)); @@ -96,15 +95,18 @@ export const NewObject = memo(function NewObject({ return ( {showRefresh && ( - refreshList()} - alignItems="center" - height={40} - marginRight={12} - cursor="pointer" - > - - + <> + refreshList()} + alignItems="center" + height={40} + marginRight={12} + cursor="pointer" + > + + + + )} (function ObjectList() { query, endpoint: primarySp.endpoint, }; + dispatch(setSelectedRowKeys([])); dispatch(setupListObjects(params)); dispatch(setupBucketQuota(bucketName)); }, [primarySp, prefix]); @@ -350,7 +350,7 @@ export const ObjectList = memo(function ObjectList() { onSelect: onSelectChange, onSelectAll: onSelectAllChange, getCheckboxProps: (record: ObjectItem) => ({ - disabled: record.folder || record.objectStatus !== 1, // Column configuration not to be checked + disabled: record.folder, // Column configuration not to be checked name: record.name, }), }; diff --git a/apps/dcellar-web-ui/src/modules/object/index.tsx b/apps/dcellar-web-ui/src/modules/object/index.tsx index 1372e313..d2be5565 100644 --- a/apps/dcellar-web-ui/src/modules/object/index.tsx +++ b/apps/dcellar-web-ui/src/modules/object/index.tsx @@ -4,21 +4,23 @@ import { useAsyncEffect } from 'ahooks'; import { setBucketStatus, setupBucket } from '@/store/slices/bucket'; import Head from 'next/head'; import { + GoBack, ObjectContainer, ObjectName, PanelContainer, PanelContent, + SelectedText, } from '@/modules/object/objects.style'; import { ObjectBreadcrumb } from '@/modules/object/components/ObjectBreadcrumb'; -import { last } from 'lodash-es'; +import { dropRight, last } from 'lodash-es'; import { NewObject } from '@/modules/object/components/NewObject'; -import { Tooltip } from '@totejs/uikit'; +import { Tooltip, Flex } from '@totejs/uikit'; import { selectObjectList, setFolders } from '@/store/slices/object'; import { ObjectList } from '@/modules/object/components/ObjectList'; import { SpItem, setPrimarySpInfo } from '@/store/slices/sp'; import { getVirtualGroupFamily } from '@/facade/virtual-group'; import React, { useEffect } from 'react'; -import { BatchOperations } from '@/modules/object/components/BatchOperations'; +import { ForwardIcon } from '@totejs/icons'; export const ObjectsPage = () => { const dispatch = useAppDispatch(); @@ -32,6 +34,7 @@ export const ObjectsPage = () => { const items = path as string[]; const title = last(items)!; const [bucketName, ...folders] = items; + useEffect(() => { dispatch(setFolders({ bucketName, folders })); @@ -72,6 +75,11 @@ export const ObjectsPage = () => { const selected = selectedRowKeys.length; + const goBack = () => { + const path = dropRight(items).map(encodeURIComponent).join('/'); + router.push(`/buckets/${path}`); + }; + return ( @@ -80,18 +88,24 @@ export const ObjectsPage = () => { - {selected > 0 ? ( - - ) : ( - 40 ? 'visible' : 'hidden'} - > - {title} - - )} - + + + + + {selected > 0 ? ( + + {selected} File{selected > 1 && 's'} Selected + + ) : ( + 40 ? 'visible' : 'hidden'} + > + {title} + + )} + {!!objectList.length && ( ` @@ -39,3 +40,36 @@ export const StyledRow = styled.div<{ $disabled: boolean }>` } `} `; + +export const GoBack = styled(Box)` + margin-right: 16px; + svg { + transform: rotate(180deg); + } + background: transparent; + border-radius: 100%; + cursor: pointer; + :hover { + background-color: #f5f5f5; + } +`; + +export const SelectedText = styled(Text)` + color: #1e2026; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; +`; + +export const GhostButton = styled(Button)` + height: 40px; + background: #fff; + border-color: #e6e8ea; + &[disabled], + &[disabled]:hover { + background: #fff; + opacity: 1; + color: #aeb4bc; + } +`; From c7f9ee1b626528995df84266cefe82bf8adcf23a Mon Sep 17 00:00:00 2001 From: aidencao Date: Tue, 8 Aug 2023 18:38:47 +0800 Subject: [PATCH 10/20] feat(dcellar-web-ui): add share permission grant --- .../components/common/SvgIcon/ComingSoon.svg | 23 ++++ apps/dcellar-web-ui/src/facade/object.ts | 72 ++++++++++++- .../file/components/FileDetailModal.tsx | 2 - .../modules/object/components/AccessItem.tsx | 2 +- .../object/components/BatchOperations.tsx | 2 +- .../object/components/DetailObject.tsx | 16 +-- .../object/components/SharePermission.tsx | 101 ++++++++++-------- .../modules/object/components/ViewerList.tsx | 83 +++++++++++--- .../src/modules/object/objects.style.ts | 6 ++ .../src/modules/share/SharedFile.tsx | 4 +- apps/dcellar-web-ui/src/pages/share/index.tsx | 19 +++- 11 files changed, 255 insertions(+), 75 deletions(-) create mode 100644 apps/dcellar-web-ui/src/components/common/SvgIcon/ComingSoon.svg diff --git a/apps/dcellar-web-ui/src/components/common/SvgIcon/ComingSoon.svg b/apps/dcellar-web-ui/src/components/common/SvgIcon/ComingSoon.svg new file mode 100644 index 00000000..d0230e39 --- /dev/null +++ b/apps/dcellar-web-ui/src/components/common/SvgIcon/ComingSoon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/dcellar-web-ui/src/facade/object.ts b/apps/dcellar-web-ui/src/facade/object.ts index dec869be..e0375e6c 100644 --- a/apps/dcellar-web-ui/src/facade/object.ts +++ b/apps/dcellar-web-ui/src/facade/object.ts @@ -6,12 +6,15 @@ import { TxResponse, ISimulateGasFee, generateUrlByBucketName, + PermissionTypes, } from '@bnb-chain/greenfield-chain-sdk'; import { broadcastFault, commonFault, + createTxFault, E_NO_QUOTA, E_NOT_FOUND, + E_OFF_CHAIN_AUTH, E_PERMISSION_DENIED, E_UNKNOWN, ErrorMsg, @@ -41,6 +44,7 @@ import { signTypedDataV4 } from '@/utils/signDataV4'; import { getDomain } from '@/utils/getDomain'; import { generateGetObjectOptions } from '@/modules/file/utils/generateGetObjectOptions'; import { batchDownload, directlyDownload } from '@/modules/file/utils'; +import { ActionType } from '@bnb-chain/greenfield-cosmos-types/greenfield/permission/common'; export type DeliverResponse = Awaited>; @@ -70,6 +74,20 @@ export const updateObjectInfo = async ( return updateInfoTx.broadcast(broadcastPayload).then(resolve, broadcastFault); }; +export const hasObjectPermission = async ( + bucketName: string, + objectName: string, + actionType: ActionType, + loginAccount: string, +) => { + const client = await getClient(); + return client.object + .isObjectPermissionAllowed(bucketName, objectName, actionType, loginAccount) + .catch(() => ({ + effect: PermissionTypes.Effect.EFFECT_DENY, + })); +}; + export const getCanObjectAccess = async ( bucketName: string, objectName: string, @@ -88,7 +106,18 @@ export const getCanObjectAccess = async ( if (!info) return [false, E_NOT_FOUND]; const size = info.payloadSize.toString(); - if (info.visibility === VisibilityType.VISIBILITY_TYPE_PRIVATE && loginAccount !== info.owner) + // ACTION_GET_OBJECT + const res = await hasObjectPermission( + bucketName, + objectName, + PermissionTypes.ActionType.ACTION_GET_OBJECT, + loginAccount, + ); + if ( + info.visibility === VisibilityType.VISIBILITY_TYPE_PRIVATE && + loginAccount !== info.owner && + res.effect !== PermissionTypes.Effect.EFFECT_ALLOW + ) return [false, E_PERMISSION_DENIED]; if (!quota) return [false, E_UNKNOWN]; @@ -126,6 +155,8 @@ export const getAuthorizedLink = async ( export const downloadObject = async ( params: DownloadPreviewParams, seedString: string, + batch = false, + owner = true, ): Promise<[boolean, ErrorMsg?]> => { const { primarySp, objectInfo } = params; const { endpoint } = primarySp; @@ -133,15 +164,18 @@ export const downloadObject = async ( const isPrivate = visibility === VisibilityType.VISIBILITY_TYPE_PRIVATE; const link = `${endpoint}/download/${bucketName}/${encodeObjectName(objectName)}`; - if (!isPrivate) { - batchDownload(link); + + if (!isPrivate && owner) { + if (batch) batchDownload(link); + else window.location.href = link; return [true]; } const [url, error] = await getAuthorizedLink(params, seedString, 0); if (!url) return [false, error]; - batchDownload(url); + if (batch) batchDownload(url); + else window.location.href = url; return [true]; }; @@ -291,6 +325,36 @@ export const putObjectPolicy = async ( return tx.broadcast(broadcastPayload).then(resolve, broadcastFault); }; +export const putObjectPolicies = async ( + connector: Connector, + bucketName: string, + objectName: string, + srcMsg: Omit[], +): BroadcastResponse => { + const client = await getClient(); + const opts = await Promise.all( + srcMsg.map((msg) => + client.object.putObjectPolicy(bucketName, objectName, msg).then(resolve, createTxFault), + ), + ); + if (opts.some(([opt, error]) => !!error)) return [null, E_OFF_CHAIN_AUTH]; + const _opts = opts.map((opt) => opt[0] as TxResponse); + const txs = await client.basic.multiTx(_opts); + const [simulateInfo, simulateError] = await txs + .simulate({ denom: 'BNB' }) + .then(resolve, simulateFault); + if (simulateError) return [null, simulateError]; + const broadcastPayload = { + denom: 'BNB', + gasLimit: Number(simulateInfo?.gasLimit), + gasPrice: simulateInfo?.gasPrice || '5000000000', + payer: srcMsg[0].operator, + granter: '', + signTypedDataCallback: signTypedDataCallback(connector), + }; + return txs.broadcast(broadcastPayload).then(resolve, broadcastFault); +}; + export const preExecDeleteObject = async ( bucketName: string, objectName: string, diff --git a/apps/dcellar-web-ui/src/modules/file/components/FileDetailModal.tsx b/apps/dcellar-web-ui/src/modules/file/components/FileDetailModal.tsx index 973d3df0..7625f01c 100644 --- a/apps/dcellar-web-ui/src/modules/file/components/FileDetailModal.tsx +++ b/apps/dcellar-web-ui/src/modules/file/components/FileDetailModal.tsx @@ -16,7 +16,6 @@ import React, { useEffect, useRef, useState } from 'react'; import PrivateFileIcon from '@/public/images/icons/private_file.svg'; import PublicFileIcon from '@/public/images/icons/public_file.svg'; -import { useLogin } from '@/hooks/useLogin'; import { GREENFIELD_CHAIN_EXPLORER_URL } from '@/base/env'; import { BUTTON_GOT_IT, @@ -57,7 +56,6 @@ import { SpItem } from '@/store/slices/sp'; import { useAppDispatch, useAppSelector } from '@/store'; import { selectBnbPrice, setupTmpAvailableBalance } from '@/store/slices/global'; import { getSpOffChainData } from '@/store/slices/persist'; -import { selectBalance } from '@/store/slices/balance'; const renderFee = ( key: string, diff --git a/apps/dcellar-web-ui/src/modules/object/components/AccessItem.tsx b/apps/dcellar-web-ui/src/modules/object/components/AccessItem.tsx index 06d5e99d..738660ac 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/AccessItem.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/AccessItem.tsx @@ -19,7 +19,7 @@ const options = [ { icon: , label: 'Private', - desc: 'Only me can open with the link.', + desc: 'Only people with access can open with the link.', value: 2, bg: '#E6E8EA', }, diff --git a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx index 023a1fe9..36fa980c 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx @@ -62,7 +62,7 @@ export const BatchOperations = memo(function BatchOperatio for (const item of items) { const payload = { primarySp, objectInfo: item, address: loginAccount }; - await downloadObject(payload, seedString); + await downloadObject(payload, seedString, items.length > 1); } dispatch(setSelectedRowKeys([])); dispatch(setupBucketQuota(bucketName)); diff --git a/apps/dcellar-web-ui/src/modules/object/components/DetailObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/DetailObject.tsx index 98d6293d..d8a01190 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/DetailObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/DetailObject.tsx @@ -222,12 +222,10 @@ export const DetailObject = (props: modalProps) => { const dispatch = useAppDispatch(); const [action, setAction] = useState(''); const { accounts, loginAccount } = useAppSelector((root) => root.persist); - const { directDownload: allowDirectDownload } = accounts?.[loginAccount]; + const { directDownload: allowDirectDownload } = accounts?.[loginAccount] || {}; const { setOpenAuthModal } = useOffChainAuth(); - const {primarySpInfo}= useAppSelector((root) => root.sp); - const { editDetail, bucketName, objectsInfo, path } = useAppSelector( - (root) => root.object, - ); + const { primarySpInfo } = useAppSelector((root) => root.sp); + const { editDetail, bucketName, objectsInfo, path } = useAppSelector((root) => root.object); const primarySp = primarySpInfo[bucketName]; const key = `${path}/${editDetail.name}`; const objectInfo = objectsInfo[key]; @@ -259,7 +257,9 @@ export const DetailObject = (props: modalProps) => { const objectName = editDetail.objectName; const endpoint = primarySp.endpoint; setAction(e); - const { seedString } = await dispatch(getSpOffChainData(loginAccount, primarySp.operatorAddress)); + const { seedString } = await dispatch( + getSpOffChainData(loginAccount, primarySp.operatorAddress), + ); const [_, accessError, objectInfo] = await getCanObjectAccess( bucketName, objectName, @@ -282,6 +282,10 @@ export const DetailObject = (props: modalProps) => { return success; }; + if (!primarySp || !objectInfo) { + return <>; + } + return ( <> (function SharePermissi const [manageOpen, setManageOpen] = useState(false); const { connector } = useAccount(); const { setOpenAuthModal } = useOffChainAuth(); + const { hasCopied, onCopy, setValue } = useClipboard(''); + + useEffect(() => { + setValue(getShareLink(bucketName, editDetail.objectName)); + }, [setValue, bucketName, editDetail.objectName]); if (!editDetail.name) return <>; @@ -63,6 +82,7 @@ export const SharePermission = memo(function SharePermissi const handleError = (msg: ErrorMsg) => { switch (msg) { case AUTH_EXPIRED: + case E_OFF_CHAIN_AUTH: setOpenAuthModal(); return; default: @@ -96,28 +116,15 @@ export const SharePermission = memo(function SharePermissi ); const [_, error] = await updateObjectInfo(payload, connector!); - if (error) { - return handleError(error); - } + if (error) return handleError(error); dispatch(setStatusDetail({} as TStatusDetail)); toast.success({ description: 'Access updated!' }); - dispatch( - updateObjectVisibility({ - object: item, - visibility, - }), - ); + dispatch(updateObjectVisibility({ object: item, visibility })); }; return ( <> - setManageOpen(false)} - > + setManageOpen(false)}> setManageOpen(false)} /> (function SharePermissi + + + + Permission list will coming soon. + + + + + {hasCopied ? ( + <> + copy + Copied + + ) : ( + <> + Copy Link + + )} + + - {editDetail.objectStatus === 1 && ( + {editDetail.objectStatus === 1 && owner && ( - {/*Share with*/} - - {/**/} - {/* */} - {/* {CurrentAccess.icon}*/} - {/* {CurrentAccess.text}*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* setManageOpen(true)}>Manage Access*/} - {/**/} - {owner && ( - onAccessChange(editDetail, e)} - /> - )} - + Share with + + + {CurrentAccess.icon} + {CurrentAccess.text} + + + setManageOpen(true)}>Manage Access + + Only people with access can open with the link. + Copy Link - {/*Only people with access can open with the link.*/} )} diff --git a/apps/dcellar-web-ui/src/modules/object/components/ViewerList.tsx b/apps/dcellar-web-ui/src/modules/object/components/ViewerList.tsx index b573a375..e90ec4d3 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/ViewerList.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/ViewerList.tsx @@ -1,12 +1,24 @@ import { memo, useState } from 'react'; import styled from '@emotion/styled'; -import { Flex, Text } from '@totejs/uikit'; +import { Flex, Text, toast } from '@totejs/uikit'; import { DCComboBox } from '@/components/common/DCComboBox'; import { DCButton } from '@/components/common/DCButton'; -import { putObjectPolicy } from '@/facade/object'; -import { useAppSelector } from '@/store'; +import { putObjectPolicies } from '@/facade/object'; +import { useAppDispatch, useAppSelector } from '@/store'; import { useAccount } from 'wagmi'; -import { MsgPutPolicy } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/tx'; +import { E_OFF_CHAIN_AUTH } from '@/facade/error'; +import { setStatusDetail, TStatusDetail } from '@/store/slices/object'; +import { useOffChainAuth } from '@/hooks/useOffChainAuth'; +import { + BUTTON_GOT_IT, + FILE_ACCESS, + FILE_ACCESS_URL, + FILE_FAILED_URL, + FILE_STATUS_ACCESS, +} from '@/modules/file/constant'; +import { PermissionTypes } from '@bnb-chain/greenfield-chain-sdk'; + +const MAX_COUNT = 20; interface ViewerListProps {} @@ -15,25 +27,67 @@ export const ViewerList = memo(function ViewerList() { const { connector } = useAccount(); const { editDetail, bucketName } = useAppSelector((root) => root.object); const { loginAccount } = useAppSelector((root) => root.persist); + const { setOpenAuthModal } = useOffChainAuth(); + const dispatch = useAppDispatch(); + const [loading, setLoading] = useState(false); const _onChange = (e: string[]) => { setValues(e.filter((i) => i.match(/0x[a-z0-9]{40}/i))); }; + const inValid = values.length > MAX_COUNT; + + const onError = (type: string) => { + switch (type) { + case E_OFF_CHAIN_AUTH: + setOpenAuthModal(); + return; + default: + dispatch( + setStatusDetail({ + title: FILE_ACCESS, + icon: FILE_FAILED_URL, + buttonText: BUTTON_GOT_IT, + buttonOnClick: () => dispatch(setStatusDetail({} as TStatusDetail)), + errorText: 'Error message: ' + type, + }), + ); + return; + } + }; + const onInvite = async () => { - if (!values.length) return; - const msg: Omit = { + if (!values.length || loading) return; + const payloads = values.map((value) => ({ operator: loginAccount, // allow, get - statements: [{ effect: 1, actions: [6], resources: [''] }], + statements: [ + { + effect: PermissionTypes.Effect.EFFECT_ALLOW, + actions: [PermissionTypes.ActionType.ACTION_GET_OBJECT], + resources: [''], + }, + ], principal: { - // account - type: 1, - value: values[0], + /* account */ type: PermissionTypes.PrincipalType.PRINCIPAL_TYPE_GNFD_ACCOUNT, + value, }, - }; - const [res, error] = await putObjectPolicy(connector!, bucketName, editDetail.objectName, msg); - console.log(values, res, error); + })); + setLoading(true); + dispatch( + setStatusDetail({ title: FILE_ACCESS, icon: FILE_ACCESS_URL, desc: FILE_STATUS_ACCESS }), + ); + const [res, error] = await putObjectPolicies( + connector!, + bucketName, + editDetail.objectName, + payloads, + ); + setLoading(false); + if (error) return onError(error); + dispatch(setStatusDetail({} as TStatusDetail)); + setValues([]); + toast.success({ description: 'Access updated!' }); }; return ( @@ -53,10 +107,11 @@ export const ViewerList = memo(function ViewerList() { /> Viewer - + Invite + {inValid && Please enter less than 20 addresses. } ); }); diff --git a/apps/dcellar-web-ui/src/modules/object/objects.style.ts b/apps/dcellar-web-ui/src/modules/object/objects.style.ts index 328b372f..62703cd1 100644 --- a/apps/dcellar-web-ui/src/modules/object/objects.style.ts +++ b/apps/dcellar-web-ui/src/modules/object/objects.style.ts @@ -66,10 +66,16 @@ export const GhostButton = styled(Button)` height: 40px; background: #fff; border-color: #e6e8ea; + &:hover { + background: #1e2026; + color: #ffffff; + border-color: #1e2026; + } &[disabled], &[disabled]:hover { background: #fff; opacity: 1; color: #aeb4bc; + border-color: #e6e8ea; } `; diff --git a/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx b/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx index 39ab30a4..f46bd17b 100644 --- a/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx +++ b/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx @@ -86,7 +86,7 @@ export const SharedFile = memo(function SharedFile({ const operator = primarySp.operatorAddress; const { seedString } = await dispatch(getSpOffChainData(loginAccount, operator)); const isPrivate = objectInfo.visibility === VisibilityType.VISIBILITY_TYPE_PRIVATE; - if (isPrivate) { + if (isPrivate && loginAccount === objectInfo.owner) { const [_, accessError] = await getCanObjectAccess( bucketName, objectName, @@ -103,7 +103,7 @@ export const SharedFile = memo(function SharedFile({ address: loginAccount, }; const [success, opsError] = await (e === 'download' - ? downloadObject(params, seedString) + ? downloadObject(params, seedString, false, loginAccount === objectInfo.owner) : previewObject(params, seedString)); if (opsError) return onError(opsError as ShareErrorType); dispatch(setupBucketQuota(bucketName)); diff --git a/apps/dcellar-web-ui/src/pages/share/index.tsx b/apps/dcellar-web-ui/src/pages/share/index.tsx index 432aa191..67f565d4 100644 --- a/apps/dcellar-web-ui/src/pages/share/index.tsx +++ b/apps/dcellar-web-ui/src/pages/share/index.tsx @@ -23,7 +23,8 @@ import { Loading } from '@/components/common/Loading'; import { useAppDispatch, useAppSelector } from '@/store'; import { getSpOffChainData } from '@/store/slices/persist'; import { getSpUrlByBucketName } from '@/facade/virtual-group'; -import { headObject } from '@/facade/object'; +import { hasObjectPermission, headObject } from '@/facade/object'; +import { PermissionTypes } from '@bnb-chain/greenfield-chain-sdk'; const Container = styled.main` min-height: calc(100vh - 48px); @@ -46,6 +47,7 @@ const SharePage: NextPage = (props) => { const title = `${bucketName} - ${fileName}`; const { loginAccount } = useAppSelector((root) => root.persist); const dispatch = useAppDispatch(); + const [getPermission, setGetPermission] = useState(true); useAsyncEffect(async () => { if (!oneSp) return; @@ -63,7 +65,7 @@ const SharePage: NextPage = (props) => { endpoint: primarySpEndpoint, seedString, address: loginAccount, - } + }; if (!loginAccount || !isOwner) { const objectInfo = await headObject(bucketName, objectName); setObjectInfo(objectInfo); @@ -76,6 +78,17 @@ const SharePage: NextPage = (props) => { setQuotaData(quotaData); }, [oneSp]); + useAsyncEffect(async () => { + if (!loginAccount) return; + const res = await hasObjectPermission( + bucketName, + objectName, + PermissionTypes.ActionType.ACTION_GET_OBJECT, + loginAccount, + ); + setGetPermission(res.effect === PermissionTypes.Effect.EFFECT_ALLOW); + }, [bucketName, objectName, loginAccount]); + const isPrivate = objectInfo?.visibility === VisibilityType.VISIBILITY_TYPE_PRIVATE; const walletConnected = !!loginAccount; const isOwner = objectInfo?.owner === loginAccount; @@ -112,7 +125,7 @@ const SharePage: NextPage = (props) => { ) : ( <> - {isPrivate && !isOwner ? ( + {isPrivate && !isOwner && !getPermission ? ( ) : ( Date: Tue, 8 Aug 2023 21:27:21 +0800 Subject: [PATCH 11/20] feat(dcellar-web-ui): add share permission grant --- apps/dcellar-web-ui/src/facade/object.ts | 3 +-- apps/dcellar-web-ui/src/modules/share/SharedFile.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/dcellar-web-ui/src/facade/object.ts b/apps/dcellar-web-ui/src/facade/object.ts index e0375e6c..9e9b184b 100644 --- a/apps/dcellar-web-ui/src/facade/object.ts +++ b/apps/dcellar-web-ui/src/facade/object.ts @@ -156,7 +156,6 @@ export const downloadObject = async ( params: DownloadPreviewParams, seedString: string, batch = false, - owner = true, ): Promise<[boolean, ErrorMsg?]> => { const { primarySp, objectInfo } = params; const { endpoint } = primarySp; @@ -165,7 +164,7 @@ export const downloadObject = async ( const isPrivate = visibility === VisibilityType.VISIBILITY_TYPE_PRIVATE; const link = `${endpoint}/download/${bucketName}/${encodeObjectName(objectName)}`; - if (!isPrivate && owner) { + if (!isPrivate) { if (batch) batchDownload(link); else window.location.href = link; return [true]; diff --git a/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx b/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx index f46bd17b..46d0bc25 100644 --- a/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx +++ b/apps/dcellar-web-ui/src/modules/share/SharedFile.tsx @@ -103,7 +103,7 @@ export const SharedFile = memo(function SharedFile({ address: loginAccount, }; const [success, opsError] = await (e === 'download' - ? downloadObject(params, seedString, false, loginAccount === objectInfo.owner) + ? downloadObject(params, seedString) : previewObject(params, seedString)); if (opsError) return onError(opsError as ShareErrorType); dispatch(setupBucketQuota(bucketName)); From 06860971f3e12a07e44cc75deb4a496dd69e9d7a Mon Sep 17 00:00:00 2001 From: aidencao Date: Wed, 9 Aug 2023 14:51:40 +0800 Subject: [PATCH 12/20] feat(dcellar-web-ui): add fault sps & fix download pop tip ui issue --- .../src/components/common/DCSelect/index.tsx | 5 +- .../buckets/List/components/SPSelector.tsx | 78 ++++++++++++++----- .../object/components/BatchOperations.tsx | 8 +- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/apps/dcellar-web-ui/src/components/common/DCSelect/index.tsx b/apps/dcellar-web-ui/src/components/common/DCSelect/index.tsx index 16dcb17f..2bc3da34 100644 --- a/apps/dcellar-web-ui/src/components/common/DCSelect/index.tsx +++ b/apps/dcellar-web-ui/src/components/common/DCSelect/index.tsx @@ -158,15 +158,16 @@ export function Select(props: DCSelectProps) { return ( onSelectItem(item)} + onClick={() => item.access && onSelectItem(item)} _last={{ mb: 8, }} diff --git a/apps/dcellar-web-ui/src/modules/buckets/List/components/SPSelector.tsx b/apps/dcellar-web-ui/src/modules/buckets/List/components/SPSelector.tsx index 67fdea89..4411dcf7 100644 --- a/apps/dcellar-web-ui/src/modules/buckets/List/components/SPSelector.tsx +++ b/apps/dcellar-web-ui/src/modules/buckets/List/components/SPSelector.tsx @@ -1,18 +1,21 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { Box, Text } from '@totejs/uikit'; +import { Box, Flex, Text } from '@totejs/uikit'; import { IDCSelectOption, Select } from '@/components/common/DCSelect'; import { trimLongStr } from '@/utils/string'; import { useAppSelector } from '@/store'; import { useMount } from 'ahooks'; import { SpItem } from '@/store/slices/sp'; +import { sortBy } from 'lodash-es'; +import { ExternalLinkIcon } from '@totejs/icons'; interface SPSelector { onChange: (value: SpItem) => void; } export function SPSelector(props: SPSelector) { - const { sps, spInfo, oneSp } = useAppSelector((root) => root.sp); - const len = sps.length; + const { spInfo, oneSp, allSps } = useAppSelector((root) => root.sp); + const { faultySps } = useAppSelector((root) => root.persist); + const len = allSps.length; const [sp, setSP] = useState({} as SpItem); const [total, setTotal] = useState(0); const { onChange } = props; @@ -47,16 +50,20 @@ export function SPSelector(props: SPSelector) { const options = useMemo( () => - sps.map((item) => { + sortBy(allSps, [(i) => (faultySps.includes(i.operatorAddress) ? 1 : 0)]).map((item) => { const { operatorAddress, moniker: name, endpoint } = item; + const access = !faultySps.includes(operatorAddress); return { - label: , + label: ( + + ), value: operatorAddress, name, endpoint: item.endpoint, + access, }; }), - [sps], + [allSps], ); return ( @@ -80,20 +87,53 @@ const renderItem = (moniker: string, address: string) => { }; function OptionItem(props: any) { - const { address, name, endpoint } = props; + const { address, name, endpoint, access } = props; return ( - - - {renderItem(name, address)} - + + + + {renderItem(name, address)}{' '} + + {!access && ( + + + SP Error + + + )} + {name && ( {endpoint} diff --git a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx index 36fa980c..01ea64df 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx @@ -79,6 +79,7 @@ export const BatchOperations = memo(function BatchOperatio {showDownload && ( (function BatchOperatio } >
- + Download
From 5e592d9507ad10b3985754bc116a0a9211872721 Mon Sep 17 00:00:00 2001 From: aidencao Date: Wed, 9 Aug 2023 16:53:33 +0800 Subject: [PATCH 13/20] feat(dcellar-web-ui): fix batch delete issues --- apps/dcellar-web-ui/src/facade/account.ts | 3 +- apps/dcellar-web-ui/src/facade/error.ts | 9 +- .../object/components/BatchOperations.tsx | 27 ++-- .../modules/object/components/NewObject.tsx | 2 +- .../modules/object/components/ObjectList.tsx | 2 +- .../batch-delete/BatchDeleteObject.tsx | 126 +++++++++++------- 6 files changed, 103 insertions(+), 66 deletions(-) diff --git a/apps/dcellar-web-ui/src/facade/account.ts b/apps/dcellar-web-ui/src/facade/account.ts index 3b8b47eb..b4181370 100644 --- a/apps/dcellar-web-ui/src/facade/account.ts +++ b/apps/dcellar-web-ui/src/facade/account.ts @@ -11,11 +11,10 @@ import { Coin } from '@bnb-chain/greenfield-cosmos-types/cosmos/base/v1beta1/coi import { Wallet } from 'ethers'; import { parseEther } from 'ethers/lib/utils.js'; import { resolve } from './common'; -import { ErrorResponse, broadcastFault, commonFault, simulateFault } from './error'; +import { ErrorResponse, broadcastFault } from './error'; import { UNKNOWN_ERROR } from '@/modules/file/constant'; import { TTmpAccount } from '@/store/slices/global'; import { signTypedDataV4 } from '@/utils/signDataV4'; -import { signTypedDataCallback } from './wallet'; export type QueryBalanceRequest = { address: string; denom?: string }; type ActionType = 'delete' | 'create'; diff --git a/apps/dcellar-web-ui/src/facade/error.ts b/apps/dcellar-web-ui/src/facade/error.ts index d9fe7d73..d9452d57 100644 --- a/apps/dcellar-web-ui/src/facade/error.ts +++ b/apps/dcellar-web-ui/src/facade/error.ts @@ -23,6 +23,7 @@ export const E_OBJECT_NAME_NOT_UTF8 = 'OBJECT_NAME_NOT_UTF8'; export const E_OBJECT_NAME_CONTAINS_SLASH = 'OBJECT_NAME_CONTAINS_SLASH'; export const E_CAL_OBJECT_HASH = 'CAL_OBJECT_HASH'; export const E_OBJECT_NAME_EXISTS = 'OBJECT_NAME_EXISTS'; +export const E_OBJECT_NOT_EXISTS = 'No such object'; export const E_ACCOUNT_BALANCE_NOT_ENOUGH = 'ACCOUNT_BALANCE_NOT_ENOUGH'; export const E_NO_PERMISSION = 'NO_PERMISSION'; export const E_SP_STORAGE_PRICE_FAILED = 'SP_STORAGE_PRICE_FAILED'; @@ -41,6 +42,9 @@ export const simulateFault = (e: any): ErrorResponse => { if (e?.message.includes('static balance is not enough')) { return [null, E_GET_GAS_FEE_LACK_BALANCE_ERROR]; } + if (e?.message.includes('No such object')) { + return [null, E_OBJECT_NOT_EXISTS]; + } return [null, e?.message || E_UNKNOWN_ERROR]; }; @@ -66,7 +70,8 @@ export const createTxFault = (e: any): ErrorResponse => { 'user public key is expired', 'invalid signature', ].includes(message)) || - ((e as any).statusCode === 400 && ['user public key is expired', 'invalid signature'].includes(message)) + ((e as any).statusCode === 400 && + ['user public key is expired', 'invalid signature'].includes(message)) ) { return [null, E_OFF_CHAIN_AUTH]; } @@ -115,4 +120,4 @@ export const queryLockFeeFault = (e: any): ErrorResponse => { } return [null, E_UNKNOWN_ERROR]; -} +}; diff --git a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx index b1f27876..e02c85d9 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/BatchOperations.tsx @@ -3,7 +3,7 @@ import { Box, Text, Tooltip } from '@totejs/uikit'; import { useAppDispatch, useAppSelector } from '@/store'; import { E_NO_QUOTA, E_OFF_CHAIN_AUTH, E_UNKNOWN } from '@/facade/error'; import { OBJECT_ERROR_TYPES, ObjectErrorType } from '@/modules/object/ObjectError'; -import { setSelectedRowKeys, setStatusDetail } from '@/store/slices/object'; +import { setSelectedRowKeys, setStatusDetail, setupListObjects } from '@/store/slices/object'; import { useOffChainAuth } from '@/hooks/useOffChainAuth'; import { useMount } from 'ahooks'; import { setupBucketQuota } from '@/store/slices/bucket'; @@ -71,13 +71,6 @@ export const BatchOperations = memo(function BatchOperatio }; const onBatchDelete = async () => { - const items = objects[path].filter((i) => selectedRowKeys.includes(i.objectName)); - let remainQuota = quotaRemains( - { readQuota: 2000000, freeQuota: 2000000, consumedQuota: 2000000 }, - String(items.reduce((x, y) => x + y.payloadSize, 0)), - ); - if (!remainQuota) return onError(E_NO_QUOTA); - console.log(isBatchDeleteOpen, 'onBatchDelete'); setBatchDeleteOpen(true); }; @@ -87,7 +80,19 @@ export const BatchOperations = memo(function BatchOperatio quotaData.freeQuota + quotaData.readQuota - quotaData.consumedQuota, ); - const refetch = async (name?: string) => {}; + const refetch = async () => { + if (!primarySp || !loginAccount) return; + const { seedString } = await dispatch( + getSpOffChainData(loginAccount, primarySp.operatorAddress), + ); + const query = new URLSearchParams(); + const params = { + seedString, + query, + endpoint: primarySp.endpoint, + }; + dispatch(setupListObjects(params)); + }; return ( <> @@ -124,7 +129,9 @@ export const BatchOperations = memo(function BatchOperatio
)} - Delete + + Delete +
); diff --git a/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx index bc277e43..f09603c1 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/NewObject.tsx @@ -97,7 +97,7 @@ export const NewObject = memo(function NewObject({ {showRefresh && ( <> refreshList()} + onClick={refreshList} alignItems="center" height={40} marginRight={12} diff --git a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx index b8d9b7db..e3f62e53 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx @@ -350,7 +350,7 @@ export const ObjectList = memo(function ObjectList() { onSelect: onSelectChange, onSelectAll: onSelectAllChange, getCheckboxProps: (record: ObjectItem) => ({ - disabled: record.folder, // Column configuration not to be checked + disabled: record.folder || record.objectStatus !== 1, // Column configuration not to be checked name: record.name, }), }; diff --git a/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx index ef92a0c0..bc307d08 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx @@ -1,6 +1,6 @@ -import { ModalCloseButton, ModalHeader, ModalFooter, Text, Flex, toast, Box } from '@totejs/uikit'; +import { Box, Flex, ModalCloseButton, ModalFooter, ModalHeader, Text, toast } from '@totejs/uikit'; import { useAccount } from 'wagmi'; -import React, { use, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { renderBalanceNumber, renderFeeValue, @@ -14,28 +14,34 @@ import { FILE_STATUS_DELETING, FILE_TITLE_DELETE_FAILED, FILE_TITLE_DELETING, - FOLDER_TITLE_DELETING, } from '@/modules/file/constant'; import { DCModal } from '@/components/common/DCModal'; -import { Tips } from '@/components/common/Tips'; import { DCButton } from '@/components/common/DCButton'; import { reportEvent } from '@/utils/reportEvent'; import { getClient } from '@/base/client'; -import { signTypedDataV4 } from '@/utils/signDataV4'; -import { E_USER_REJECT_STATUS_NUM, broadcastFault } from '@/facade/error'; +import { + broadcastFault, + createTxFault, + E_OBJECT_NOT_EXISTS, + E_OFF_CHAIN_AUTH, + E_USER_REJECT_STATUS_NUM, + simulateFault, +} from '@/facade/error'; import { useAppDispatch, useAppSelector } from '@/store'; import { - TStatusDetail, + addDeletedObject, setSelectedRowKeys, setStatusDetail, - addDeletedObject, + TStatusDetail, } from '@/store/slices/object'; import { MsgDeleteObjectTypeUrl } from '@bnb-chain/greenfield-chain-sdk'; -import { setupTmpAvailableBalance, setTmpAccount, TTmpAccount } from '@/store/slices/global'; +import { setTmpAccount, setupTmpAvailableBalance, TTmpAccount } from '@/store/slices/global'; import { createTmpAccount } from '@/facade/account'; import { parseEther } from 'ethers/lib/utils.js'; import { round } from 'lodash-es'; import { ColoredWaitingIcon } from '@totejs/icons'; +import { resolve } from '@/facade/common'; +import { useOffChainAuth } from '@/hooks/useOffChainAuth'; interface modalProps { refetch: () => void; @@ -88,16 +94,15 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => const { price: bnbPrice } = useAppSelector((root) => root.global.bnb); const selectedRowKeys = useAppSelector((root) => root.object.selectedRowKeys); const { bucketName, objectsInfo, path } = useAppSelector((root) => root.object); - const { primarySpInfo } = useAppSelector((root) => root.sp); - const primarySp = primarySpInfo[bucketName]; const exchangeRate = +bnbPrice ?? 0; const [loading, setLoading] = useState(false); const [buttonDisabled, setButtonDisabled] = useState(false); const { _availableBalance: availableBalance } = useAppSelector((root) => root.global); const [isModalOpen, setModalOpen] = useState(isOpen); + const { setOpenAuthModal } = useOffChainAuth(); const deleteObjects = selectedRowKeys.map((key) => { - return objectsInfo[path + '/' + key]; + return objectsInfo[bucketName + '/' + key]; }); const onClose = () => { @@ -106,7 +111,7 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => cancelFn(); }; const { gasObjects } = useAppSelector((root) => root.global.gasHub); - const simulateGasFee = gasObjects[MsgDeleteObjectTypeUrl]?.gasFee ?? 0; + const simulateGasFee = gasObjects[MsgDeleteObjectTypeUrl]?.gasFee * deleteObjects.length ?? 0; const { connector } = useAccount(); useEffect(() => { @@ -153,45 +158,56 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => }; const errorHandler = (error: string) => { setLoading(false); - dispatch( - setStatusDetail({ - title: FILE_TITLE_DELETE_FAILED, - icon: FILE_FAILED_URL, - desc: 'Sorry, there’s something wrong when signing with the wallet.', - buttonText: BUTTON_GOT_IT, - errorText: 'Error message: ' + error, - buttonOnClick: () => dispatch(setStatusDetail({} as TStatusDetail)), - }), - ); + switch (error) { + case E_OFF_CHAIN_AUTH: + setOpenAuthModal(); + return; + default: + dispatch( + setStatusDetail({ + title: FILE_TITLE_DELETE_FAILED, + icon: FILE_FAILED_URL, + desc: 'Sorry, there’s something wrong when signing with the wallet.', + buttonText: BUTTON_GOT_IT, + errorText: 'Error message: ' + error, + buttonOnClick: () => dispatch(setStatusDetail({} as TStatusDetail)), + }), + ); + } }; const deleteObject = async (objectName: string, tmpAccount: TTmpAccount) => { const client = await getClient(); - const delObjTx = await client.object.deleteObject({ - bucketName, - objectName, - operator: tmpAccount.address, - }); + const [delObjTx, delError] = await client.object + .deleteObject({ + bucketName, + objectName, + operator: tmpAccount.address, + }) + .then(resolve, createTxFault); + if (delError) return [false, delError]; - const simulateInfo = await delObjTx.simulate({ - denom: 'BNB', - }); + const [simulateInfo, simulateError] = await delObjTx! + .simulate({ + denom: 'BNB', + }) + .then(resolve, simulateFault); + if (simulateError) return [false, simulateError]; - const txRes = await delObjTx.broadcast({ - denom: 'BNB', - gasLimit: Number(simulateInfo?.gasLimit), - gasPrice: simulateInfo?.gasPrice || '5000000000', - payer: tmpAccount.address, - granter: loginAccount, - privateKey: tmpAccount.privateKey, - }); + const [txRes, error] = await delObjTx! + .broadcast({ + denom: 'BNB', + gasLimit: Number(simulateInfo?.gasLimit), + gasPrice: simulateInfo?.gasPrice || '5000000000', + payer: tmpAccount.address, + granter: loginAccount, + privateKey: tmpAccount.privateKey, + }) + .then(resolve, broadcastFault); - if (txRes === null) { - dispatch(setStatusDetail({} as TStatusDetail)); - return toast.error({ description: 'Delete object error.' }); - } - if (txRes.code === 0) { + if (error) return [false, error]; + if (txRes!.code === 0) { toast.success({ - description: 'object deleted successfully.', + description: `${objectName} deleted successfully.`, }); reportEvent({ name: 'dc.toast.file_delete.success.show', @@ -199,6 +215,7 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => } else { toast.error({ description: 'Delete file error.' }); } + return [true, '']; }; const onConfirmDelete = async () => { @@ -227,8 +244,12 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => async function deleteInRow() { if (!tmpAccount) return; - for (let obj of deleteObjects) { - await deleteObject(obj.object_info.object_name, tmpAccount); + for await (let obj of deleteObjects) { + const [success, error] = await deleteObject(obj.object_info.object_name, tmpAccount); + if (error && error !== E_OBJECT_NOT_EXISTS) { + errorHandler(error as string); + return false; + } dispatch( addDeletedObject({ path: [bucketName, obj.object_info.object_name].join('/'), @@ -236,13 +257,18 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => }), ); } + return true; } - deleteInRow(); + toast.info({ description: 'Objects deleting', icon: }); + const success = await deleteInRow(); refetch(); onClose(); - dispatch(setSelectedRowKeys([])); - dispatch(setStatusDetail({} as TStatusDetail)); + + if (success) { + dispatch(setSelectedRowKeys([])); + dispatch(setStatusDetail({} as TStatusDetail)); + } setLoading(false); } catch (error: any) { setLoading(false); From b82453108903dd0ab850ecb5deeb54a6e837b50d Mon Sep 17 00:00:00 2001 From: aidencao Date: Wed, 9 Aug 2023 17:09:04 +0800 Subject: [PATCH 14/20] feat(dcellar-web-ui): fix createTmpAccount params --- .../src/modules/upload/UploadObjects.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx index b300daa5..1d63e260 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx @@ -164,14 +164,12 @@ export const UploadObjects = memo(function UploadObjects() { Number(amount) * 1.05 > Number(availableBalance) ? round(Number(availableBalance), 6) : round(Number(amount) * 1.05, 6); - const [tmpAccount, error] = await createTmpAccount( - { - address: loginAccount, - bucketName, - amount: parseEther(String(safeAmount)).toString(), - }, + const [tmpAccount, error] = await createTmpAccount({ + address: loginAccount, + bucketName, + amount: parseEther(String(safeAmount)).toString(), connector, - ); + }); if (!tmpAccount) { return errorHandler(error); } From 02da9b1f5909eeb1d39f853f3c68abbd86bd3640 Mon Sep 17 00:00:00 2001 From: Wenty Li <105278450+wenty22@users.noreply.github.com> Date: Wed, 9 Aug 2023 17:26:23 +0800 Subject: [PATCH 15/20] fix(dcellar-web-ui): fix refresh page will logout issue (#166) * fix(dcellar-web-ui): auto logout if wallet is locked * test(dcellar-web-ui): add wallet log * feat(dcellar-web-ui): add wallet log * test(dcellar-web-ui): add log * test(dcellar-web-ui): add log * test(dcellar-web-ui): add log * test: add log * test: add log * fix(dcellar-web-ui): fix refresh page will logout if using trust wallet * test: add log * test: test wallet * test: add log * test: add log * test: remove MetaMaskConnector * fix: customize MetaMaskConnector & TrustWalletConnector * test: add TrustWalletConnector connect method * refactor: fix wallet issue * fix(dcellar-web-ui): fix wallet issue * fix: remove wallet log * test: test * test: add log * fix: fix wallet issues * fix: remove logs * fix(dcellar-web-ui): remove unused dependencies --- apps/dcellar-web-ui/package.json | 3 +- .../src/context/LoginContext/provider.tsx | 26 +++++-- .../{config/chains.ts => chains/index.ts} | 0 .../components/WalletConnectProvider.tsx | 4 +- .../WalletConnectContext/config/connectors.ts | 16 ----- .../connectors/MetaMaskConnector.ts | 3 + .../connectors/TrustWalletConnector.ts | 72 +++++++++++++++++++ .../WalletConnectContext/connectors/index.tsx | 8 +++ .../hooks/useWalletSwitchAccount.ts | 12 ++-- apps/dcellar-web-ui/typings.d.ts | 2 +- common/config/rush/pnpm-lock.yaml | 2 + 11 files changed, 117 insertions(+), 31 deletions(-) rename apps/dcellar-web-ui/src/context/WalletConnectContext/{config/chains.ts => chains/index.ts} (100%) delete mode 100644 apps/dcellar-web-ui/src/context/WalletConnectContext/config/connectors.ts create mode 100644 apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/MetaMaskConnector.ts create mode 100644 apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/TrustWalletConnector.ts create mode 100644 apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/index.tsx diff --git a/apps/dcellar-web-ui/package.json b/apps/dcellar-web-ui/package.json index a11a2732..a3da1d0e 100644 --- a/apps/dcellar-web-ui/package.json +++ b/apps/dcellar-web-ui/package.json @@ -48,7 +48,8 @@ "@reduxjs/toolkit": "^1.9.5", "react-redux": "^8.1.1", "next-redux-wrapper": "^8.1.0", - "redux-persist": "^6.0.0" + "redux-persist": "^6.0.0", + "@wagmi/core": "^0.10.13" }, "devDependencies": { "@babel/plugin-syntax-flow": "^7.14.5", diff --git a/apps/dcellar-web-ui/src/context/LoginContext/provider.tsx b/apps/dcellar-web-ui/src/context/LoginContext/provider.tsx index 4ccd4312..c49445d0 100644 --- a/apps/dcellar-web-ui/src/context/LoginContext/provider.tsx +++ b/apps/dcellar-web-ui/src/context/LoginContext/provider.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, useCallback, useMemo } from 'react'; +import { PropsWithChildren, useCallback, useEffect, useMemo } from 'react'; import { LoginContext } from '@/context/LoginContext/index'; @@ -19,11 +19,12 @@ export function LoginContextProvider(props: PropsWithChildren root.persist); + const { disconnect } = useDisconnect(); const logout = useCallback( - (removeSpAuth = false) => { + (removeSpAuth = true) => { dispatch(resetUploadQueue({loginAccount})) dispatch(setLogout(removeSpAuth)); disconnect(); @@ -42,21 +43,36 @@ export function LoginContextProvider(props: PropsWithChildren { + useEffect(() => { if (pathname === '/' || inline) return; if (!walletAddress || loginAccount !== walletAddress) { logout(); } + // Once the wallet is connected, we can get the address + // but if wallet is locked, we can't get the connector from wagmi + // to avoid errors when using the connector, we treat this situation as logout. + const timer = setTimeout(() => { + if (!connector) { + logout() + } + }, 1000) + + return () => { + clearTimeout(timer) + } + }, [connector, inline, loginAccount, logout, pathname, walletAddress]) + + useAsyncEffect(async () => { if (loginAccount === walletAddress) { // expire date less than 24h,remove sp auth & logout const spMayExpired = await dispatch(checkSpOffChainMayExpired(walletAddress)); if (spMayExpired) logout(true); } - }, [walletAddress, pathname]); + }, [walletAddress]); const { pass } = useLoginGuard(inline); diff --git a/apps/dcellar-web-ui/src/context/WalletConnectContext/config/chains.ts b/apps/dcellar-web-ui/src/context/WalletConnectContext/chains/index.ts similarity index 100% rename from apps/dcellar-web-ui/src/context/WalletConnectContext/config/chains.ts rename to apps/dcellar-web-ui/src/context/WalletConnectContext/chains/index.ts diff --git a/apps/dcellar-web-ui/src/context/WalletConnectContext/components/WalletConnectProvider.tsx b/apps/dcellar-web-ui/src/context/WalletConnectContext/components/WalletConnectProvider.tsx index 30428333..372fddd4 100644 --- a/apps/dcellar-web-ui/src/context/WalletConnectContext/components/WalletConnectProvider.tsx +++ b/apps/dcellar-web-ui/src/context/WalletConnectContext/components/WalletConnectProvider.tsx @@ -1,6 +1,6 @@ import { WagmiConfig, createClient } from 'wagmi'; -import { provider, webSocketProvider } from '@/context/WalletConnectContext/config/chains'; -import { connectors } from '@/context/WalletConnectContext/config/connectors'; +import { provider, webSocketProvider } from '@/context/WalletConnectContext/chains'; +import { connectors } from '@/context/WalletConnectContext/connectors'; const client = createClient({ autoConnect: true, diff --git a/apps/dcellar-web-ui/src/context/WalletConnectContext/config/connectors.ts b/apps/dcellar-web-ui/src/context/WalletConnectContext/config/connectors.ts deleted file mode 100644 index 938e28f9..00000000 --- a/apps/dcellar-web-ui/src/context/WalletConnectContext/config/connectors.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { chains } from '@/context/WalletConnectContext/config/chains'; -import { InjectedConnector } from 'wagmi/connectors/injected'; -import { MetaMaskConnector } from 'wagmi/connectors/metaMask'; - -const trustWalletConnector = new InjectedConnector({ - chains, - options: { - name: 'Trust Wallet', - shimDisconnect: true, - getProvider: () => (typeof window !== 'undefined' ? window.trustwallet : undefined), - }, -}); - -const metaMaskConnector = new MetaMaskConnector({ chains }); - -export const connectors = [trustWalletConnector, metaMaskConnector]; diff --git a/apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/MetaMaskConnector.ts b/apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/MetaMaskConnector.ts new file mode 100644 index 00000000..28f4dd58 --- /dev/null +++ b/apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/MetaMaskConnector.ts @@ -0,0 +1,3 @@ +import { MetaMaskConnector as WagmiMetaMaskConnector } from 'wagmi/connectors/metaMask'; + +export class MetaMaskConnector extends WagmiMetaMaskConnector {} \ No newline at end of file diff --git a/apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/TrustWalletConnector.ts b/apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/TrustWalletConnector.ts new file mode 100644 index 00000000..f47aa532 --- /dev/null +++ b/apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/TrustWalletConnector.ts @@ -0,0 +1,72 @@ +import { Chain } from 'wagmi' +import { MetaMaskConnector as WagmiMetaMaskConnector } from 'wagmi/connectors/metaMask'; +import { + getClient, +} from '@wagmi/core' + +export type TrustWalletConnectorOptions = { + shimDisconnect?: boolean +} + +export class TrustWalletConnector extends WagmiMetaMaskConnector { + readonly id: any = 'trustWallet'; + + constructor({ + chains, + options: _options, + }: { + chains?: Chain[] + options?: TrustWalletConnectorOptions + } = {}) { + + const options = { + name: 'Trust Wallet', + shimDisconnect: true, + UNSTABLE_shimOnConnectSelectAccount: true, + getProvider: getTrustWalletProvider, + ..._options, + } + + super({ + chains, + options, + }) + } + + async disconnect() { + super.disconnect() + + const provider: any = await this.getProvider() + if (!provider?.off) return + + provider.off('accountsChanged', this.onAccountsChanged) + provider.off('chainChanged', this.onChainChanged) + provider.off('disconnect', this.onDisconnect) + + if (this.options.shimDisconnect) { + getClient().storage?.removeItem(this.shimDisconnectKey) + } + } +} + +export function getTrustWalletProvider() { + const isTrustWallet = (ethereum: any) => { + return !!ethereum.isTrust + } + + const injectedProviderExist = typeof window !== 'undefined' && typeof window.ethereum !== 'undefined' + + if (!injectedProviderExist) { + return + } + + if (isTrustWallet(window.ethereum)) { + return window.ethereum + } + + if (window.ethereum?.providers) { + return window.ethereum.providers.find(isTrustWallet) + } + + return window.trustWallet +} diff --git a/apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/index.tsx b/apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/index.tsx new file mode 100644 index 00000000..53f85a7d --- /dev/null +++ b/apps/dcellar-web-ui/src/context/WalletConnectContext/connectors/index.tsx @@ -0,0 +1,8 @@ +import { chains } from '@/context/WalletConnectContext/chains'; +import { MetaMaskConnector } from '@/context/WalletConnectContext/connectors/MetaMaskConnector'; +import { TrustWalletConnector } from '@/context/WalletConnectContext/connectors/TrustWalletConnector'; + +const trustWalletConnector = new TrustWalletConnector({ chains }) +const metaMaskConnector = new MetaMaskConnector({ chains }) + +export const connectors = [trustWalletConnector, metaMaskConnector]; diff --git a/apps/dcellar-web-ui/src/context/WalletConnectContext/hooks/useWalletSwitchAccount.ts b/apps/dcellar-web-ui/src/context/WalletConnectContext/hooks/useWalletSwitchAccount.ts index 9d8c87c2..063bb133 100644 --- a/apps/dcellar-web-ui/src/context/WalletConnectContext/hooks/useWalletSwitchAccount.ts +++ b/apps/dcellar-web-ui/src/context/WalletConnectContext/hooks/useWalletSwitchAccount.ts @@ -5,20 +5,20 @@ import { ConnectorData, useAccount } from 'wagmi'; export type WalletSwitchAccountHandler = (data: ConnectorData) => void; export function useWalletSwitchAccount(handler: WalletSwitchAccountHandler) { - const { connector } = useAccount(); + const {address, connector } = useAccount(); const handlerRef = useSaveFuncRef(handler); useEffect(() => { - const handler = (data: ConnectorData) => { - if (data.account) { + const onChange = (data: ConnectorData) => { + if (data.account && data.account !== address) { handlerRef.current?.(data); } }; - connector?.on('change', handler); + connector?.on('change', onChange); return () => { - connector?.off('change', handler); + connector?.off('change', onChange); }; - }, [connector, handlerRef]); + }, [address, connector, handlerRef]); } diff --git a/apps/dcellar-web-ui/typings.d.ts b/apps/dcellar-web-ui/typings.d.ts index c726fd98..3fcb5b49 100644 --- a/apps/dcellar-web-ui/typings.d.ts +++ b/apps/dcellar-web-ui/typings.d.ts @@ -28,7 +28,7 @@ declare global { ethereum: any; ga: any; clipboardData: any; - trustwallet: any; + trustWallet: any; // zk.wasm export eddsaSign: any; } diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index ee49b69b..f34af1f2 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -32,6 +32,7 @@ importers: '@types/node': 18.16.0 '@types/react': 18.0.38 '@types/react-dom': 18.0.11 + '@wagmi/core': ^0.10.13 ahooks: 3.7.7 antd: 5.6.3 apollo-node-client: 1.4.3 @@ -74,6 +75,7 @@ importers: '@tanstack/react-virtual': 3.0.0-alpha.0_react@18.2.0 '@totejs/icons': 2.15.0_aa3274991927adc2766d9259998fdd18 '@totejs/uikit': 2.49.4_aa3274991927adc2766d9259998fdd18 + '@wagmi/core': 0.10.17_01c1674540fb06277196a21a8a2e0b3e ahooks: 3.7.7_react@18.2.0 antd: 5.6.3_react-dom@18.2.0+react@18.2.0 apollo-node-client: 1.4.3 From 46131b32b43baa2d304e41840f81b8d9478d2182 Mon Sep 17 00:00:00 2001 From: devinxl <94832688+devinxl@users.noreply.github.com> Date: Wed, 9 Aug 2023 18:14:11 +0800 Subject: [PATCH 16/20] fix(dcellar-web-ui): upload error and add tmp account fee (#163) * fix(dcellar-web-ui): upload error and add tmp account fee * fix(dcellar-web-ui): recover sealing status to table --- .../src/components/common/DCTable/index.tsx | 27 ++++++++++++++----- .../components/layout/Header/GlobalTasks.tsx | 7 ++--- apps/dcellar-web-ui/src/facade/account.ts | 1 + .../src/modules/upload/SimulateFee.tsx | 15 +++++++---- .../src/modules/upload/UploadObjects.tsx | 21 ++++++++------- .../dcellar-web-ui/src/store/slices/global.ts | 14 ++++++++-- .../dcellar-web-ui/src/utils/common/index.tsx | 17 ++++++++++++ 7 files changed, 76 insertions(+), 26 deletions(-) create mode 100644 apps/dcellar-web-ui/src/utils/common/index.tsx diff --git a/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx b/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx index 65704fe7..824efabd 100644 --- a/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx +++ b/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx @@ -70,12 +70,27 @@ export const SealLoading = () => { } `; return ( - + + + + + + Sealing... + + ); }; diff --git a/apps/dcellar-web-ui/src/components/layout/Header/GlobalTasks.tsx b/apps/dcellar-web-ui/src/components/layout/Header/GlobalTasks.tsx index 8859ed05..f77ed80e 100644 --- a/apps/dcellar-web-ui/src/components/layout/Header/GlobalTasks.tsx +++ b/apps/dcellar-web-ui/src/components/layout/Header/GlobalTasks.tsx @@ -24,6 +24,7 @@ import { genCreateObjectTx } from '@/modules/file/utils/genCreateObjectTx'; import { resolve } from '@/facade/common'; import { broadcastFault, commonFault, createTxFault, simulateFault } from '@/facade/error'; import { isEmpty } from 'lodash-es'; +import { parseErrorXml } from '@/utils/common'; interface GlobalTasksProps {} @@ -159,12 +160,12 @@ export const GlobalTasks = memo(function GlobalTasks() { 'X-Gnfd-User-Address': headers.get('X-Gnfd-User-Address'), 'X-Gnfd-App-Domain': headers.get('X-Gnfd-App-Domain'), }, - }).catch(e => { - console.log('upload error', e); + }).catch(async (e: Response) => { + const {code, message} = await parseErrorXml(e) dispatch(updateUploadTaskMsg({ account: loginAccount, id: task.id, - msg: e?.message || 'Upload error', + msg: message || 'Upload error', })); }) }; diff --git a/apps/dcellar-web-ui/src/facade/account.ts b/apps/dcellar-web-ui/src/facade/account.ts index b4181370..72345e8d 100644 --- a/apps/dcellar-web-ui/src/facade/account.ts +++ b/apps/dcellar-web-ui/src/facade/account.ts @@ -51,6 +51,7 @@ export const createTmpAccount = async ({ // 2. allow temporary account to submit specified tx and amount const client = await getClient(); + // MsgGrantAllowanceTypeUrl const grantAllowanceTx = await client.feegrant.grantAllowance({ granter: address, grantee: wallet.address, diff --git a/apps/dcellar-web-ui/src/modules/upload/SimulateFee.tsx b/apps/dcellar-web-ui/src/modules/upload/SimulateFee.tsx index 7ac7743c..55aae2cf 100644 --- a/apps/dcellar-web-ui/src/modules/upload/SimulateFee.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/SimulateFee.tsx @@ -6,15 +6,14 @@ import { renderPrelockedFeeValue, } from '@/modules/file/utils'; import { useAppDispatch, useAppSelector } from '@/store'; -import { MsgCreateObjectTypeUrl } from '@bnb-chain/greenfield-chain-sdk'; -import { Box, Flex, Slide, Text, useDisclosure, Link } from '@totejs/uikit'; +import { MsgCreateObjectTypeUrl, MsgGrantAllowanceTypeUrl, MsgPutPolicyTypeUrl } from '@bnb-chain/greenfield-chain-sdk'; +import { Box, Flex, Text, useDisclosure, Link } from '@totejs/uikit'; import React, { useEffect, useMemo } from 'react'; import { useAsyncEffect, useMount } from 'ahooks'; import { WaitFile, setupPreLockFeeObjects, setupTmpAvailableBalance } from '@/store/slices/global'; import { isEmpty } from 'lodash-es'; import { calPreLockFee } from '@/utils/sp'; import { MenuCloseIcon } from '@totejs/icons'; -import { useUpdateEffect } from 'react-use'; import { setEditUpload } from '@/store/slices/object'; import BigNumber from 'bignumber.js'; import { DECIMAL_NUMBER } from '../wallet/constants'; @@ -39,8 +38,14 @@ export const Fee = () => { } }, [primarySp?.operatorAddress]); - const lockFee = useMemo(() => { + const createTmpAccountGasFee = useMemo(() => { + const grantAllowTxFee = BigNumber(gasObjects[MsgGrantAllowanceTypeUrl].gasFee).plus(BigNumber(gasObjects[MsgGrantAllowanceTypeUrl].perItemFee).times(1)); + const putPolicyTxFee = BigNumber(gasObjects[MsgPutPolicyTypeUrl].gasFee); + + return grantAllowTxFee.plus(putPolicyTxFee).toString(DECIMAL_NUMBER); + }, [gasObjects]); + const lockFee = useMemo(() => { if (!primarySp?.operatorAddress) return; const preLockFeeObject = preLockFeeObjects[primarySp.operatorAddress]; if (isEmpty(preLockFeeObject) || isChecking) { @@ -63,7 +68,7 @@ export const Fee = () => { const gasFee = isChecking ? -1 - : waitQueue.filter((item: WaitFile) => item.status !== 'ERROR').length * singleTxGasFee; + : BigNumber(waitQueue.filter((item: WaitFile) => item.status !== 'ERROR').length).times(singleTxGasFee).plus(BigNumber(createTmpAccountGasFee).toString(DECIMAL_NUMBER)).toString(DECIMAL_NUMBER); useEffect(() => { if (gasFee && lockFee) { diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx index 1d63e260..a8b6d0ec 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadObjects.tsx @@ -120,16 +120,18 @@ export const UploadObjects = memo(function UploadObjects() { if (file.name.includes('//')) { return E_OBJECT_NAME_CONTAINS_SLASH; } - const objectListNames = objectList.map((item) => item.name); - const uploadingNames = (uploadQueue?.[loginAccount] || []) + // Validation only works to data within the current path. + const objectListObjectNames = objectList.map((item) => bucketName + '/' + item.objectName); + // Avoid add same file to the uploading queue. + const uploadingObjectNames = (uploadQueue?.[loginAccount] || []) + .filter((item) => ['WAIT', 'HASH', 'READY', 'UPLOAD', 'SEAL'].includes(item.status)) .map((item) => { - const curPrefix = [bucketName, ...folders].join('/'); - const filePrefix = [item.bucketName, ...item.prefixFolders].join('/'); - return curPrefix === filePrefix ? item.file.name : ''; - }) - .filter((item) => item); - const isExistObjectList = objectListNames.includes(file.name); - const isExistUploadList = uploadingNames.includes(file.name); + return [item.bucketName, ...item.prefixFolders, item.file.name].join('/'); + }); + const fullObjectName = [path, file.name].join('/'); + const isExistObjectList = objectListObjectNames.includes(fullObjectName); + const isExistUploadList = uploadingObjectNames.includes(fullObjectName); + if (isExistObjectList || isExistUploadList) { return E_OBJECT_NAME_EXISTS; } @@ -207,7 +209,6 @@ export const UploadObjects = memo(function UploadObjects() { }, [preLockFeeObjects, selectedFiles]); const checkedQueue = selectedFiles.filter((item) => item.status === 'WAIT'); - // console.log(loading, creating, !checkedQueue?.length, !editUpload.isBalanceAvailable, editUpload); return ( diff --git a/apps/dcellar-web-ui/src/store/slices/global.ts b/apps/dcellar-web-ui/src/store/slices/global.ts index 8c92855d..cae2f20d 100644 --- a/apps/dcellar-web-ui/src/store/slices/global.ts +++ b/apps/dcellar-web-ui/src/store/slices/global.ts @@ -9,12 +9,14 @@ import { getSpOffChainData } from '@/store/slices/persist'; import { defaultBalance } from '@/store/slices/balance'; import Long from 'long'; import { VisibilityType } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/common'; +import { MsgGrantAllowanceTypeUrl } from '@bnb-chain/greenfield-chain-sdk'; export type TGasList = { [msgTypeUrl: string]: { gasLimit: number; msgTypeUrl: string; gasFee: number; + perItemFee: number; }; }; @@ -201,12 +203,20 @@ export const globalSlice = createSlice({ const { gasPrice } = state.gasHub; const gasObjects = keyBy( payload.msgGasParams.map((item) => { - const gasLimit = item.fixedType?.fixedGas.low || 0; - const gasFee = gasPrice * gasLimit; + let gasLimit = item.fixedType?.fixedGas.low || 0; + let gasFee = gasPrice * gasLimit; + let perItemFee = 0; + if (item.msgTypeUrl === MsgGrantAllowanceTypeUrl) { + gasLimit = item.grantAllowanceType?.fixedGas.low || 0; + gasFee = gasPrice * gasLimit; + perItemFee = (item.grantAllowanceType?.gasPerItem.low || 0) * gasPrice; + } + return { msgTypeUrl: item.msgTypeUrl, gasLimit, gasFee, + perItemFee, }; }), 'msgTypeUrl', diff --git a/apps/dcellar-web-ui/src/utils/common/index.tsx b/apps/dcellar-web-ui/src/utils/common/index.tsx new file mode 100644 index 00000000..7b20adbe --- /dev/null +++ b/apps/dcellar-web-ui/src/utils/common/index.tsx @@ -0,0 +1,17 @@ +export const parseErrorXml = async (result: Response) => { + try { + const xmlText = await result.text(); + const xml = await new window.DOMParser().parseFromString(xmlText, 'text/xml'); + const code = (xml as XMLDocument).getElementsByTagName('Code')[0].textContent; + const message = (xml as XMLDocument).getElementsByTagName('Message')[0].textContent; + return { + code, + message, + }; + } catch { + return { + code: null, + message: null, + }; + } +}; \ No newline at end of file From a56378c6f7023459137a8de19bff01233694e645 Mon Sep 17 00:00:00 2001 From: aidencao Date: Wed, 9 Aug 2023 21:12:03 +0800 Subject: [PATCH 17/20] feat(dcellar-web-ui): fix objectname regexp compile error --- apps/dcellar-web-ui/src/facade/account.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/dcellar-web-ui/src/facade/account.ts b/apps/dcellar-web-ui/src/facade/account.ts index 72345e8d..996db18a 100644 --- a/apps/dcellar-web-ui/src/facade/account.ts +++ b/apps/dcellar-web-ui/src/facade/account.ts @@ -15,6 +15,7 @@ import { ErrorResponse, broadcastFault } from './error'; import { UNKNOWN_ERROR } from '@/modules/file/constant'; import { TTmpAccount } from '@/store/slices/global'; import { signTypedDataV4 } from '@/utils/signDataV4'; +import { escapeRegExp } from 'lodash-es'; export type QueryBalanceRequest = { address: string; denom?: string }; type ActionType = 'delete' | 'create'; @@ -61,7 +62,8 @@ export const createTmpAccount = async ({ }); const resources = isDelete ? objectList.map((objectName: string) => { - return GRNToString(newObjectGRN(bucketName, objectName)); + // todo fix it escape + return GRNToString(newObjectGRN(bucketName, escapeRegExp(objectName))); }) : [GRNToString(newBucketGRN(bucketName))]; // 3. Put bucket policy so that the temporary account can create objects within this bucket @@ -70,6 +72,7 @@ export const createTmpAccount = async ({ actions: statementAction, resources: resources, }; + const putPolicyTx = await client.bucket.putBucketPolicy(bucketName, { operator: address, statements: [statement], From c5295d9b0d0c27d81e983c189eefe49569c14b0c Mon Sep 17 00:00:00 2001 From: aidencao Date: Thu, 10 Aug 2023 10:51:01 +0800 Subject: [PATCH 18/20] feat(dcellar-web-ui): bucket level statement --- apps/dcellar-web-ui/src/facade/account.ts | 7 +------ .../object/components/batch-delete/BatchDeleteObject.tsx | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/dcellar-web-ui/src/facade/account.ts b/apps/dcellar-web-ui/src/facade/account.ts index 996db18a..2e25c22f 100644 --- a/apps/dcellar-web-ui/src/facade/account.ts +++ b/apps/dcellar-web-ui/src/facade/account.ts @@ -15,7 +15,6 @@ import { ErrorResponse, broadcastFault } from './error'; import { UNKNOWN_ERROR } from '@/modules/file/constant'; import { TTmpAccount } from '@/store/slices/global'; import { signTypedDataV4 } from '@/utils/signDataV4'; -import { escapeRegExp } from 'lodash-es'; export type QueryBalanceRequest = { address: string; denom?: string }; type ActionType = 'delete' | 'create'; @@ -37,7 +36,6 @@ export const createTmpAccount = async ({ amount, connector, actionType, - objectList, }: any): Promise => { //messages and resources are different for create and delete const isDelete = actionType === 'delete'; @@ -61,10 +59,7 @@ export const createTmpAccount = async ({ denom: 'BNB', }); const resources = isDelete - ? objectList.map((objectName: string) => { - // todo fix it escape - return GRNToString(newObjectGRN(bucketName, escapeRegExp(objectName))); - }) + ? [GRNToString(newObjectGRN(bucketName, '*'))] : [GRNToString(newBucketGRN(bucketName))]; // 3. Put bucket policy so that the temporary account can create objects within this bucket const statement: PermissionTypes.Statement = { diff --git a/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx index bc307d08..e9e59c2b 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/batch-delete/BatchDeleteObject.tsx @@ -235,7 +235,6 @@ export const BatchDeleteObject = ({ refetch, isOpen, cancelFn }: modalProps) => amount: parseEther(round(Number(lockFee), 6).toString()).toString(), connector, actionType: 'delete', - objectList: selectedRowKeys, }); if (!tmpAccount) { return errorHandler(err); From 00d2dc5f59b0840d71845b83e38523723c978bba Mon Sep 17 00:00:00 2001 From: aidencao Date: Thu, 10 Aug 2023 17:08:48 +0800 Subject: [PATCH 19/20] feat(dcellar-web-ui): fix signTypedDataCallback params lost --- apps/dcellar-web-ui/src/facade/account.ts | 52 ++++++++++++----------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/apps/dcellar-web-ui/src/facade/account.ts b/apps/dcellar-web-ui/src/facade/account.ts index 2e25c22f..b82f8fc4 100644 --- a/apps/dcellar-web-ui/src/facade/account.ts +++ b/apps/dcellar-web-ui/src/facade/account.ts @@ -11,10 +11,10 @@ import { Coin } from '@bnb-chain/greenfield-cosmos-types/cosmos/base/v1beta1/coi import { Wallet } from 'ethers'; import { parseEther } from 'ethers/lib/utils.js'; import { resolve } from './common'; -import { ErrorResponse, broadcastFault } from './error'; +import { ErrorResponse, broadcastFault, simulateFault, createTxFault } from './error'; import { UNKNOWN_ERROR } from '@/modules/file/constant'; import { TTmpAccount } from '@/store/slices/global'; -import { signTypedDataV4 } from '@/utils/signDataV4'; +import { signTypedDataCallback } from '@/facade/wallet'; export type QueryBalanceRequest = { address: string; denom?: string }; type ActionType = 'delete' | 'create'; @@ -58,6 +58,7 @@ export const createTmpAccount = async ({ amount: parseEther(amount <= 0 ? '0.1' : amount).toString(), denom: 'BNB', }); + const resources = isDelete ? [GRNToString(newObjectGRN(bucketName, '*'))] : [GRNToString(newBucketGRN(bucketName))]; @@ -68,39 +69,40 @@ export const createTmpAccount = async ({ resources: resources, }; - const putPolicyTx = await client.bucket.putBucketPolicy(bucketName, { - operator: address, - statements: [statement], - principal: { - type: PermissionTypes.PrincipalType.PRINCIPAL_TYPE_GNFD_ACCOUNT, - value: wallet.address, - }, - }); + const [putPolicyTx, createTxError] = await client.bucket + .putBucketPolicy(bucketName, { + operator: address, + statements: [statement], + principal: { + type: PermissionTypes.PrincipalType.PRINCIPAL_TYPE_GNFD_ACCOUNT, + value: wallet.address, + }, + }) + .then(resolve, createTxFault); + + if (createTxError) return [null, createTxError]; // 4. broadcast txs include 2 msg - const txs = await client.basic.multiTx([grantAllowanceTx, putPolicyTx]); + const txs = await client.basic.multiTx([grantAllowanceTx, putPolicyTx!]); + + const [simulateInfo, simulateError] = await txs + .simulate({ + denom: 'BNB', + }) + .then(resolve, simulateFault); + + if (simulateError) return [null, simulateError]; - const simulateInfo = await txs.simulate({ - denom: 'BNB', - }); const payload = { denom: 'BNB', gasLimit: Number(210000), gasPrice: '5000000000', payer: address, granter: '', + signTypedDataCallback: signTypedDataCallback(connector), }; - const payloadParam = isDelete - ? { - ...payload, - signTypedDataCallback: async (addr: string, message: string) => { - const provider = await connector?.getProvider(); - return await signTypedDataV4(provider, addr, message); - }, - } - : payload; - console.log('payload', payload); - const [res, error] = await txs.broadcast(payloadParam).then(resolve, broadcastFault); + + const [res, error] = await txs.broadcast(payload).then(resolve, broadcastFault); if ((res && res.code !== 0) || error) { return [null, error || UNKNOWN_ERROR]; From 06f79f445d2e9ca82d04cb53d23e87c6fb7f4bfb Mon Sep 17 00:00:00 2001 From: aidencao Date: Thu, 10 Aug 2023 18:23:40 +0800 Subject: [PATCH 20/20] feat(dcellar-web-ui): fix checkbox ui --- .../src/components/common/DCTable/index.tsx | 16 +++++++++++++++ apps/dcellar-web-ui/src/facade/account.ts | 20 +++++++++++-------- .../modules/object/components/ObjectList.tsx | 2 +- .../dcellar-web-ui/src/store/slices/object.ts | 4 +++- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx b/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx index 824efabd..c287a154 100644 --- a/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx +++ b/apps/dcellar-web-ui/src/components/common/DCTable/index.tsx @@ -225,4 +225,20 @@ const Container = styled.div` .ant-checkbox-checked:after { display: none; } + .ant-checkbox-checked:not(.ant-checkbox-disabled):hover .ant-checkbox-inner { + background-color: #2ec659; + border-color: transparent; + } + .ant-checkbox-indeterminate .ant-checkbox-inner { + background-color: #00ba34; + border-color: #00ba34; + &:after { + background-color: #fff; + height: 2px; + } + } + .ant-checkbox-indeterminate:hover .ant-checkbox-inner { + background-color: #2ec659; + border-color: #2ec659; + } `; diff --git a/apps/dcellar-web-ui/src/facade/account.ts b/apps/dcellar-web-ui/src/facade/account.ts index b82f8fc4..38e043e4 100644 --- a/apps/dcellar-web-ui/src/facade/account.ts +++ b/apps/dcellar-web-ui/src/facade/account.ts @@ -51,13 +51,17 @@ export const createTmpAccount = async ({ // 2. allow temporary account to submit specified tx and amount const client = await getClient(); // MsgGrantAllowanceTypeUrl - const grantAllowanceTx = await client.feegrant.grantAllowance({ - granter: address, - grantee: wallet.address, - allowedMessages: grantAllowedMessage, - amount: parseEther(amount <= 0 ? '0.1' : amount).toString(), - denom: 'BNB', - }); + const [grantAllowanceTx, allowError] = await client.feegrant + .grantAllowance({ + granter: address, + grantee: wallet.address, + allowedMessages: grantAllowedMessage, + amount: parseEther(amount <= 0 ? '0.1' : amount).toString(), + denom: 'BNB', + }) + .then(resolve, createTxFault); + + if (allowError) return [null, allowError]; const resources = isDelete ? [GRNToString(newObjectGRN(bucketName, '*'))] @@ -83,7 +87,7 @@ export const createTmpAccount = async ({ if (createTxError) return [null, createTxError]; // 4. broadcast txs include 2 msg - const txs = await client.basic.multiTx([grantAllowanceTx, putPolicyTx!]); + const txs = await client.basic.multiTx([grantAllowanceTx!, putPolicyTx!]); const [simulateInfo, simulateError] = await txs .simulate({ diff --git a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx index e3f62e53..bd02e0c4 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx @@ -58,7 +58,7 @@ import { CreateFolder } from './CreateFolder'; import { useOffChainAuth } from '@/hooks/useOffChainAuth'; import { StyledRow } from '@/modules/object/objects.style'; import { UploadFile, selectUploadQueue } from '@/store/slices/global'; -import { copy, encodeObjectName, getShareLink } from '@/utils/string'; +import { copy, getShareLink } from '@/utils/string'; import { toast } from '@totejs/uikit'; const Actions: ActionMenuItem[] = [ diff --git a/apps/dcellar-web-ui/src/store/slices/object.ts b/apps/dcellar-web-ui/src/store/slices/object.ts index c1b19668..eddc312c 100644 --- a/apps/dcellar-web-ui/src/store/slices/object.ts +++ b/apps/dcellar-web-ui/src/store/slices/object.ts @@ -6,6 +6,7 @@ import { find, last, omit, trimEnd } from 'lodash-es'; import { IObjectResponse, TListObjects } from '@bnb-chain/greenfield-chain-sdk'; import { ErrorResponse } from '@/facade/error'; import { Key } from 'react'; +import { getMillisecond } from '@/utils/time'; export const SINGLE_OBJECT_MAX_SIZE = 128 * 1024 * 1024; export const SELECT_OBJECT_NUM_LIMIT = 10; @@ -238,6 +239,7 @@ export const objectSlice = createSlice({ objectName: object_name, name: last(object_name.split('/'))!, payloadSize: Number(payload_size), + // todo fix it *second* createAt: Number(create_at), contentType: content_type, folder: false, @@ -250,7 +252,7 @@ export const objectSlice = createSlice({ .filter((o) => { const path = [bucketName, o.objectName].join('/'); const ts = state.deletedObjects[path]; - return !ts || ts < o.createAt; + return !ts || ts < getMillisecond(o.createAt); }); state.objectsMeta[path] = omit(list, ['objects', 'common_prefixes']);