From 8d1658f645bd34b8478b37f75eed91a252871273 Mon Sep 17 00:00:00 2001 From: aidencao Date: Tue, 23 Apr 2024 10:50:23 +0800 Subject: [PATCH 1/7] feat(dcellar-web-ui): fix spselector styles --- .../src/modules/bucket/components/SPSelector/OptionItem.tsx | 2 +- .../src/modules/bucket/components/SPSelector/index.tsx | 2 +- .../src/modules/bucket/components/SPSelector/style.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/OptionItem.tsx b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/OptionItem.tsx index ff44facf..02c7c4cb 100644 --- a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/OptionItem.tsx +++ b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/OptionItem.tsx @@ -74,7 +74,7 @@ export const OptionItem = memo(function OptionItem({ {meta && meta.FreeReadQuota ? formatBytes(meta.FreeReadQuota) : '--'} - + {meta && meta.MonthlyFreeQuota ? formatBytes(meta.MonthlyFreeQuota) : '--'} diff --git a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/index.tsx b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/index.tsx index 6b4c8f9c..2b2dcc2f 100644 --- a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/index.tsx +++ b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/index.tsx @@ -136,7 +136,7 @@ export const SPSelector = memo(function SPSelector({ onChange } <> SP list ({total}) Free Quota - Free Monthly Quota + Free Monthly Quota Latency )} diff --git a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/style.ts b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/style.ts index 313ad87e..dc96356c 100644 --- a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/style.ts +++ b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/style.ts @@ -33,6 +33,7 @@ export const TD = styled(Box, transientOptions)<{ $dot?: number }>` position: relative; font-size: 14px; font-weight: 400; + flex-shrink: 0; ${(props) => props.$dot && From 29420e70a201cc131d7dcd38a536e079311ca408 Mon Sep 17 00:00:00 2001 From: aidencao Date: Tue, 23 Apr 2024 17:38:47 +0800 Subject: [PATCH 2/7] feat(dcellar-web-ui): add object version history --- .../components/common/DCTable/ListEmpty.tsx | 8 +- apps/dcellar-web-ui/src/facade/object.ts | 9 + .../src/hooks/useHandleFolderTree.ts | 1 + .../components/DetailObjectOperation.tsx | 166 +++++++++++------- .../object/components/VersionTable.tsx | 99 +++++++++++ .../src/modules/upload/ListItem.tsx | 5 +- .../src/modules/upload/NameItem.tsx | 6 +- .../src/modules/upload/UploadObjectsFees.tsx | 3 +- .../src/modules/upload/UploadObjectsList.tsx | 65 ++++++- .../modules/upload/UploadObjectsOperation.tsx | 8 +- .../modules/upload/UploadingObjectsList.tsx | 9 +- .../src/modules/upload/useUploadTab.tsx | 5 +- .../src/pages/api/versions/[[...slug]].ts | 20 +++ .../dcellar-web-ui/src/store/slices/global.ts | 13 +- .../dcellar-web-ui/src/store/slices/object.ts | 29 ++- apps/dcellar-web-ui/src/utils/object/index.ts | 27 ++- 16 files changed, 382 insertions(+), 91 deletions(-) create mode 100644 apps/dcellar-web-ui/src/modules/object/components/VersionTable.tsx create mode 100644 apps/dcellar-web-ui/src/pages/api/versions/[[...slug]].ts diff --git a/apps/dcellar-web-ui/src/components/common/DCTable/ListEmpty.tsx b/apps/dcellar-web-ui/src/components/common/DCTable/ListEmpty.tsx index ef371c67..3f2d77c9 100644 --- a/apps/dcellar-web-ui/src/components/common/DCTable/ListEmpty.tsx +++ b/apps/dcellar-web-ui/src/components/common/DCTable/ListEmpty.tsx @@ -23,7 +23,13 @@ export const ListEmpty = memo(function ListEmpty({ {empty && ( - + >; @@ -638,6 +639,14 @@ export const getObjectMeta = async ( ); }; +export const getObjectVersions = async (id: string): Promise => { + const [result] = await axios + .get<{ result: ObjectVersion[] }>(`/api/versions/${id}`) + .then(resolve, commonFault); + if (!result) return []; + return result.data.result || []; +}; + export type UpdateObjectTagsParams = { address: string; bucketName: string; diff --git a/apps/dcellar-web-ui/src/hooks/useHandleFolderTree.ts b/apps/dcellar-web-ui/src/hooks/useHandleFolderTree.ts index 288a1277..fba79d43 100644 --- a/apps/dcellar-web-ui/src/hooks/useHandleFolderTree.ts +++ b/apps/dcellar-web-ui/src/hooks/useHandleFolderTree.ts @@ -169,6 +169,7 @@ export const useHandleFolderTree = () => { size: file.size, relativePath: relativePath, lockFee: '', + isUpdate: false, }; return waitObject; diff --git a/apps/dcellar-web-ui/src/modules/object/components/DetailObjectOperation.tsx b/apps/dcellar-web-ui/src/modules/object/components/DetailObjectOperation.tsx index 8af5e16e..9438a0cf 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/DetailObjectOperation.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/DetailObjectOperation.tsx @@ -15,7 +15,12 @@ import { EMPTY_TX_HASH } from '@/modules/object/constant'; import { useAppDispatch, useAppSelector } from '@/store'; import { AccountInfo } from '@/store/slices/accounts'; import { TBucket, setBucketQuota } from '@/store/slices/bucket'; -import { ObjectActionType, setObjectEditTagsData, setObjectOperation } from '@/store/slices/object'; +import { + ObjectActionType, + setObjectEditTagsData, + setObjectOperation, + setupObjectVersion, +} from '@/store/slices/object'; import { getSpOffChainData } from '@/store/slices/persist'; import { SpEntity } from '@/store/slices/sp'; import { convertObjectKey } from '@/utils/common'; @@ -25,12 +30,30 @@ import { formatFullTime } from '@/utils/time'; import { VisibilityType } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/common'; import { ResourceTags_Tag } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/types'; import { ObjectMeta } from '@bnb-chain/greenfield-js-sdk/dist/esm/types/sp/Common'; -import { Divider, Flex, QDrawerBody, QDrawerFooter, QDrawerHeader, Text } from '@node-real/uikit'; -import { useUnmount } from 'ahooks'; +import { + Divider, + Flex, + Loading, + QDrawerBody, + QDrawerFooter, + QDrawerHeader, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + Text, +} from '@node-real/uikit'; +import { useMount, useUnmount } from 'ahooks'; import { last } from 'lodash-es'; import { memo, useState } from 'react'; import { OBJECT_ERROR_TYPES, ObjectErrorType } from '../ObjectError'; import { setSignatureAction } from '@/store/slices/global'; +import { ListEmpty } from '@/components/common/DCTable/ListEmpty'; +import { TD, TH } from '@/modules/bucket/components/SPSelector/style'; +import { VersionTable } from '@/modules/object/components/VersionTable'; + +const VERSION_TABS = ['General Info', 'Versions']; interface DetailObjectOperationProps { selectObjectInfo: ObjectMeta; @@ -43,6 +66,7 @@ export const DetailObjectOperation = memo( function DetailOperation(props) { const { selectObjectInfo, selectBucket, bucketAccountDetail, primarySp } = props; const dispatch = useAppDispatch(); + const objectVersionRecords = useAppSelector((root) => root.object.objectVersionRecords); const accountRecords = useAppSelector((root) => root.persist.accountRecords); const loginAccount = useAppSelector((root) => root.persist.loginAccount); const currentBucketName = useAppSelector((root) => root.object.currentBucketName); @@ -54,6 +78,9 @@ export const DetailObjectOperation = memo( const { directDownload: allowDirectDownload } = accountRecords?.[loginAccount] || {}; const objectInfo = selectObjectInfo.ObjectInfo; const name = last(objectInfo.ObjectName.split('/')); + const versionKey = [currentBucketName, objectInfo.ObjectName].join('/'); + const loading = !(versionKey in objectVersionRecords); + const objectVersions = objectVersionRecords[versionKey]; const errorHandler = (type: string) => { setAction(''); @@ -121,6 +148,10 @@ export const DetailObjectOperation = memo( ); }; + useMount(() => { + dispatch(setupObjectVersion(objectInfo.ObjectName, objectInfo.Id)); + }); + useUnmount(() => dispatch(setObjectEditTagsData([DEFAULT_TAG]))); return ( @@ -154,63 +185,78 @@ export const DetailObjectOperation = memo( - - - {renderPropRow('Date created', formatFullTime(+objectInfo.CreateAt * 1000))} - {renderAddressLink( - 'Object ID', - formatId(Number(objectInfo.Id)), - 'dc.file.f_detail_pop.id.click', - 'dc.file.f_detail_pop.copy_id.click', - 'object', - )} - {renderAddressLink( - 'Primary SP address', - primarySp.operatorAddress, - 'dc.file.f_detail_pop.spadd.click', - 'dc.file.f_detail_pop.copy_spadd.click', - )} - {renderAddressLink( - 'Payment address', - selectBucket.PaymentAddress, - 'dc.file.f_detail_pop.seal.click', - 'dc.file.f_detail_pop.copy_seal.click', - )} - {renderAddressLink( - 'Create transaction hash', - selectObjectInfo.CreateTxHash, - 'dc.object.f_detail_pop.CreateTxHash.click', - 'dc.object.f_detail_pop.copy_create_tx_hash.click', - 'tx', - )} - {selectObjectInfo.SealTxHash !== EMPTY_TX_HASH && - renderAddressLink( - 'Seal transaction hash', - selectObjectInfo.SealTxHash, - 'dc.object.f_detail_pop.SealTxHash.click', - 'dc.object.f_detail_pop.copy_seal_tx_hash.click', - 'tx', - )} - {objectInfo.Visibility === VisibilityType.VISIBILITY_TYPE_PUBLIC_READ && - renderPropRow( - 'Universal link', - renderUrlWithLink( - `${primarySp.endpoint}/view/${currentBucketName}/${encodeObjectName( - objectInfo.ObjectName, - )}`, - true, - 32, - 'dc.file.f_detail_pop.universal.click', - 'dc.file.f_detail_pop.copy_universal.click', - ), - )} - {renderTags({ - onClick: onEditTags, - tagsCount: selectObjectInfo.ObjectInfo?.Tags.Tags.length || 0, - })} - - - + + + {VERSION_TABS.map((tab) => ( + + {tab} + + ))} + + + + + {renderPropRow('Date created', formatFullTime(+objectInfo.CreateAt * 1000))} + {renderAddressLink( + 'Object ID', + formatId(Number(objectInfo.Id)), + 'dc.file.f_detail_pop.id.click', + 'dc.file.f_detail_pop.copy_id.click', + 'object', + )} + {renderAddressLink( + 'Primary SP address', + primarySp.operatorAddress, + 'dc.file.f_detail_pop.spadd.click', + 'dc.file.f_detail_pop.copy_spadd.click', + )} + {renderAddressLink( + 'Payment address', + selectBucket.PaymentAddress, + 'dc.file.f_detail_pop.seal.click', + 'dc.file.f_detail_pop.copy_seal.click', + )} + {renderAddressLink( + 'Create transaction hash', + selectObjectInfo.CreateTxHash, + 'dc.object.f_detail_pop.CreateTxHash.click', + 'dc.object.f_detail_pop.copy_create_tx_hash.click', + 'tx', + )} + {selectObjectInfo.SealTxHash !== EMPTY_TX_HASH && + renderAddressLink( + 'Seal transaction hash', + selectObjectInfo.SealTxHash, + 'dc.object.f_detail_pop.SealTxHash.click', + 'dc.object.f_detail_pop.copy_seal_tx_hash.click', + 'tx', + )} + {objectInfo.Visibility === VisibilityType.VISIBILITY_TYPE_PUBLIC_READ && + renderPropRow( + 'Universal link', + renderUrlWithLink( + `${primarySp.endpoint}/view/${currentBucketName}/${encodeObjectName( + objectInfo.ObjectName, + )}`, + true, + 32, + 'dc.file.f_detail_pop.universal.click', + 'dc.file.f_detail_pop.copy_universal.click', + ), + )} + {renderTags({ + onClick: onEditTags, + tagsCount: selectObjectInfo.ObjectInfo?.Tags.Tags.length || 0, + })} + + + + + + + + + {objectInfo.ObjectStatus === 1 && isBucketOwner && ( diff --git a/apps/dcellar-web-ui/src/modules/object/components/VersionTable.tsx b/apps/dcellar-web-ui/src/modules/object/components/VersionTable.tsx new file mode 100644 index 00000000..483a3645 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/object/components/VersionTable.tsx @@ -0,0 +1,99 @@ +import React, { memo } from 'react'; +import { ObjectVersion } from '@/store/slices/object'; +import { Box, Link, Loading } from '@node-real/uikit'; +import { ListEmpty } from '@/components/common/DCTable/ListEmpty'; +import styled from '@emotion/styled'; +import dayjs from 'dayjs'; +import { GREENFIELD_CHAIN_EXPLORER_URL } from '@/base/env'; +import { trimAddress } from '@/utils/string'; +import { CopyText } from '@/components/common/CopyText'; + +interface VersionTableProps { + loading: boolean; + versions: ObjectVersion[]; +} + +export const VersionTable = memo(function VersionTable({ + loading, + versions = [], +}) { + if (loading) return ; + + if (!versions.length) + return ( + + ); + + return ( + + + Version + Date + Transaction + + {versions.map((version, index) => ( + + {version.Version} + {dayjs(version.ContentUpdatedAt * 1000).format('MMM D, YYYY HH:mm:ss A')} + + + + {trimAddress(version.TxHash, 28, 6, 5)} + + + + + ))} + + ); +}); + +const TR = styled(Box)` + display: flex; + align-items: center; + + &:not(:last-child) { + border-bottom: 1px solid var(--ui-colors-readable-border); + } +`; + +const TD = styled(Box)` + padding: 8px 12px; + font-size: 14px; + + &:nth-of-type(1) { + width: 100px; + } + + &:nth-of-type(2) { + width: 240px; + } + + &:nth-of-type(3) { + flex: 1; + } +`; + +const TH = styled(TD)` + font-size: 12px; + font-weight: 500; + background: var(--ui-colors-bg-bottom); +`; diff --git a/apps/dcellar-web-ui/src/modules/upload/ListItem.tsx b/apps/dcellar-web-ui/src/modules/upload/ListItem.tsx index 957fba0e..11d174b9 100644 --- a/apps/dcellar-web-ui/src/modules/upload/ListItem.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/ListItem.tsx @@ -9,6 +9,7 @@ import { UploadMenuList } from '@/modules/object/components/UploadMenuList'; import { useAppSelector } from '@/store'; import { TransferItemTree } from '@/utils/dom'; import { UploadObjectsList } from './UploadObjectsList'; +import { errorUploadFilterFn, waitUploadFilterFn } from '@/utils/object'; type ListItemProps = { path: string; @@ -23,9 +24,9 @@ export const ListItem = ({ path, type, handleFolderTree }: ListItemProps) => { case 'ALL': return objectWaitQueue; case 'WAIT': - return objectWaitQueue.filter((file) => file.status === 'WAIT'); + return objectWaitQueue.filter(waitUploadFilterFn); case 'ERROR': - return objectWaitQueue.filter((file) => file.status === 'ERROR'); + return objectWaitQueue.filter(errorUploadFilterFn); default: return objectWaitQueue; } diff --git a/apps/dcellar-web-ui/src/modules/upload/NameItem.tsx b/apps/dcellar-web-ui/src/modules/upload/NameItem.tsx index 6f38f120..fb9e15bf 100644 --- a/apps/dcellar-web-ui/src/modules/upload/NameItem.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/NameItem.tsx @@ -9,11 +9,12 @@ import { UploadObject, setTaskManagement } from '@/store/slices/global'; import { setObjectOperation } from '@/store/slices/object'; import { formatBytes } from '@/utils/formatter'; import { encodeObjectName } from '@/utils/string'; +import { ReactNode } from 'react'; type Props = { name: string; size: number; - msg?: string; + msg?: ReactNode; status?: string; task?: UploadObject; [key: string]: any; @@ -65,12 +66,11 @@ export const NameItem = ({ name, size, msg, status, task, ...styleProps }: Props return ( - + (function Fees({ delegateUpload, return '-1'; } return objectWaitQueue - .filter((item) => item.status !== 'ERROR') + .filter((item) => item.status !== 'ERROR' || isUploadObjectUpdate(item)) .reduce( (sum, obj) => sum.plus( diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadObjectsList.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadObjectsList.tsx index 48e78315..1c33c236 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadObjectsList.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadObjectsList.tsx @@ -1,6 +1,6 @@ import { DCTable } from '@/components/common/DCTable'; import { useAppDispatch } from '@/store'; -import { UploadObject, WaitObject, removeFromWaitQueue } from '@/store/slices/global'; +import { WaitObject, removeFromWaitQueue, toggleObjectReplaceState } from '@/store/slices/global'; import { ColumnProps } from 'antd/es/table'; import React, { useState } from 'react'; import { NameItem } from './NameItem'; @@ -8,31 +8,49 @@ import { PathItem } from './PathItem'; import { useCreation } from 'ahooks'; import { chunk } from 'lodash-es'; import { IconFont } from '@/components/IconFont'; +import { DCButton } from '@/components/common/DCButton'; +import { Text } from '@node-real/uikit'; +import { E_OBJECT_NAME_EXISTS } from '@/facade/error'; +import { getObjectErrorMsg } from '@/utils/object'; +import { DELEGATE_UPLOAD } from '@/store/slices/object'; const uploadingPageSize = 10; export const UploadObjectsList = ({ path, data }: { path: string; data: WaitObject[] }) => { const dispatch = useAppDispatch(); - const [pageSize, setPageSize] = useState(10); + const [pageSize] = useState(10); const [curPage, setCurPage] = useState(1); const chunks = useCreation(() => chunk(data, pageSize), [data, pageSize]); const page = chunks[curPage - 1] || []; + const onRemove = (id: number) => { dispatch(removeFromWaitQueue({ id })); }; - const columns: ColumnProps[] = [ + + const updateObjectReplaceState = (id: number) => { + dispatch(toggleObjectReplaceState({ id })); + }; + + const columns: ColumnProps[] = [ { key: 'name', title: 'Name', - render: (record) => { + render: (_, record) => { return ( + Replace the existing object + + ) : ( + record.msg + ) + } + w={222} /> ); }, @@ -44,6 +62,7 @@ export const UploadObjectsList = ({ path, data }: { path: string; data: WaitObje render: (record) => { return ( , + width: 64, + render: (_: WaitObject, record: WaitObject) => { + const replaceable = + !record.name.endsWith('/') && + record.msg === getObjectErrorMsg(E_OBJECT_NAME_EXISTS).title; + + if (!replaceable) return null; + return ( + updateObjectReplaceState(record.id)} + > + {record.isUpdate ? 'Undo' : 'Replace'} + + ); + }, + }, + ] + : []), { key: 'status', - title: 'Status', - width: 70, + title: <>, + width: 50, render: (record) => { return ( ( const loading = useMemo(() => { return selectedFiles.some((item) => item.status === 'CHECK') || isEmpty(storeFeeParams); }, [storeFeeParams, selectedFiles]); - const checkedQueue = selectedFiles.filter((item) => item.status === 'WAIT'); + + const checkedQueue = selectedFiles.filter(waitUploadFilterFn); const cleanup = () => { onClose(); @@ -127,7 +129,7 @@ export const UploadObjectsOperation = memo( }; const onUploadClick = async () => { - const validFiles = selectedFiles.filter((item) => item.status === 'WAIT'); + const validFiles = selectedFiles.filter(waitUploadFilterFn); const isOneFile = validFiles.length === 1; if (isEmpty(validFiles)) { return errorHandler('No valid files to upload.'); @@ -339,7 +341,7 @@ export const UploadObjectsOperation = memo( {formatBytes( checkedQueue - .filter((item) => item.status === 'WAIT') + .filter(waitUploadFilterFn) .reduce((accumulator, currentValue) => accumulator + currentValue.size, 0), )} {' '} diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx index 9f80d440..c1bed8a9 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx @@ -12,7 +12,7 @@ import { Flex } from '@node-real/uikit'; const uploadingPageSize = 10; export const UploadingObjectsList = ({ data }: { data: UploadObject[] }) => { - const [pageSize, setPageSize] = useState(10); + const [pageSize] = useState(10); const [curPage, setCurPage] = useState(1); const chunks = useCreation(() => chunk(data, pageSize), [data, pageSize]); const page = chunks[curPage - 1] || []; @@ -21,7 +21,7 @@ export const UploadingObjectsList = ({ data }: { data: UploadObject[] }) => { { key: 'name', title: 'Name', - render: (record) => { + render: (_, record) => { return ( { key: 'path', title: 'Path', width: 170, - render: (record) => { + render: (_, record) => { return ( { key: 'status', title: 'Status', width: 100, - render: (record) => { + render: (_, record) => { return ( @@ -69,6 +69,7 @@ export const UploadingObjectsList = ({ data }: { data: UploadObject[] }) => { return ( { const { allLen, waitLen, errorLen } = useMemo(() => { const allLen = objectWaitQueue.length; - const waitLen = objectWaitQueue.filter((item) => item.status === 'WAIT').length; - const errorLen = objectWaitQueue.filter((item) => item.status === 'ERROR').length; + const waitLen = objectWaitQueue.filter(waitUploadFilterFn).length; + const errorLen = objectWaitQueue.filter(errorUploadFilterFn).length; return { allLen, waitLen, diff --git a/apps/dcellar-web-ui/src/pages/api/versions/[[...slug]].ts b/apps/dcellar-web-ui/src/pages/api/versions/[[...slug]].ts new file mode 100644 index 00000000..2270e3b2 --- /dev/null +++ b/apps/dcellar-web-ui/src/pages/api/versions/[[...slug]].ts @@ -0,0 +1,20 @@ +import { EXPLORER_API_URL } from '@/base/env'; +import axios from 'axios'; +import { NextApiRequest, NextApiResponse } from 'next'; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const { slug } = req.query; + const slugs = slug as string[]; + const url = `${EXPLORER_API_URL}/greenfield/storage/object/version/list/by_object/${slugs.join( + '/', + )}?page=1&per_page=1000`; + try { + const { data } = await axios.get(url); + res.json(data); + } catch (e) { + console.error('explorer chart error', e); + res.json({}); + } +}; + +export default handler; diff --git a/apps/dcellar-web-ui/src/store/slices/global.ts b/apps/dcellar-web-ui/src/store/slices/global.ts index 60676f63..309c1764 100644 --- a/apps/dcellar-web-ui/src/store/slices/global.ts +++ b/apps/dcellar-web-ui/src/store/slices/global.ts @@ -12,6 +12,7 @@ import { getStoreFeeParams } from '@/facade/payment'; import { AppDispatch, AppState, GetState } from '@/store'; import { setObjectStatus, setupListObjects } from '@/store/slices/object'; import { getSpOffChainData } from '@/store/slices/persist'; +import { waitUploadFilterFn } from '@/utils/object'; export const GAS_PRICE = '0.000000005'; export const BNB_USDT_EXCHANGE_RATE = '350'; @@ -82,6 +83,7 @@ export type WaitObject = { name: string; lockFee?: string; relativePath: string; + isUpdate?: boolean; }; export type UploadObject = { @@ -117,6 +119,7 @@ export interface GlobalState { walletConnected: boolean; signatureAction: SignatureAction | object; } + const defaultStoreFeeParams: StoreFeeParams = { readPrice: '0', primarySpStorePrice: '0', @@ -238,7 +241,6 @@ export const globalSlice = createSlice({ resetWaitQueue(state) { state.objectWaitQueue = []; }, - removeFromWaitQueue(state, { payload }: PayloadAction<{ id: number }>) { // 1. When deleting a file, check if the parent folder is empty, delete it and recursively delete empty parent folders // 2. When deleting a folder, delete all subfolders and subfiles first; And like delete a file to recursively delete empty parent folders. @@ -287,6 +289,12 @@ export const globalSlice = createSlice({ deleteParent(deleteObject); state.objectWaitQueue = state.objectWaitQueue.filter((task) => !new Set(ids).has(task.id)); }, + toggleObjectReplaceState(state, { payload }: PayloadAction<{ id: number }>) { + const { id } = payload; + const task = find(state.objectWaitQueue, (t) => t.id === id); + if (!task) return; + task.isUpdate = !task.isUpdate; + }, addToUploadQueue( state, { payload }: PayloadAction<{ account: string; tasks: UploadObject[] }>, @@ -414,6 +422,7 @@ export const { updateUploadProgress, setTaskManagement, removeFromWaitQueue, + toggleObjectReplaceState, resetWaitQueue, resetUploadQueue, // cancelUploadFolder, @@ -646,7 +655,7 @@ export const addDelegatedTasksToUploadQueue = const { objectWaitQueue } = getState().global; const { currentBucketName, pathSegments } = getState().object; const { loginAccount } = getState().persist; - const wQueue = objectWaitQueue.filter((t) => t.status === 'WAIT'); + const wQueue = objectWaitQueue.filter(waitUploadFilterFn); if (!wQueue || wQueue.length === 0) return; const newUploadQueue = wQueue.map((task) => { const uploadTask: UploadObject = { diff --git a/apps/dcellar-web-ui/src/store/slices/object.ts b/apps/dcellar-web-ui/src/store/slices/object.ts index 4cf2b00f..81868478 100644 --- a/apps/dcellar-web-ui/src/store/slices/object.ts +++ b/apps/dcellar-web-ui/src/store/slices/object.ts @@ -22,7 +22,7 @@ import { numberToHex } from 'viem'; import { DEFAULT_TAG } from '@/components/common/ManageTags'; import { getFolderPolicies, getObjectPolicies } from '@/facade/bucket'; import { ErrorResponse } from '@/facade/error'; -import { ListObjectsParams, getListObjects } from '@/facade/object'; +import { ListObjectsParams, getListObjects, getObjectVersions } from '@/facade/object'; import { AppDispatch, AppState, GetState } from '@/store'; import { convertObjectKey } from '@/utils/common'; import { getMillisecond } from '@/utils/time'; @@ -30,7 +30,7 @@ import { getMillisecond } from '@/utils/time'; export const DELEGATE_UPLOAD = true; export const SELF_UPLOAD_MAX_SIZE = 256 * 1024 * 1024; -export const DELEGATE_UPLOAD_MAX_SIZE = 1 * 1024 * 1024 * 1024; +export const DELEGATE_UPLOAD_MAX_SIZE = 1024 * 1024 * 1024; export const SELF_UPLOAD_MAX_COUNT = 100; export const DELEGATE_UPLOAD_MAX_COUNT = 500; @@ -88,6 +88,14 @@ export type ObjectEntity = { removed: boolean; }; +export type ObjectVersion = { + ContentUpdatedAt: number; + Height: number; + ObjectID: string; + TxHash: string; + Version: number; +}; + export interface ObjectState { currentBucketName: string; pathSegments: string[]; @@ -96,6 +104,7 @@ export interface ObjectState { objectListRecords: Record; objectListTruncated: Record; objectRecords: Record; + objectVersionRecords: Record; objectListPageRecords: Record; objectListPageRestored: boolean; objectSelectedKeys: Key[]; @@ -123,6 +132,7 @@ const initialState: ObjectState = { completeCommonPrefix: '', objectListRecords: {}, objectRecords: {}, + objectVersionRecords: {}, objectListPageRecords: {}, objectListPageRestored: true, objectSelectedKeys: [], @@ -373,6 +383,14 @@ export const objectSlice = createSlice({ } >['Tags']; }, + setObjectVersion( + state, + { payload }: PayloadAction<{ versions: ObjectVersion[]; objectName: string }>, + ) { + const { objectName, versions } = payload; + const key = [state.currentBucketName, objectName].join('/'); + state.objectVersionRecords[key] = versions; + }, }, }); @@ -403,6 +421,7 @@ export const { setObjectShareModePath, setObjectTags, setObjectEditTagsData, + setObjectVersion, } = objectSlice.actions; export const selectPathLoading = (root: AppState) => { @@ -569,4 +588,10 @@ export const setupObjectPolicies = return policies; }; +export const setupObjectVersion = + (objectName: string, id: number) => async (dispatch: AppDispatch) => { + const versions = await getObjectVersions(numberToHex(Number(id), { size: 32 })); + dispatch(setObjectVersion({ objectName, versions })); + }; + export default objectSlice.reducer; diff --git a/apps/dcellar-web-ui/src/utils/object/index.ts b/apps/dcellar-web-ui/src/utils/object/index.ts index 886e11c0..c6849f72 100644 --- a/apps/dcellar-web-ui/src/utils/object/index.ts +++ b/apps/dcellar-web-ui/src/utils/object/index.ts @@ -1,7 +1,7 @@ import { VisibilityType } from '@bnb-chain/greenfield-cosmos-types/greenfield/storage/common'; import { ObjectActionValueType } from '@/modules/object/components/ObjectList'; -import { UploadObject } from '@/store/slices/global'; +import { UploadObject, WaitObject } from '@/store/slices/global'; import { AuthType, DelegatedPubObjectRequest, @@ -12,7 +12,9 @@ import { makePutObjectHeaders, } from '@/modules/object/utils/getPutObjectHeaders'; import { resolve } from '@/facade/common'; -import { commonFault } from '@/facade/error'; +import { commonFault, E_OBJECT_NAME_EXISTS, E_UNKNOWN } from '@/facade/error'; +import { OBJECT_ERROR_TYPES, ObjectErrorType } from '@/modules/object/ObjectError'; +import { DELEGATE_UPLOAD } from '@/store/slices/object'; export type TKey = keyof typeof VisibilityType; export type TReverseVisibilityType = { @@ -76,9 +78,30 @@ export const getPutObjectRequestConfig = async ( objectName: fullObjectName, body: file, delegatedOpts: { + isUpdate: task.waitObject.isUpdate, visibility: task.visibility, }, }; return makeDelegatePutObjectHeaders(payload, authType, endpoint).then(resolve, commonFault); }; + +export const getObjectErrorMsg = (type: string) => { + return OBJECT_ERROR_TYPES[type as ObjectErrorType] + ? OBJECT_ERROR_TYPES[type as ObjectErrorType] + : OBJECT_ERROR_TYPES[E_UNKNOWN]; +}; + +export const isUploadObjectUpdate = (item: WaitObject) => { + return ( + item.msg === getObjectErrorMsg(E_OBJECT_NAME_EXISTS).title && DELEGATE_UPLOAD && item.isUpdate + ); +}; + +export const waitUploadFilterFn = (item: WaitObject) => { + return item.status === 'WAIT' || isUploadObjectUpdate(item); +}; + +export const errorUploadFilterFn = (item: WaitObject) => { + return item.status === 'ERROR' && !isUploadObjectUpdate(item); +}; From b2079d8627c61147b1b277752cb96fc7bcbc01b2 Mon Sep 17 00:00:00 2001 From: aidencao Date: Thu, 25 Apr 2024 12:48:26 +0800 Subject: [PATCH 3/7] feat(dcellar-web-ui): only sealed object can be updated --- .../GlobalManagements/GlobalObjectUploadManager.tsx | 9 ++++++++- apps/dcellar-web-ui/src/utils/object/index.ts | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx b/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx index 677454bc..3cb3a443 100644 --- a/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx +++ b/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx @@ -63,6 +63,7 @@ export const GlobalObjectUploadManager = memo( const objectSealingTimestamp = useAppSelector((root) => root.global.objectSealingTimestamp); const tempAccountRecords = useAppSelector((root) => root.accounts.tempAccountRecords); const bucketRecords = useAppSelector((root) => root.bucket.bucketRecords); + const objectRecords = useAppSelector((root) => root.object.objectRecords); const hashTask = useAppSelector(selectHashTask(loginAccount)); const signTask = useAppSelector(selectSignTask(loginAccount)); const queue = useAppSelector(selectUploadQueue(loginAccount)); @@ -92,15 +93,21 @@ export const GlobalObjectUploadManager = memo( const runUploadTask = async (task: UploadObject) => { if (authModal) return; - const isFolder = task.waitObject.name.endsWith('/'); + const name = task.waitObject.name; + const isFolder = name.endsWith('/'); const { seedString } = await dispatch(getSpOffChainData(loginAccount, task.spAddress)); const endpoint = spRecords[task.spAddress].endpoint; + const key = [task.bucketName, ...task.prefixFolders, task.waitObject.relativePath, name].join( + '/', + ); + const sealed = objectRecords[key]?.ObjectInfo.ObjectStatus === 1; const [uploadOptions, error1] = await getPutObjectRequestConfig( task, loginAccount, seedString, endpoint, task.waitObject.file, + sealed, ); if (!uploadOptions || error1) { return dispatch( diff --git a/apps/dcellar-web-ui/src/utils/object/index.ts b/apps/dcellar-web-ui/src/utils/object/index.ts index c6849f72..963d710b 100644 --- a/apps/dcellar-web-ui/src/utils/object/index.ts +++ b/apps/dcellar-web-ui/src/utils/object/index.ts @@ -51,6 +51,7 @@ export const getPutObjectRequestConfig = async ( seedString: string, endpoint: string, file: File, + sealed: boolean, ) => { const fullObjectName = [...task.prefixFolders, task.waitObject.relativePath, task.waitObject.name] .filter((item) => !!item) @@ -78,7 +79,7 @@ export const getPutObjectRequestConfig = async ( objectName: fullObjectName, body: file, delegatedOpts: { - isUpdate: task.waitObject.isUpdate, + isUpdate: task.waitObject.isUpdate && sealed, visibility: task.visibility, }, }; From 5917deafa03ee947f10f27cad6b95de0290ef1b8 Mon Sep 17 00:00:00 2001 From: aidencao Date: Thu, 25 Apr 2024 13:29:26 +0800 Subject: [PATCH 4/7] feat(dcellar-web-ui): fix empty path segment --- .../GlobalManagements/GlobalObjectUploadManager.tsx | 12 +++++++++--- .../src/modules/upload/UploadingObjectsList.tsx | 2 +- apps/dcellar-web-ui/src/utils/object/index.ts | 4 +--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx b/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx index 3cb3a443..c4a640df 100644 --- a/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx +++ b/apps/dcellar-web-ui/src/components/layout/GlobalManagements/GlobalObjectUploadManager.tsx @@ -97,9 +97,14 @@ export const GlobalObjectUploadManager = memo( const isFolder = name.endsWith('/'); const { seedString } = await dispatch(getSpOffChainData(loginAccount, task.spAddress)); const endpoint = spRecords[task.spAddress].endpoint; - const key = [task.bucketName, ...task.prefixFolders, task.waitObject.relativePath, name].join( - '/', - ); + const fullObjectName = [ + ...task.prefixFolders, + task.waitObject.relativePath, + task.waitObject.name, + ] + .filter(Boolean) + .join('/'); + const key = `${task.bucketName}/${fullObjectName}`; const sealed = objectRecords[key]?.ObjectInfo.ObjectStatus === 1; const [uploadOptions, error1] = await getPutObjectRequestConfig( task, @@ -108,6 +113,7 @@ export const GlobalObjectUploadManager = memo( endpoint, task.waitObject.file, sealed, + fullObjectName, ); if (!uploadOptions || error1) { return dispatch( diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx index c1bed8a9..e1ce7b89 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadingObjectsList.tsx @@ -28,7 +28,7 @@ export const UploadingObjectsList = ({ data }: { data: UploadObject[] }) => { size={record.waitObject.size} msg={record.msg} status={record.status} - w={240} + w={234} task={record} /> ); diff --git a/apps/dcellar-web-ui/src/utils/object/index.ts b/apps/dcellar-web-ui/src/utils/object/index.ts index 963d710b..57117243 100644 --- a/apps/dcellar-web-ui/src/utils/object/index.ts +++ b/apps/dcellar-web-ui/src/utils/object/index.ts @@ -52,10 +52,8 @@ export const getPutObjectRequestConfig = async ( endpoint: string, file: File, sealed: boolean, + fullObjectName: string, ) => { - const fullObjectName = [...task.prefixFolders, task.waitObject.relativePath, task.waitObject.name] - .filter((item) => !!item) - .join('/'); const authType = { type: 'EDDSA', seed: seedString, From 2eb7b7b1aff01bc73dd3cca900bc9cbfabf4b374 Mon Sep 17 00:00:00 2001 From: aidencao Date: Thu, 25 Apr 2024 14:13:17 +0800 Subject: [PATCH 5/7] feat(dcellar-web-ui): replace object update object meta --- apps/dcellar-web-ui/src/store/slices/global.ts | 2 ++ apps/dcellar-web-ui/src/store/slices/object.ts | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/dcellar-web-ui/src/store/slices/global.ts b/apps/dcellar-web-ui/src/store/slices/global.ts index 309c1764..b1f73049 100644 --- a/apps/dcellar-web-ui/src/store/slices/global.ts +++ b/apps/dcellar-web-ui/src/store/slices/global.ts @@ -559,6 +559,8 @@ export const uploadQueueAndRefresh = folders: task.prefixFolders, name: task.waitObject.name, objectStatus: 1, + contentType: task.waitObject.type, + payloadSize: task.waitObject.size, }), ); }); diff --git a/apps/dcellar-web-ui/src/store/slices/object.ts b/apps/dcellar-web-ui/src/store/slices/object.ts index 81868478..e52e0c75 100644 --- a/apps/dcellar-web-ui/src/store/slices/object.ts +++ b/apps/dcellar-web-ui/src/store/slices/object.ts @@ -267,19 +267,25 @@ export const objectSlice = createSlice({ folders: string[]; name: string; objectStatus: number; + contentType: string; + payloadSize: number; }>, ) { - const { name, folders, objectStatus, bucketName } = payload; + const { name, folders, objectStatus, bucketName, contentType, payloadSize } = payload; const path = [bucketName, ...folders].join('/'); const items = state.objectListRecords[path] || []; const objectName = [...folders, name].join('/'); const object = find(items, (i) => i.objectName === objectName); if (object) { object.objectStatus = objectStatus; + object.contentType = contentType; + object.payloadSize = payloadSize; } const info = state.objectRecords[[path, objectName].join('/')]; if (!info) return; info.ObjectInfo.ObjectStatus = objectStatus as any; // number + info.ObjectInfo.ContentType = contentType; + info.ObjectInfo.PayloadSize = payloadSize; }, setObjectListPageRestored(state, { payload }: PayloadAction) { state.objectListPageRestored = payload; From 0ca69e30ace60166d5674ac44ef1606a94d3065b Mon Sep 17 00:00:00 2001 From: aidencao Date: Thu, 25 Apr 2024 15:20:49 +0800 Subject: [PATCH 6/7] feat(dcellar-web-ui): create on chain object can only be replace original object --- .../src/modules/upload/UploadObjectsList.tsx | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/apps/dcellar-web-ui/src/modules/upload/UploadObjectsList.tsx b/apps/dcellar-web-ui/src/modules/upload/UploadObjectsList.tsx index 1c33c236..2b740afd 100644 --- a/apps/dcellar-web-ui/src/modules/upload/UploadObjectsList.tsx +++ b/apps/dcellar-web-ui/src/modules/upload/UploadObjectsList.tsx @@ -1,5 +1,5 @@ import { DCTable } from '@/components/common/DCTable'; -import { useAppDispatch } from '@/store'; +import { useAppDispatch, useAppSelector } from '@/store'; import { WaitObject, removeFromWaitQueue, toggleObjectReplaceState } from '@/store/slices/global'; import { ColumnProps } from 'antd/es/table'; import React, { useState } from 'react'; @@ -13,11 +13,13 @@ import { Text } from '@node-real/uikit'; import { E_OBJECT_NAME_EXISTS } from '@/facade/error'; import { getObjectErrorMsg } from '@/utils/object'; import { DELEGATE_UPLOAD } from '@/store/slices/object'; +import { DCTooltip } from '@/components/common/DCTooltip'; const uploadingPageSize = 10; export const UploadObjectsList = ({ path, data }: { path: string; data: WaitObject[] }) => { const dispatch = useAppDispatch(); + const objectRecords = useAppSelector((root) => root.object.objectRecords); const [pageSize] = useState(10); const [curPage, setCurPage] = useState(1); const chunks = useCreation(() => chunk(data, pageSize), [data, pageSize]); @@ -82,16 +84,33 @@ export const UploadObjectsList = ({ path, data }: { path: string; data: WaitObje record.msg === getObjectErrorMsg(E_OBJECT_NAME_EXISTS).title; if (!replaceable) return null; + + const prefix = `${path}/${record.relativePath ? record.relativePath + '/' : ''}`; + const createOnChainObject = objectRecords[`${prefix}${record.name}`]; + const objectChanged = + createOnChainObject && + createOnChainObject.ObjectInfo.ObjectStatus !== 1 && + createOnChainObject.ObjectInfo.PayloadSize !== record.size; + return ( - updateObjectReplaceState(record.id)} + - {record.isUpdate ? 'Undo' : 'Replace'} - + updateObjectReplaceState(record.id)} + > + {record.isUpdate ? 'Undo' : 'Replace'} + + ); }, }, From e922662e7c820e571a4ca3e55ce373ade0594510 Mon Sep 17 00:00:00 2001 From: aidencao Date: Mon, 29 Apr 2024 14:36:29 +0800 Subject: [PATCH 7/7] chore(dcellar-web-ui): add change log --- apps/dcellar-web-ui/CHANGELOG.json | 12 ++++++++++++ apps/dcellar-web-ui/CHANGELOG.md | 9 ++++++++- apps/dcellar-web-ui/package.json | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/apps/dcellar-web-ui/CHANGELOG.json b/apps/dcellar-web-ui/CHANGELOG.json index 62a27a2d..0ba83859 100644 --- a/apps/dcellar-web-ui/CHANGELOG.json +++ b/apps/dcellar-web-ui/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "dcellar-web-ui", "entries": [ + { + "version": "1.1.0", + "tag": "dcellar-web-ui_v1.1.0", + "date": "Mon, 29 Apr 2024 06:35:57 GMT", + "comments": { + "minor": [ + { + "comment": "Support object versions & replace object" + } + ] + } + }, { "version": "1.0.3", "tag": "dcellar-web-ui_v1.0.3", diff --git a/apps/dcellar-web-ui/CHANGELOG.md b/apps/dcellar-web-ui/CHANGELOG.md index ddb035cf..3b7d1e7e 100644 --- a/apps/dcellar-web-ui/CHANGELOG.md +++ b/apps/dcellar-web-ui/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - dcellar-web-ui -This log was last generated on Mon, 22 Apr 2024 03:46:28 GMT and should not be manually modified. +This log was last generated on Mon, 29 Apr 2024 06:35:57 GMT and should not be manually modified. + +## 1.1.0 +Mon, 29 Apr 2024 06:35:57 GMT + +### Minor changes + +- Support object versions & replace object ## 1.0.3 Mon, 22 Apr 2024 03:46:28 GMT diff --git a/apps/dcellar-web-ui/package.json b/apps/dcellar-web-ui/package.json index 966ba46e..2937cca4 100644 --- a/apps/dcellar-web-ui/package.json +++ b/apps/dcellar-web-ui/package.json @@ -1,6 +1,6 @@ { "name": "dcellar-web-ui", - "version": "1.0.3", + "version": "1.1.0", "private": false, "scripts": { "dev": "node ./scripts/dev.js -p 3200",