From 18d3301d6d3876765e881daf0cc75ea8dcb77d3e Mon Sep 17 00:00:00 2001 From: devinxl Date: Tue, 25 Jul 2023 14:13:10 +0800 Subject: [PATCH] feat(dcellar-web-ui): add basic batch upload --- apps/dcellar-web-ui/package.json | 4 +- .../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 | 2 + .../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 | 165 +++++++---- .../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 | 55 +++- 25 files changed, 1054 insertions(+), 417 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 c5188fa3..a9197e49 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-20230713041344", + "@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", @@ -26,7 +26,7 @@ "@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 d6a1ce1f..600509e2 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 cd70d662..3d9b35f4 100644 --- a/apps/dcellar-web-ui/src/facade/object.ts +++ b/apps/dcellar-web-ui/src/facade/object.ts @@ -9,6 +9,7 @@ import { E_UNKNOWN, ErrorMsg, ErrorResponse, + queryLockFeeFault, simulateFault, } from '@/facade/error'; import { getObjectInfoAndBucketQuota, resolve } from '@/facade/common'; @@ -33,6 +34,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>; @@ -259,8 +261,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 64cf36ca..766ab24d 100644 --- a/apps/dcellar-web-ui/src/modules/file/constant.ts +++ b/apps/dcellar-web-ui/src/modules/file/constant.ts @@ -18,6 +18,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 FILE_TITLE_CANCELING = 'Canceling Uploading'; @@ -120,4 +121,5 @@ export { UNKNOWN_ERROR_URL, FILE_UPLOAD_STATIC_URL, OBJECT_TITLE_CREATING, + 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 eca85ad5..de532dca 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/DeleteObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/DeleteObject.tsx @@ -88,8 +88,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 e33f9421..5d79148f 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 invalidPath = folders.some((name) => new Blob([name]).size > MAX_FOLDER_NAME_LEN); @@ -42,6 +35,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 5c882fdf..a2ca9525 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -10,7 +10,7 @@ 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-20230713041344 + '@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 @@ -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,7 +62,7 @@ importers: wagmi: ^0.12.9 dependencies: '@babel/core': 7.22.5 - '@bnb-chain/greenfield-chain-sdk': 0.0.0-snapshot-20230713041344 + '@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 @@ -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-20230713041344: - resolution: {integrity: sha512-j13H0V2mQg/dve03eLyWmlK0BZXkxPfqJ/w4ZovxdKo0ckgZqg/wqieAmUVpTFGDYd8vB7cD7h++tjAO/Z+ZlQ==} + /@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 @@ -1637,6 +1637,7 @@ packages: '@ethersproject/strings': 5.7.0 '@ethersproject/units': 5.7.0 '@metamask/eth-sig-util': 5.1.0 + cross-fetch: 4.0.0 dayjs: 1.11.8 ethereum-cryptography: 2.0.0 long: 5.2.3 @@ -1645,6 +1646,7 @@ packages: transitivePeerDependencies: - bufferutil - debug + - encoding - utf-8-validate dev: false @@ -3716,6 +3718,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: @@ -3745,8 +3762,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' @@ -3756,7 +3773,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 @@ -5460,6 +5477,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'} @@ -7707,6 +7732,18 @@ packages: whatwg-url: 5.0.0 dev: false + /node-fetch/2.6.12: + resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-gyp-build/4.6.0: resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} hasBin: true