From b4614e9517547d1128d7cc2ee1d250f9a9821c97 Mon Sep 17 00:00:00 2001 From: balibabu Date: Wed, 15 Jan 2025 14:39:33 +0800 Subject: [PATCH] Feat: Add FilesTable #3221 (#4491) ### What problem does this PR solve? Feat: Add FilesTable #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/app.tsx | 13 +- web/src/components/list-filter-bar.tsx | 5 +- web/src/components/skeleton-card.tsx | 13 + web/src/components/table-skeleton.tsx | 27 ++ web/src/components/ui/input.tsx | 37 +- web/src/layouts/next-header.tsx | 3 +- web/src/pages/dataset/dataset/index.tsx | 2 +- .../datasets/dataset-creating-dialog.tsx | 2 +- .../pages/file-manager/action-cell/index.tsx | 15 +- web/src/pages/file-manager/file-toolbar.tsx | 11 +- web/src/pages/files/files-table.tsx | 343 ++++++++++++++++++ web/src/pages/files/hooks.ts | 294 +++++++++++++++ web/src/pages/files/index.tsx | 15 + web/src/pages/flow/canvas/index.tsx | 29 +- web/src/pages/flow/flow-tooltip.tsx | 15 +- .../pages/knowledge/knowledge-card/index.tsx | 2 +- web/src/pages/login-next/index.tsx | 4 +- web/src/routes.ts | 12 + web/src/utils/common-util.ts | 25 ++ web/typings.d.ts | 8 + 20 files changed, 824 insertions(+), 51 deletions(-) create mode 100644 web/src/components/skeleton-card.tsx create mode 100644 web/src/components/table-skeleton.tsx create mode 100644 web/src/pages/files/files-table.tsx create mode 100644 web/src/pages/files/hooks.ts create mode 100644 web/src/pages/files/index.tsx diff --git a/web/src/app.tsx b/web/src/app.tsx index bbb1d9d7b..1681fc3c4 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -15,6 +15,7 @@ import weekYear from 'dayjs/plugin/weekYear'; import weekday from 'dayjs/plugin/weekday'; import React, { ReactNode, useEffect, useState } from 'react'; import { ThemeProvider, useTheme } from './components/theme-provider'; +import { TooltipProvider } from './components/ui/tooltip'; import storage from './utils/authorization-util'; dayjs.extend(customParseFormat); @@ -78,11 +79,13 @@ const RootProvider = ({ children }: React.PropsWithChildren) => { }, []); return ( - - - {children} - - + + + + {children} + + + ); }; export function rootContainer(container: ReactNode) { diff --git a/web/src/components/list-filter-bar.tsx b/web/src/components/list-filter-bar.tsx index f1d2cf8ef..9732e5d77 100644 --- a/web/src/components/list-filter-bar.tsx +++ b/web/src/components/list-filter-bar.tsx @@ -1,6 +1,7 @@ -import { Filter, Search } from 'lucide-react'; +import { Filter } from 'lucide-react'; import { PropsWithChildren } from 'react'; import { Button } from './ui/button'; +import { SearchInput } from './ui/input'; interface IProps { title: string; @@ -17,7 +18,7 @@ export default function ListFilterBar({ {title}
- + diff --git a/web/src/components/skeleton-card.tsx b/web/src/components/skeleton-card.tsx new file mode 100644 index 000000000..76142c48e --- /dev/null +++ b/web/src/components/skeleton-card.tsx @@ -0,0 +1,13 @@ +import { Skeleton } from '@/components/ui/skeleton'; + +export function SkeletonCard() { + return ( +
+ +
+ + +
+
+ ); +} diff --git a/web/src/components/table-skeleton.tsx b/web/src/components/table-skeleton.tsx new file mode 100644 index 000000000..b3b74aa56 --- /dev/null +++ b/web/src/components/table-skeleton.tsx @@ -0,0 +1,27 @@ +import { PropsWithChildren } from 'react'; +import { SkeletonCard } from './skeleton-card'; +import { TableCell, TableRow } from './ui/table'; + +type IProps = { columnsLength: number }; + +function Row({ children, columnsLength }: PropsWithChildren & IProps) { + return ( + + + {children} + + + ); +} + +export function TableSkeleton({ columnsLength }: { columnsLength: number }) { + return ( + + + + ); +} + +export function TableEmpty({ columnsLength }: { columnsLength: number }) { + return No results.; +} diff --git a/web/src/components/ui/input.tsx b/web/src/components/ui/input.tsx index 90afc7339..0f558fb84 100644 --- a/web/src/components/ui/input.tsx +++ b/web/src/components/ui/input.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; +import { Search } from 'lucide-react'; export interface InputProps extends React.InputHTMLAttributes {} @@ -22,4 +23,38 @@ const Input = React.forwardRef( ); Input.displayName = 'Input'; -export { Input }; +export interface ExpandedInputProps extends Omit { + prefix?: React.ReactNode; + suffix?: React.ReactNode; +} + +const ExpandedInput = ({ suffix, prefix, ...props }: ExpandedInputProps) => { + return ( +
+ + {prefix} + + + + {suffix} + +
+ ); +}; + +const SearchInput = (props: InputProps) => { + return } {...props}>; +}; + +export { ExpandedInput, Input, SearchInput }; diff --git a/web/src/layouts/next-header.tsx b/web/src/layouts/next-header.tsx index ca934edbe..f0e2900ba 100644 --- a/web/src/layouts/next-header.tsx +++ b/web/src/layouts/next-header.tsx @@ -10,6 +10,7 @@ import { Routes } from '@/routes'; import { ChevronDown, Cpu, + File, Github, House, Library, @@ -33,7 +34,7 @@ export function Header() { { path: Routes.Chat, name: t('chat'), icon: MessageSquareText }, { path: Routes.Search, name: t('search'), icon: Search }, { path: Routes.Agent, name: t('flow'), icon: Cpu }, - // { path: '/file', name: t('fileManager'), icon: FileIcon }, + { path: Routes.Files, name: t('fileManager'), icon: File }, ], [t], ); diff --git a/web/src/pages/dataset/dataset/index.tsx b/web/src/pages/dataset/dataset/index.tsx index 99b3c3117..620fc04f1 100644 --- a/web/src/pages/dataset/dataset/index.tsx +++ b/web/src/pages/dataset/dataset/index.tsx @@ -13,7 +13,7 @@ export default function Dataset() { documentUploadLoading, } = useHandleUploadDocument(); return ( -
+
Upload file diff --git a/web/src/pages/datasets/dataset-creating-dialog.tsx b/web/src/pages/datasets/dataset-creating-dialog.tsx index 50b99f3ed..cf4a832bb 100644 --- a/web/src/pages/datasets/dataset-creating-dialog.tsx +++ b/web/src/pages/datasets/dataset-creating-dialog.tsx @@ -50,7 +50,7 @@ export function InputForm() {
- + )} {isKnowledgeBase || ( - )} diff --git a/web/src/pages/file-manager/file-toolbar.tsx b/web/src/pages/file-manager/file-toolbar.tsx index 387cc393b..8d65ec670 100644 --- a/web/src/pages/file-manager/file-toolbar.tsx +++ b/web/src/pages/file-manager/file-toolbar.tsx @@ -1,5 +1,3 @@ -import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg'; -import SvgIcon from '@/components/svg-icon'; import { useTranslate } from '@/hooks/common-hooks'; import { IListResult, @@ -29,6 +27,7 @@ import { useSelectBreadcrumbItems, } from './hooks'; +import { FolderInput, Trash2 } from 'lucide-react'; import styles from './index.less'; interface IProps @@ -127,8 +126,8 @@ const FileToolbar = ({ onClick: handleRemoveFile, label: ( - - + + {t('delete', { keyPrefix: 'common' })} @@ -139,8 +138,8 @@ const FileToolbar = ({ onClick: handleShowMoveFileModal, label: ( - - + + {t('move', { keyPrefix: 'common' })} diff --git a/web/src/pages/files/files-table.tsx b/web/src/pages/files/files-table.tsx new file mode 100644 index 000000000..400f0ba74 --- /dev/null +++ b/web/src/pages/files/files-table.tsx @@ -0,0 +1,343 @@ +'use client'; + +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { ArrowUpDown, MoreHorizontal, Pencil } from 'lucide-react'; +import * as React from 'react'; + +import SvgIcon from '@/components/svg-icon'; +import { TableEmpty, TableSkeleton } from '@/components/table-skeleton'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Switch } from '@/components/ui/switch'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { useFetchFileList } from '@/hooks/file-manager-hooks'; +import { IFile } from '@/interfaces/database/file-manager'; +import { cn } from '@/lib/utils'; +import { formatFileSize } from '@/utils/common-util'; +import { formatDate } from '@/utils/date'; +import { getExtension } from '@/utils/document-util'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigateToOtherFolder } from './hooks'; + +export function FilesTable() { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + const { t } = useTranslation('translation', { + keyPrefix: 'fileManager', + }); + const navigateToOtherFolder = useNavigateToOtherFolder(); + + const { pagination, data, loading, setPagination } = useFetchFileList(); + + const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: 'name', + header: ({ column }) => { + return ( + + ); + }, + meta: { cellClassName: 'max-w-[20vw]' }, + cell: ({ row }) => { + const name: string = row.getValue('name'); + const type = row.original.type; + const id = row.original.id; + const isFolder = type === 'folder'; + + const handleNameClick = () => { + if (isFolder) { + navigateToOtherFolder(id); + } + }; + + return ( + + +
+ + + {name} + +
+
+ +

{name}

+
+
+ ); + }, + }, + { + accessorKey: 'create_time', + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
+ {formatDate(row.getValue('create_time'))} +
+ ), + }, + { + accessorKey: 'size', + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
{formatFileSize(row.getValue('size'))}
+ ), + }, + { + accessorKey: 'kbs_info', + header: t('knowledgeBase'), + cell: ({ row }) => ( + + ), + }, + { + id: 'actions', + header: t('action'), + enableHiding: false, + cell: ({ row }) => { + const payment = row.original; + + return ( +
+ + + + + + + + Actions + navigator.clipboard.writeText(payment.id)} + > + Copy payment ID + + + View customer + View payment details + + +
+ ); + }, + }, + ]; + + const currentPagination = useMemo(() => { + return { + pageIndex: (pagination.current || 1) - 1, + pageSize: pagination.pageSize || 10, + }; + }, [pagination]); + + const table = useReactTable({ + data: data?.files || [], + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + // getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onPaginationChange: (updaterOrValue: any) => { + if (typeof updaterOrValue === 'function') { + const nextPagination = updaterOrValue(currentPagination); + setPagination({ + page: nextPagination.pageIndex + 1, + pageSize: nextPagination.pageSize, + }); + } else { + setPagination({ + page: updaterOrValue.pageIndex, + pageSize: updaterOrValue.pageSize, + }); + } + }, + manualPagination: true, //we're doing manual "server-side" pagination + + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + pagination: currentPagination, + }, + rowCount: data?.total ?? 0, + debugTable: true, + }); + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {loading ? ( + + ) : table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + )} + +
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of {data?.total}{' '} + row(s) selected. +
+
+ + +
+
+
+ ); +} diff --git a/web/src/pages/files/hooks.ts b/web/src/pages/files/hooks.ts new file mode 100644 index 000000000..d1145e572 --- /dev/null +++ b/web/src/pages/files/hooks.ts @@ -0,0 +1,294 @@ +import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks'; +import { + useConnectToKnowledge, + useCreateFolder, + useDeleteFile, + useFetchParentFolderList, + useMoveFile, + useRenameFile, + useUploadFile, +} from '@/hooks/file-manager-hooks'; +import { IFile } from '@/interfaces/database/file-manager'; +import { TableRowSelection } from 'antd/es/table/interface'; +import { UploadFile } from 'antd/lib'; +import { useCallback, useMemo, useState } from 'react'; +import { useNavigate, useSearchParams } from 'umi'; + +export const useGetFolderId = () => { + const [searchParams] = useSearchParams(); + const id = searchParams.get('folderId') as string; + + return id ?? ''; +}; + +export const useGetRowSelection = () => { + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const rowSelection: TableRowSelection = { + selectedRowKeys, + getCheckboxProps: (record) => { + return { disabled: record.source_type === 'knowledgebase' }; + }, + onChange: (newSelectedRowKeys: React.Key[]) => { + setSelectedRowKeys(newSelectedRowKeys); + }, + }; + + return { rowSelection, setSelectedRowKeys }; +}; + +export const useNavigateToOtherFolder = () => { + const navigate = useNavigate(); + const navigateToOtherFolder = useCallback( + (folderId: string) => { + navigate(`/file?folderId=${folderId}`); + }, + [navigate], + ); + + return navigateToOtherFolder; +}; + +export const useRenameCurrentFile = () => { + const [file, setFile] = useState({} as IFile); + const { + visible: fileRenameVisible, + hideModal: hideFileRenameModal, + showModal: showFileRenameModal, + } = useSetModalState(); + const { renameFile, loading } = useRenameFile(); + + const onFileRenameOk = useCallback( + async (name: string) => { + const ret = await renameFile({ + fileId: file.id, + name, + }); + + if (ret === 0) { + hideFileRenameModal(); + } + }, + [renameFile, file, hideFileRenameModal], + ); + + const handleShowFileRenameModal = useCallback( + async (record: IFile) => { + setFile(record); + showFileRenameModal(); + }, + [showFileRenameModal], + ); + + return { + fileRenameLoading: loading, + initialFileName: file.name, + onFileRenameOk, + fileRenameVisible, + hideFileRenameModal, + showFileRenameModal: handleShowFileRenameModal, + }; +}; + +export const useSelectBreadcrumbItems = () => { + const parentFolderList = useFetchParentFolderList(); + + return parentFolderList.length === 1 + ? [] + : parentFolderList.map((x) => ({ + title: x.name === '/' ? 'root' : x.name, + path: `/file?folderId=${x.id}`, + })); +}; + +export const useHandleCreateFolder = () => { + const { + visible: folderCreateModalVisible, + hideModal: hideFolderCreateModal, + showModal: showFolderCreateModal, + } = useSetModalState(); + const { createFolder, loading } = useCreateFolder(); + const id = useGetFolderId(); + + const onFolderCreateOk = useCallback( + async (name: string) => { + const ret = await createFolder({ parentId: id, name }); + + if (ret === 0) { + hideFolderCreateModal(); + } + }, + [createFolder, hideFolderCreateModal, id], + ); + + return { + folderCreateLoading: loading, + onFolderCreateOk, + folderCreateModalVisible, + hideFolderCreateModal, + showFolderCreateModal, + }; +}; + +export const useHandleDeleteFile = ( + fileIds: string[], + setSelectedRowKeys: (keys: string[]) => void, +) => { + const { deleteFile: removeDocument } = useDeleteFile(); + const showDeleteConfirm = useShowDeleteConfirm(); + const parentId = useGetFolderId(); + + const handleRemoveFile = () => { + showDeleteConfirm({ + onOk: async () => { + const code = await removeDocument({ fileIds, parentId }); + if (code === 0) { + setSelectedRowKeys([]); + } + return; + }, + }); + }; + + return { handleRemoveFile }; +}; + +export const useHandleUploadFile = () => { + const { + visible: fileUploadVisible, + hideModal: hideFileUploadModal, + showModal: showFileUploadModal, + } = useSetModalState(); + const { uploadFile, loading } = useUploadFile(); + const id = useGetFolderId(); + + const onFileUploadOk = useCallback( + async (fileList: UploadFile[]): Promise => { + if (fileList.length > 0) { + const ret: number = await uploadFile({ fileList, parentId: id }); + if (ret === 0) { + hideFileUploadModal(); + } + return ret; + } + }, + [uploadFile, hideFileUploadModal, id], + ); + + return { + fileUploadLoading: loading, + onFileUploadOk, + fileUploadVisible, + hideFileUploadModal, + showFileUploadModal, + }; +}; + +export const useHandleConnectToKnowledge = () => { + const { + visible: connectToKnowledgeVisible, + hideModal: hideConnectToKnowledgeModal, + showModal: showConnectToKnowledgeModal, + } = useSetModalState(); + const { connectFileToKnowledge: connectToKnowledge, loading } = + useConnectToKnowledge(); + const [record, setRecord] = useState({} as IFile); + + const initialValue = useMemo(() => { + return Array.isArray(record?.kbs_info) + ? record?.kbs_info?.map((x) => x.kb_id) + : []; + }, [record?.kbs_info]); + + const onConnectToKnowledgeOk = useCallback( + async (knowledgeIds: string[]) => { + const ret = await connectToKnowledge({ + fileIds: [record.id], + kbIds: knowledgeIds, + }); + + if (ret === 0) { + hideConnectToKnowledgeModal(); + } + return ret; + }, + [connectToKnowledge, hideConnectToKnowledgeModal, record.id], + ); + + const handleShowConnectToKnowledgeModal = useCallback( + (record: IFile) => { + setRecord(record); + showConnectToKnowledgeModal(); + }, + [showConnectToKnowledgeModal], + ); + + return { + initialValue, + connectToKnowledgeLoading: loading, + onConnectToKnowledgeOk, + connectToKnowledgeVisible, + hideConnectToKnowledgeModal, + showConnectToKnowledgeModal: handleShowConnectToKnowledgeModal, + }; +}; + +export const useHandleBreadcrumbClick = () => { + const navigate = useNavigate(); + + const handleBreadcrumbClick = useCallback( + (path?: string) => { + if (path) { + navigate(path); + } + }, + [navigate], + ); + + return { handleBreadcrumbClick }; +}; + +export const useHandleMoveFile = ( + setSelectedRowKeys: (keys: string[]) => void, +) => { + const { + visible: moveFileVisible, + hideModal: hideMoveFileModal, + showModal: showMoveFileModal, + } = useSetModalState(); + const { moveFile, loading } = useMoveFile(); + const [sourceFileIds, setSourceFileIds] = useState([]); + + const onMoveFileOk = useCallback( + async (targetFolderId: string) => { + const ret = await moveFile({ + src_file_ids: sourceFileIds, + dest_file_id: targetFolderId, + }); + + if (ret === 0) { + setSelectedRowKeys([]); + hideMoveFileModal(); + } + return ret; + }, + [moveFile, hideMoveFileModal, sourceFileIds, setSelectedRowKeys], + ); + + const handleShowMoveFileModal = useCallback( + (ids: string[]) => { + setSourceFileIds(ids); + showMoveFileModal(); + }, + [showMoveFileModal], + ); + + return { + initialValue: '', + moveFileLoading: loading, + onMoveFileOk, + moveFileVisible, + hideMoveFileModal, + showMoveFileModal: handleShowMoveFileModal, + }; +}; diff --git a/web/src/pages/files/index.tsx b/web/src/pages/files/index.tsx new file mode 100644 index 000000000..15869d4c9 --- /dev/null +++ b/web/src/pages/files/index.tsx @@ -0,0 +1,15 @@ +import ListFilterBar from '@/components/list-filter-bar'; +import { Upload } from 'lucide-react'; +import { FilesTable } from './files-table'; + +export default function Files() { + return ( +
+ + + Upload file + + +
+ ); +} diff --git a/web/src/pages/flow/canvas/index.tsx b/web/src/pages/flow/canvas/index.tsx index cbd107ad8..a39a822c4 100644 --- a/web/src/pages/flow/canvas/index.tsx +++ b/web/src/pages/flow/canvas/index.tsx @@ -1,7 +1,6 @@ import { Tooltip, TooltipContent, - TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { @@ -175,24 +174,20 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { - - - - - - Import - - + + + + + Import + - - - - - - Export - - + + + + + Export + diff --git a/web/src/pages/flow/flow-tooltip.tsx b/web/src/pages/flow/flow-tooltip.tsx index d39393249..9386dd06b 100644 --- a/web/src/pages/flow/flow-tooltip.tsx +++ b/web/src/pages/flow/flow-tooltip.tsx @@ -1,7 +1,6 @@ import { Tooltip, TooltipContent, - TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { PropsWithChildren } from 'react'; @@ -10,13 +9,11 @@ import { useTranslation } from 'react-i18next'; export const RunTooltip = ({ children }: PropsWithChildren) => { const { t } = useTranslation(); return ( - - - {children} - -

{t('flow.testRun')}

-
-
-
+ + {children} + +

{t('flow.testRun')}

+
+
); }; diff --git a/web/src/pages/knowledge/knowledge-card/index.tsx b/web/src/pages/knowledge/knowledge-card/index.tsx index bc48b7865..47992ab28 100644 --- a/web/src/pages/knowledge/knowledge-card/index.tsx +++ b/web/src/pages/knowledge/knowledge-card/index.tsx @@ -40,7 +40,7 @@ const KnowledgeCard = ({ item }: IProps) => { return ( { const step = Number((searchParams.get('step') ?? Step.SignIn) as Step); return ( -
-
+
+
{step === Step.SignIn && } {step === Step.SignUp && } {step === Step.VerifyEmail && } diff --git a/web/src/routes.ts b/web/src/routes.ts index 456fa0844..abbc6cfe5 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -7,6 +7,7 @@ export enum Routes { Agent = '/agent', Search = '/next-search', Chat = '/next-chat', + Files = '/files', ProfileSetting = '/profile-setting', DatasetTesting = '/testing', DatasetSetting = '/setting', @@ -189,6 +190,17 @@ const routes = [ }, ], }, + { + path: Routes.Files, + layout: false, + component: '@/layouts/next', + routes: [ + { + path: Routes.Files, + component: `@/pages${Routes.Files}`, + }, + ], + }, { path: Routes.DatasetBase, layout: false, diff --git a/web/src/utils/common-util.ts b/web/src/utils/common-util.ts index fb41adead..9ad4d1638 100644 --- a/web/src/utils/common-util.ts +++ b/web/src/utils/common-util.ts @@ -113,3 +113,28 @@ export function hexToArrayBuffer(input: string) { return view.buffer; } + +export function formatFileSize(bytes: number, si = true, dp = 1) { + let nextBytes = bytes; + const thresh = si ? 1000 : 1024; + + if (Math.abs(bytes) < thresh) { + return nextBytes + ' B'; + } + + const units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let u = -1; + const r = 10 ** dp; + + do { + nextBytes /= thresh; + ++u; + } while ( + Math.round(Math.abs(nextBytes) * r) / r >= thresh && + u < units.length - 1 + ); + + return nextBytes.toFixed(dp) + ' ' + units[u]; +} diff --git a/web/typings.d.ts b/web/typings.d.ts index bd56ed098..34e505311 100644 --- a/web/typings.d.ts +++ b/web/typings.d.ts @@ -1,5 +1,13 @@ +import '@tanstack/react-table'; declare module 'lodash'; declare global { type Nullable = T | null; } + +declare module '@tanstack/react-table' { + interface ColumnMeta { + headerClassName?: string; + cellClassName?: string; + } +}