### What problem does this PR solve? Feat: Add FilesTable #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.16.0
| import weekday from 'dayjs/plugin/weekday'; | import weekday from 'dayjs/plugin/weekday'; | ||||
| import React, { ReactNode, useEffect, useState } from 'react'; | import React, { ReactNode, useEffect, useState } from 'react'; | ||||
| import { ThemeProvider, useTheme } from './components/theme-provider'; | import { ThemeProvider, useTheme } from './components/theme-provider'; | ||||
| import { TooltipProvider } from './components/ui/tooltip'; | |||||
| import storage from './utils/authorization-util'; | import storage from './utils/authorization-util'; | ||||
| dayjs.extend(customParseFormat); | dayjs.extend(customParseFormat); | ||||
| }, []); | }, []); | ||||
| return ( | return ( | ||||
| <QueryClientProvider client={queryClient}> | |||||
| <ThemeProvider defaultTheme="light" storageKey="ragflow-ui-theme"> | |||||
| <Root>{children}</Root> | |||||
| </ThemeProvider> | |||||
| </QueryClientProvider> | |||||
| <TooltipProvider> | |||||
| <QueryClientProvider client={queryClient}> | |||||
| <ThemeProvider defaultTheme="light" storageKey="ragflow-ui-theme"> | |||||
| <Root>{children}</Root> | |||||
| </ThemeProvider> | |||||
| </QueryClientProvider> | |||||
| </TooltipProvider> | |||||
| ); | ); | ||||
| }; | }; | ||||
| export function rootContainer(container: ReactNode) { | export function rootContainer(container: ReactNode) { |
| import { Filter, Search } from 'lucide-react'; | |||||
| import { Filter } from 'lucide-react'; | |||||
| import { PropsWithChildren } from 'react'; | import { PropsWithChildren } from 'react'; | ||||
| import { Button } from './ui/button'; | import { Button } from './ui/button'; | ||||
| import { SearchInput } from './ui/input'; | |||||
| interface IProps { | interface IProps { | ||||
| title: string; | title: string; | ||||
| <span className="text-3xl font-bold ">{title}</span> | <span className="text-3xl font-bold ">{title}</span> | ||||
| <div className="flex gap-4 items-center"> | <div className="flex gap-4 items-center"> | ||||
| <Filter className="size-5" /> | <Filter className="size-5" /> | ||||
| <Search className="size-5" /> | |||||
| <SearchInput></SearchInput> | |||||
| <Button variant={'tertiary'} size={'sm'} onClick={showDialog}> | <Button variant={'tertiary'} size={'sm'} onClick={showDialog}> | ||||
| {children} | {children} | ||||
| </Button> | </Button> |
| import { Skeleton } from '@/components/ui/skeleton'; | |||||
| export function SkeletonCard() { | |||||
| return ( | |||||
| <div className="flex flex-col space-y-3 items-center"> | |||||
| <Skeleton className="h-[125px] w-[250px] rounded-xl" /> | |||||
| <div className="space-y-2 w-[250px]"> | |||||
| <Skeleton className="h-4 w-[250px]" /> | |||||
| <Skeleton className="h-4 w-[200px]" /> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } |
| 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 ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={columnsLength} className="h-24 text-center "> | |||||
| {children} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ); | |||||
| } | |||||
| export function TableSkeleton({ columnsLength }: { columnsLength: number }) { | |||||
| return ( | |||||
| <Row columnsLength={columnsLength}> | |||||
| <SkeletonCard></SkeletonCard> | |||||
| </Row> | |||||
| ); | |||||
| } | |||||
| export function TableEmpty({ columnsLength }: { columnsLength: number }) { | |||||
| return <Row columnsLength={columnsLength}>No results.</Row>; | |||||
| } |
| import * as React from 'react'; | import * as React from 'react'; | ||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||
| import { Search } from 'lucide-react'; | |||||
| export interface InputProps | export interface InputProps | ||||
| extends React.InputHTMLAttributes<HTMLInputElement> {} | extends React.InputHTMLAttributes<HTMLInputElement> {} | ||||
| ); | ); | ||||
| Input.displayName = 'Input'; | Input.displayName = 'Input'; | ||||
| export { Input }; | |||||
| export interface ExpandedInputProps extends Omit<InputProps, 'prefix'> { | |||||
| prefix?: React.ReactNode; | |||||
| suffix?: React.ReactNode; | |||||
| } | |||||
| const ExpandedInput = ({ suffix, prefix, ...props }: ExpandedInputProps) => { | |||||
| return ( | |||||
| <div className="relative"> | |||||
| <span | |||||
| className={cn({ | |||||
| ['absolute left-3 top-[50%] translate-y-[-50%]']: prefix, | |||||
| })} | |||||
| > | |||||
| {prefix} | |||||
| </span> | |||||
| <Input | |||||
| className={cn({ 'pr-10': suffix, 'pl-10': prefix })} | |||||
| {...props} | |||||
| ></Input> | |||||
| <span | |||||
| className={cn({ | |||||
| ['absolute right-3 top-[50%] translate-y-[-50%]']: suffix, | |||||
| })} | |||||
| > | |||||
| {suffix} | |||||
| </span> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| const SearchInput = (props: InputProps) => { | |||||
| return <ExpandedInput suffix={<Search />} {...props}></ExpandedInput>; | |||||
| }; | |||||
| export { ExpandedInput, Input, SearchInput }; |
| import { | import { | ||||
| ChevronDown, | ChevronDown, | ||||
| Cpu, | Cpu, | ||||
| File, | |||||
| Github, | Github, | ||||
| House, | House, | ||||
| Library, | Library, | ||||
| { path: Routes.Chat, name: t('chat'), icon: MessageSquareText }, | { path: Routes.Chat, name: t('chat'), icon: MessageSquareText }, | ||||
| { path: Routes.Search, name: t('search'), icon: Search }, | { path: Routes.Search, name: t('search'), icon: Search }, | ||||
| { path: Routes.Agent, name: t('flow'), icon: Cpu }, | { path: Routes.Agent, name: t('flow'), icon: Cpu }, | ||||
| // { path: '/file', name: t('fileManager'), icon: FileIcon }, | |||||
| { path: Routes.Files, name: t('fileManager'), icon: File }, | |||||
| ], | ], | ||||
| [t], | [t], | ||||
| ); | ); |
| documentUploadLoading, | documentUploadLoading, | ||||
| } = useHandleUploadDocument(); | } = useHandleUploadDocument(); | ||||
| return ( | return ( | ||||
| <section className="p-8 text-foreground"> | |||||
| <section className="p-8"> | |||||
| <ListFilterBar title="Files" showDialog={showDocumentUploadModal}> | <ListFilterBar title="Files" showDialog={showDocumentUploadModal}> | ||||
| <Upload /> | <Upload /> | ||||
| Upload file | Upload file |
| <Form {...form}> | <Form {...form}> | ||||
| <form | <form | ||||
| onSubmit={form.handleSubmit(onSubmit)} | onSubmit={form.handleSubmit(onSubmit)} | ||||
| className="w-2/3 space-y-6" | |||||
| className="space-y-6" | |||||
| id={FormId} | id={FormId} | ||||
| > | > | ||||
| <FormField | <FormField |
| import NewDocumentLink from '@/components/new-document-link'; | import NewDocumentLink from '@/components/new-document-link'; | ||||
| import SvgIcon from '@/components/svg-icon'; | |||||
| import { useTranslate } from '@/hooks/common-hooks'; | import { useTranslate } from '@/hooks/common-hooks'; | ||||
| import { useDownloadFile } from '@/hooks/file-manager-hooks'; | import { useDownloadFile } from '@/hooks/file-manager-hooks'; | ||||
| import { IFile } from '@/interfaces/database/file-manager'; | import { IFile } from '@/interfaces/database/file-manager'; | ||||
| isSupportedPreviewDocumentType, | isSupportedPreviewDocumentType, | ||||
| } from '@/utils/document-util'; | } from '@/utils/document-util'; | ||||
| import { | import { | ||||
| DeleteOutlined, | |||||
| DownloadOutlined, | DownloadOutlined, | ||||
| EditOutlined, | EditOutlined, | ||||
| EyeOutlined, | EyeOutlined, | ||||
| LinkOutlined, | LinkOutlined, | ||||
| } from '@ant-design/icons'; | } from '@ant-design/icons'; | ||||
| import { Button, Space, Tooltip } from 'antd'; | import { Button, Space, Tooltip } from 'antd'; | ||||
| import { FolderInput, Trash2 } from 'lucide-react'; | |||||
| import { useHandleDeleteFile } from '../hooks'; | import { useHandleDeleteFile } from '../hooks'; | ||||
| interface IProps { | interface IProps { | ||||
| type="text" | type="text" | ||||
| disabled={beingUsed} | disabled={beingUsed} | ||||
| onClick={onShowMoveFileModal} | onClick={onShowMoveFileModal} | ||||
| className="flex items-end" | |||||
| > | > | ||||
| <SvgIcon name={`move`} width={16}></SvgIcon> | |||||
| <FolderInput className="size-4" /> | |||||
| </Button> | </Button> | ||||
| </Tooltip> | </Tooltip> | ||||
| )} | )} | ||||
| {isKnowledgeBase || ( | {isKnowledgeBase || ( | ||||
| <Tooltip title={t('delete', { keyPrefix: 'common' })}> | <Tooltip title={t('delete', { keyPrefix: 'common' })}> | ||||
| <Button type="text" disabled={beingUsed} onClick={handleRemoveFile}> | |||||
| <DeleteOutlined size={20} /> | |||||
| <Button | |||||
| type="text" | |||||
| disabled={beingUsed} | |||||
| onClick={handleRemoveFile} | |||||
| className="flex items-end" | |||||
| > | |||||
| <Trash2 className="size-4" /> | |||||
| </Button> | </Button> | ||||
| </Tooltip> | </Tooltip> | ||||
| )} | )} |
| import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg'; | |||||
| import SvgIcon from '@/components/svg-icon'; | |||||
| import { useTranslate } from '@/hooks/common-hooks'; | import { useTranslate } from '@/hooks/common-hooks'; | ||||
| import { | import { | ||||
| IListResult, | IListResult, | ||||
| useSelectBreadcrumbItems, | useSelectBreadcrumbItems, | ||||
| } from './hooks'; | } from './hooks'; | ||||
| import { FolderInput, Trash2 } from 'lucide-react'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| interface IProps | interface IProps | ||||
| onClick: handleRemoveFile, | onClick: handleRemoveFile, | ||||
| label: ( | label: ( | ||||
| <Flex gap={10}> | <Flex gap={10}> | ||||
| <span className={styles.deleteIconWrapper}> | |||||
| <DeleteIcon width={18} /> | |||||
| <span className="flex items-center justify-center"> | |||||
| <Trash2 className="size-4" /> | |||||
| </span> | </span> | ||||
| <b>{t('delete', { keyPrefix: 'common' })}</b> | <b>{t('delete', { keyPrefix: 'common' })}</b> | ||||
| </Flex> | </Flex> | ||||
| onClick: handleShowMoveFileModal, | onClick: handleShowMoveFileModal, | ||||
| label: ( | label: ( | ||||
| <Flex gap={10}> | <Flex gap={10}> | ||||
| <span className={styles.deleteIconWrapper}> | |||||
| <SvgIcon name={`move`} width={18}></SvgIcon> | |||||
| <span className="flex items-center justify-center"> | |||||
| <FolderInput className="size-4"></FolderInput> | |||||
| </span> | </span> | ||||
| <b>{t('move', { keyPrefix: 'common' })}</b> | <b>{t('move', { keyPrefix: 'common' })}</b> | ||||
| </Flex> | </Flex> |
| '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<SortingState>([]); | |||||
| const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( | |||||
| [], | |||||
| ); | |||||
| const [columnVisibility, setColumnVisibility] = | |||||
| React.useState<VisibilityState>({}); | |||||
| const [rowSelection, setRowSelection] = React.useState({}); | |||||
| const { t } = useTranslation('translation', { | |||||
| keyPrefix: 'fileManager', | |||||
| }); | |||||
| const navigateToOtherFolder = useNavigateToOtherFolder(); | |||||
| const { pagination, data, loading, setPagination } = useFetchFileList(); | |||||
| const columns: ColumnDef<IFile>[] = [ | |||||
| { | |||||
| id: 'select', | |||||
| header: ({ table }) => ( | |||||
| <Checkbox | |||||
| checked={ | |||||
| table.getIsAllPageRowsSelected() || | |||||
| (table.getIsSomePageRowsSelected() && 'indeterminate') | |||||
| } | |||||
| onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} | |||||
| aria-label="Select all" | |||||
| /> | |||||
| ), | |||||
| cell: ({ row }) => ( | |||||
| <Checkbox | |||||
| checked={row.getIsSelected()} | |||||
| onCheckedChange={(value) => row.toggleSelected(!!value)} | |||||
| aria-label="Select row" | |||||
| /> | |||||
| ), | |||||
| enableSorting: false, | |||||
| enableHiding: false, | |||||
| }, | |||||
| { | |||||
| accessorKey: 'name', | |||||
| header: ({ column }) => { | |||||
| return ( | |||||
| <Button | |||||
| variant="ghost" | |||||
| onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} | |||||
| > | |||||
| {t('name')} | |||||
| <ArrowUpDown /> | |||||
| </Button> | |||||
| ); | |||||
| }, | |||||
| 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 ( | |||||
| <Tooltip> | |||||
| <TooltipTrigger asChild> | |||||
| <div className="flex gap-2"> | |||||
| <SvgIcon | |||||
| name={`file-icon/${isFolder ? 'folder' : getExtension(name)}`} | |||||
| width={24} | |||||
| ></SvgIcon> | |||||
| <span | |||||
| className={cn('truncate', { ['cursor-pointer']: isFolder })} | |||||
| onClick={handleNameClick} | |||||
| > | |||||
| {name} | |||||
| </span> | |||||
| </div> | |||||
| </TooltipTrigger> | |||||
| <TooltipContent> | |||||
| <p>{name}</p> | |||||
| </TooltipContent> | |||||
| </Tooltip> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| accessorKey: 'create_time', | |||||
| header: ({ column }) => { | |||||
| return ( | |||||
| <Button | |||||
| variant="ghost" | |||||
| onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} | |||||
| > | |||||
| {t('uploadDate')} | |||||
| <ArrowUpDown /> | |||||
| </Button> | |||||
| ); | |||||
| }, | |||||
| cell: ({ row }) => ( | |||||
| <div className="lowercase"> | |||||
| {formatDate(row.getValue('create_time'))} | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| accessorKey: 'size', | |||||
| header: ({ column }) => { | |||||
| return ( | |||||
| <Button | |||||
| variant="ghost" | |||||
| onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} | |||||
| > | |||||
| {t('size')} | |||||
| <ArrowUpDown /> | |||||
| </Button> | |||||
| ); | |||||
| }, | |||||
| cell: ({ row }) => ( | |||||
| <div className="capitalize">{formatFileSize(row.getValue('size'))}</div> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| accessorKey: 'kbs_info', | |||||
| header: t('knowledgeBase'), | |||||
| cell: ({ row }) => ( | |||||
| <Button variant="destructive" size={'sm'}> | |||||
| {row.getValue('kbs_info')} | |||||
| </Button> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| id: 'actions', | |||||
| header: t('action'), | |||||
| enableHiding: false, | |||||
| cell: ({ row }) => { | |||||
| const payment = row.original; | |||||
| return ( | |||||
| <section className="flex gap-4 items-center"> | |||||
| <Switch id="airplane-mode" /> | |||||
| <Button variant="secondary" size={'icon'}> | |||||
| <Pencil /> | |||||
| </Button> | |||||
| <DropdownMenu> | |||||
| <DropdownMenuTrigger asChild> | |||||
| <Button variant="secondary" size={'icon'}> | |||||
| <MoreHorizontal /> | |||||
| </Button> | |||||
| </DropdownMenuTrigger> | |||||
| <DropdownMenuContent align="end"> | |||||
| <DropdownMenuLabel>Actions</DropdownMenuLabel> | |||||
| <DropdownMenuItem | |||||
| onClick={() => navigator.clipboard.writeText(payment.id)} | |||||
| > | |||||
| Copy payment ID | |||||
| </DropdownMenuItem> | |||||
| <DropdownMenuSeparator /> | |||||
| <DropdownMenuItem>View customer</DropdownMenuItem> | |||||
| <DropdownMenuItem>View payment details</DropdownMenuItem> | |||||
| </DropdownMenuContent> | |||||
| </DropdownMenu> | |||||
| </section> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| ]; | |||||
| 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 ( | |||||
| <div className="w-full"> | |||||
| <div className="rounded-md border"> | |||||
| <Table> | |||||
| <TableHeader> | |||||
| {table.getHeaderGroups().map((headerGroup) => ( | |||||
| <TableRow key={headerGroup.id}> | |||||
| {headerGroup.headers.map((header) => { | |||||
| return ( | |||||
| <TableHead key={header.id}> | |||||
| {header.isPlaceholder | |||||
| ? null | |||||
| : flexRender( | |||||
| header.column.columnDef.header, | |||||
| header.getContext(), | |||||
| )} | |||||
| </TableHead> | |||||
| ); | |||||
| })} | |||||
| </TableRow> | |||||
| ))} | |||||
| </TableHeader> | |||||
| <TableBody> | |||||
| {loading ? ( | |||||
| <TableSkeleton columnsLength={columns.length}></TableSkeleton> | |||||
| ) : table.getRowModel().rows?.length ? ( | |||||
| table.getRowModel().rows.map((row) => ( | |||||
| <TableRow | |||||
| key={row.id} | |||||
| data-state={row.getIsSelected() && 'selected'} | |||||
| > | |||||
| {row.getVisibleCells().map((cell) => ( | |||||
| <TableCell | |||||
| key={cell.id} | |||||
| className={cell.column.columnDef.meta?.cellClassName} | |||||
| > | |||||
| {flexRender( | |||||
| cell.column.columnDef.cell, | |||||
| cell.getContext(), | |||||
| )} | |||||
| </TableCell> | |||||
| ))} | |||||
| </TableRow> | |||||
| )) | |||||
| ) : ( | |||||
| <TableEmpty columnsLength={columns.length}></TableEmpty> | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </div> | |||||
| <div className="flex items-center justify-end space-x-2 py-4"> | |||||
| <div className="flex-1 text-sm text-muted-foreground"> | |||||
| {table.getFilteredSelectedRowModel().rows.length} of {data?.total}{' '} | |||||
| row(s) selected. | |||||
| </div> | |||||
| <div className="space-x-2"> | |||||
| <Button | |||||
| variant="outline" | |||||
| size="sm" | |||||
| onClick={() => table.previousPage()} | |||||
| disabled={!table.getCanPreviousPage()} | |||||
| > | |||||
| Previous | |||||
| </Button> | |||||
| <Button | |||||
| variant="outline" | |||||
| size="sm" | |||||
| onClick={() => table.nextPage()} | |||||
| disabled={!table.getCanNextPage()} | |||||
| > | |||||
| Next | |||||
| </Button> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } |
| 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<React.Key[]>([]); | |||||
| const rowSelection: TableRowSelection<IFile> = { | |||||
| 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<IFile>({} 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<number | undefined> => { | |||||
| 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<IFile>({} 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<string[]>([]); | |||||
| 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, | |||||
| }; | |||||
| }; |
| import ListFilterBar from '@/components/list-filter-bar'; | |||||
| import { Upload } from 'lucide-react'; | |||||
| import { FilesTable } from './files-table'; | |||||
| export default function Files() { | |||||
| return ( | |||||
| <section className="p-8"> | |||||
| <ListFilterBar title="Files"> | |||||
| <Upload /> | |||||
| Upload file | |||||
| </ListFilterBar> | |||||
| <FilesTable></FilesTable> | |||||
| </section> | |||||
| ); | |||||
| } |
| import { | import { | ||||
| Tooltip, | Tooltip, | ||||
| TooltipContent, | TooltipContent, | ||||
| TooltipProvider, | |||||
| TooltipTrigger, | TooltipTrigger, | ||||
| } from '@/components/ui/tooltip'; | } from '@/components/ui/tooltip'; | ||||
| import { | import { | ||||
| <Background /> | <Background /> | ||||
| <Controls> | <Controls> | ||||
| <ControlButton onClick={handleImportJson}> | <ControlButton onClick={handleImportJson}> | ||||
| <TooltipProvider> | |||||
| <Tooltip> | |||||
| <TooltipTrigger asChild> | |||||
| <FolderInput className={controlIconClassname} /> | |||||
| </TooltipTrigger> | |||||
| <TooltipContent>Import</TooltipContent> | |||||
| </Tooltip> | |||||
| </TooltipProvider> | |||||
| <Tooltip> | |||||
| <TooltipTrigger asChild> | |||||
| <FolderInput className={controlIconClassname} /> | |||||
| </TooltipTrigger> | |||||
| <TooltipContent>Import</TooltipContent> | |||||
| </Tooltip> | |||||
| </ControlButton> | </ControlButton> | ||||
| <ControlButton onClick={handleExportJson}> | <ControlButton onClick={handleExportJson}> | ||||
| <TooltipProvider> | |||||
| <Tooltip> | |||||
| <TooltipTrigger asChild> | |||||
| <FolderOutput className={controlIconClassname} /> | |||||
| </TooltipTrigger> | |||||
| <TooltipContent>Export</TooltipContent> | |||||
| </Tooltip> | |||||
| </TooltipProvider> | |||||
| <Tooltip> | |||||
| <TooltipTrigger asChild> | |||||
| <FolderOutput className={controlIconClassname} /> | |||||
| </TooltipTrigger> | |||||
| <TooltipContent>Export</TooltipContent> | |||||
| </Tooltip> | |||||
| </ControlButton> | </ControlButton> | ||||
| </Controls> | </Controls> | ||||
| </ReactFlow> | </ReactFlow> |
| import { | import { | ||||
| Tooltip, | Tooltip, | ||||
| TooltipContent, | TooltipContent, | ||||
| TooltipProvider, | |||||
| TooltipTrigger, | TooltipTrigger, | ||||
| } from '@/components/ui/tooltip'; | } from '@/components/ui/tooltip'; | ||||
| import { PropsWithChildren } from 'react'; | import { PropsWithChildren } from 'react'; | ||||
| export const RunTooltip = ({ children }: PropsWithChildren) => { | export const RunTooltip = ({ children }: PropsWithChildren) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| return ( | return ( | ||||
| <TooltipProvider> | |||||
| <Tooltip> | |||||
| <TooltipTrigger>{children}</TooltipTrigger> | |||||
| <TooltipContent> | |||||
| <p>{t('flow.testRun')}</p> | |||||
| </TooltipContent> | |||||
| </Tooltip> | |||||
| </TooltipProvider> | |||||
| <Tooltip> | |||||
| <TooltipTrigger>{children}</TooltipTrigger> | |||||
| <TooltipContent> | |||||
| <p>{t('flow.testRun')}</p> | |||||
| </TooltipContent> | |||||
| </Tooltip> | |||||
| ); | ); | ||||
| }; | }; |
| return ( | return ( | ||||
| <Badge.Ribbon | <Badge.Ribbon | ||||
| text={item.nickname} | |||||
| text={item?.nickname} | |||||
| color={userInfo.nickname === item.nickname ? '#1677ff' : 'pink'} | color={userInfo.nickname === item.nickname ? '#1677ff' : 'pink'} | ||||
| className={classNames(styles.ribbon, { | className={classNames(styles.ribbon, { | ||||
| [styles.hideRibbon]: item.permission !== 'team', | [styles.hideRibbon]: item.permission !== 'team', |
| const step = Number((searchParams.get('step') ?? Step.SignIn) as Step); | const step = Number((searchParams.get('step') ?? Step.SignIn) as Step); | ||||
| return ( | return ( | ||||
| <div className="w-full h-full flex items-center pl-[15%] bg-[url('@/assets/svg/next-login-bg.svg')]"> | |||||
| <div className="inline-block"> | |||||
| <div className="w-full h-full flex items-center pl-[15%] bg-[url('@/assets/svg/next-login-bg.svg')] bg-cover bg-center"> | |||||
| <div className="inline-block bg-colors-background-neutral-standard rounded-lg"> | |||||
| {step === Step.SignIn && <SignInCard></SignInCard>} | {step === Step.SignIn && <SignInCard></SignInCard>} | ||||
| {step === Step.SignUp && <SignUpCard></SignUpCard>} | {step === Step.SignUp && <SignUpCard></SignUpCard>} | ||||
| {step === Step.VerifyEmail && <VerifyEmailCard></VerifyEmailCard>} | {step === Step.VerifyEmail && <VerifyEmailCard></VerifyEmailCard>} |
| Agent = '/agent', | Agent = '/agent', | ||||
| Search = '/next-search', | Search = '/next-search', | ||||
| Chat = '/next-chat', | Chat = '/next-chat', | ||||
| Files = '/files', | |||||
| ProfileSetting = '/profile-setting', | ProfileSetting = '/profile-setting', | ||||
| DatasetTesting = '/testing', | DatasetTesting = '/testing', | ||||
| DatasetSetting = '/setting', | DatasetSetting = '/setting', | ||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| { | |||||
| path: Routes.Files, | |||||
| layout: false, | |||||
| component: '@/layouts/next', | |||||
| routes: [ | |||||
| { | |||||
| path: Routes.Files, | |||||
| component: `@/pages${Routes.Files}`, | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| { | { | ||||
| path: Routes.DatasetBase, | path: Routes.DatasetBase, | ||||
| layout: false, | layout: false, |
| return view.buffer; | 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]; | |||||
| } |
| import '@tanstack/react-table'; | |||||
| declare module 'lodash'; | declare module 'lodash'; | ||||
| declare global { | declare global { | ||||
| type Nullable<T> = T | null; | type Nullable<T> = T | null; | ||||
| } | } | ||||
| declare module '@tanstack/react-table' { | |||||
| interface ColumnMeta { | |||||
| headerClassName?: string; | |||||
| cellClassName?: string; | |||||
| } | |||||
| } |