balibabu commited on
Commit
d1bf860
·
1 Parent(s): 2435d05

Feat: Add FilesTable #3221 (#4491)

Browse files

### 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 CHANGED
@@ -15,6 +15,7 @@ import weekYear from 'dayjs/plugin/weekYear';
15
  import weekday from 'dayjs/plugin/weekday';
16
  import React, { ReactNode, useEffect, useState } from 'react';
17
  import { ThemeProvider, useTheme } from './components/theme-provider';
 
18
  import storage from './utils/authorization-util';
19
 
20
  dayjs.extend(customParseFormat);
@@ -78,11 +79,13 @@ const RootProvider = ({ children }: React.PropsWithChildren) => {
78
  }, []);
79
 
80
  return (
81
- <QueryClientProvider client={queryClient}>
82
- <ThemeProvider defaultTheme="light" storageKey="ragflow-ui-theme">
83
- <Root>{children}</Root>
84
- </ThemeProvider>
85
- </QueryClientProvider>
 
 
86
  );
87
  };
88
  export function rootContainer(container: ReactNode) {
 
15
  import weekday from 'dayjs/plugin/weekday';
16
  import React, { ReactNode, useEffect, useState } from 'react';
17
  import { ThemeProvider, useTheme } from './components/theme-provider';
18
+ import { TooltipProvider } from './components/ui/tooltip';
19
  import storage from './utils/authorization-util';
20
 
21
  dayjs.extend(customParseFormat);
 
79
  }, []);
80
 
81
  return (
82
+ <TooltipProvider>
83
+ <QueryClientProvider client={queryClient}>
84
+ <ThemeProvider defaultTheme="light" storageKey="ragflow-ui-theme">
85
+ <Root>{children}</Root>
86
+ </ThemeProvider>
87
+ </QueryClientProvider>
88
+ </TooltipProvider>
89
  );
90
  };
91
  export function rootContainer(container: ReactNode) {
web/src/components/list-filter-bar.tsx CHANGED
@@ -1,6 +1,7 @@
1
- import { Filter, Search } from 'lucide-react';
2
  import { PropsWithChildren } from 'react';
3
  import { Button } from './ui/button';
 
4
 
5
  interface IProps {
6
  title: string;
@@ -17,7 +18,7 @@ export default function ListFilterBar({
17
  <span className="text-3xl font-bold ">{title}</span>
18
  <div className="flex gap-4 items-center">
19
  <Filter className="size-5" />
20
- <Search className="size-5" />
21
  <Button variant={'tertiary'} size={'sm'} onClick={showDialog}>
22
  {children}
23
  </Button>
 
1
+ import { Filter } from 'lucide-react';
2
  import { PropsWithChildren } from 'react';
3
  import { Button } from './ui/button';
4
+ import { SearchInput } from './ui/input';
5
 
6
  interface IProps {
7
  title: string;
 
18
  <span className="text-3xl font-bold ">{title}</span>
19
  <div className="flex gap-4 items-center">
20
  <Filter className="size-5" />
21
+ <SearchInput></SearchInput>
22
  <Button variant={'tertiary'} size={'sm'} onClick={showDialog}>
23
  {children}
24
  </Button>
web/src/components/skeleton-card.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Skeleton } from '@/components/ui/skeleton';
2
+
3
+ export function SkeletonCard() {
4
+ return (
5
+ <div className="flex flex-col space-y-3 items-center">
6
+ <Skeleton className="h-[125px] w-[250px] rounded-xl" />
7
+ <div className="space-y-2 w-[250px]">
8
+ <Skeleton className="h-4 w-[250px]" />
9
+ <Skeleton className="h-4 w-[200px]" />
10
+ </div>
11
+ </div>
12
+ );
13
+ }
web/src/components/table-skeleton.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PropsWithChildren } from 'react';
2
+ import { SkeletonCard } from './skeleton-card';
3
+ import { TableCell, TableRow } from './ui/table';
4
+
5
+ type IProps = { columnsLength: number };
6
+
7
+ function Row({ children, columnsLength }: PropsWithChildren & IProps) {
8
+ return (
9
+ <TableRow>
10
+ <TableCell colSpan={columnsLength} className="h-24 text-center ">
11
+ {children}
12
+ </TableCell>
13
+ </TableRow>
14
+ );
15
+ }
16
+
17
+ export function TableSkeleton({ columnsLength }: { columnsLength: number }) {
18
+ return (
19
+ <Row columnsLength={columnsLength}>
20
+ <SkeletonCard></SkeletonCard>
21
+ </Row>
22
+ );
23
+ }
24
+
25
+ export function TableEmpty({ columnsLength }: { columnsLength: number }) {
26
+ return <Row columnsLength={columnsLength}>No results.</Row>;
27
+ }
web/src/components/ui/input.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import * as React from 'react';
2
 
3
  import { cn } from '@/lib/utils';
 
4
 
5
  export interface InputProps
6
  extends React.InputHTMLAttributes<HTMLInputElement> {}
@@ -22,4 +23,38 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
22
  );
23
  Input.displayName = 'Input';
24
 
25
- export { Input };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import * as React from 'react';
2
 
3
  import { cn } from '@/lib/utils';
4
+ import { Search } from 'lucide-react';
5
 
6
  export interface InputProps
7
  extends React.InputHTMLAttributes<HTMLInputElement> {}
 
23
  );
24
  Input.displayName = 'Input';
25
 
26
+ export interface ExpandedInputProps extends Omit<InputProps, 'prefix'> {
27
+ prefix?: React.ReactNode;
28
+ suffix?: React.ReactNode;
29
+ }
30
+
31
+ const ExpandedInput = ({ suffix, prefix, ...props }: ExpandedInputProps) => {
32
+ return (
33
+ <div className="relative">
34
+ <span
35
+ className={cn({
36
+ ['absolute left-3 top-[50%] translate-y-[-50%]']: prefix,
37
+ })}
38
+ >
39
+ {prefix}
40
+ </span>
41
+ <Input
42
+ className={cn({ 'pr-10': suffix, 'pl-10': prefix })}
43
+ {...props}
44
+ ></Input>
45
+ <span
46
+ className={cn({
47
+ ['absolute right-3 top-[50%] translate-y-[-50%]']: suffix,
48
+ })}
49
+ >
50
+ {suffix}
51
+ </span>
52
+ </div>
53
+ );
54
+ };
55
+
56
+ const SearchInput = (props: InputProps) => {
57
+ return <ExpandedInput suffix={<Search />} {...props}></ExpandedInput>;
58
+ };
59
+
60
+ export { ExpandedInput, Input, SearchInput };
web/src/layouts/next-header.tsx CHANGED
@@ -10,6 +10,7 @@ import { Routes } from '@/routes';
10
  import {
11
  ChevronDown,
12
  Cpu,
 
13
  Github,
14
  House,
15
  Library,
@@ -33,7 +34,7 @@ export function Header() {
33
  { path: Routes.Chat, name: t('chat'), icon: MessageSquareText },
34
  { path: Routes.Search, name: t('search'), icon: Search },
35
  { path: Routes.Agent, name: t('flow'), icon: Cpu },
36
- // { path: '/file', name: t('fileManager'), icon: FileIcon },
37
  ],
38
  [t],
39
  );
 
10
  import {
11
  ChevronDown,
12
  Cpu,
13
+ File,
14
  Github,
15
  House,
16
  Library,
 
34
  { path: Routes.Chat, name: t('chat'), icon: MessageSquareText },
35
  { path: Routes.Search, name: t('search'), icon: Search },
36
  { path: Routes.Agent, name: t('flow'), icon: Cpu },
37
+ { path: Routes.Files, name: t('fileManager'), icon: File },
38
  ],
39
  [t],
40
  );
web/src/pages/dataset/dataset/index.tsx CHANGED
@@ -13,7 +13,7 @@ export default function Dataset() {
13
  documentUploadLoading,
14
  } = useHandleUploadDocument();
15
  return (
16
- <section className="p-8 text-foreground">
17
  <ListFilterBar title="Files" showDialog={showDocumentUploadModal}>
18
  <Upload />
19
  Upload file
 
13
  documentUploadLoading,
14
  } = useHandleUploadDocument();
15
  return (
16
+ <section className="p-8">
17
  <ListFilterBar title="Files" showDialog={showDocumentUploadModal}>
18
  <Upload />
19
  Upload file
web/src/pages/datasets/dataset-creating-dialog.tsx CHANGED
@@ -50,7 +50,7 @@ export function InputForm() {
50
  <Form {...form}>
51
  <form
52
  onSubmit={form.handleSubmit(onSubmit)}
53
- className="w-2/3 space-y-6"
54
  id={FormId}
55
  >
56
  <FormField
 
50
  <Form {...form}>
51
  <form
52
  onSubmit={form.handleSubmit(onSubmit)}
53
+ className="space-y-6"
54
  id={FormId}
55
  >
56
  <FormField
web/src/pages/file-manager/action-cell/index.tsx CHANGED
@@ -1,5 +1,4 @@
1
  import NewDocumentLink from '@/components/new-document-link';
2
- import SvgIcon from '@/components/svg-icon';
3
  import { useTranslate } from '@/hooks/common-hooks';
4
  import { useDownloadFile } from '@/hooks/file-manager-hooks';
5
  import { IFile } from '@/interfaces/database/file-manager';
@@ -8,13 +7,13 @@ import {
8
  isSupportedPreviewDocumentType,
9
  } from '@/utils/document-util';
10
  import {
11
- DeleteOutlined,
12
  DownloadOutlined,
13
  EditOutlined,
14
  EyeOutlined,
15
  LinkOutlined,
16
  } from '@ant-design/icons';
17
  import { Button, Space, Tooltip } from 'antd';
 
18
  import { useHandleDeleteFile } from '../hooks';
19
 
20
  interface IProps {
@@ -92,15 +91,21 @@ const ActionCell = ({
92
  type="text"
93
  disabled={beingUsed}
94
  onClick={onShowMoveFileModal}
 
95
  >
96
- <SvgIcon name={`move`} width={16}></SvgIcon>
97
  </Button>
98
  </Tooltip>
99
  )}
100
  {isKnowledgeBase || (
101
  <Tooltip title={t('delete', { keyPrefix: 'common' })}>
102
- <Button type="text" disabled={beingUsed} onClick={handleRemoveFile}>
103
- <DeleteOutlined size={20} />
 
 
 
 
 
104
  </Button>
105
  </Tooltip>
106
  )}
 
1
  import NewDocumentLink from '@/components/new-document-link';
 
2
  import { useTranslate } from '@/hooks/common-hooks';
3
  import { useDownloadFile } from '@/hooks/file-manager-hooks';
4
  import { IFile } from '@/interfaces/database/file-manager';
 
7
  isSupportedPreviewDocumentType,
8
  } from '@/utils/document-util';
9
  import {
 
10
  DownloadOutlined,
11
  EditOutlined,
12
  EyeOutlined,
13
  LinkOutlined,
14
  } from '@ant-design/icons';
15
  import { Button, Space, Tooltip } from 'antd';
16
+ import { FolderInput, Trash2 } from 'lucide-react';
17
  import { useHandleDeleteFile } from '../hooks';
18
 
19
  interface IProps {
 
91
  type="text"
92
  disabled={beingUsed}
93
  onClick={onShowMoveFileModal}
94
+ className="flex items-end"
95
  >
96
+ <FolderInput className="size-4" />
97
  </Button>
98
  </Tooltip>
99
  )}
100
  {isKnowledgeBase || (
101
  <Tooltip title={t('delete', { keyPrefix: 'common' })}>
102
+ <Button
103
+ type="text"
104
+ disabled={beingUsed}
105
+ onClick={handleRemoveFile}
106
+ className="flex items-end"
107
+ >
108
+ <Trash2 className="size-4" />
109
  </Button>
110
  </Tooltip>
111
  )}
web/src/pages/file-manager/file-toolbar.tsx CHANGED
@@ -1,5 +1,3 @@
1
- import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg';
2
- import SvgIcon from '@/components/svg-icon';
3
  import { useTranslate } from '@/hooks/common-hooks';
4
  import {
5
  IListResult,
@@ -29,6 +27,7 @@ import {
29
  useSelectBreadcrumbItems,
30
  } from './hooks';
31
 
 
32
  import styles from './index.less';
33
 
34
  interface IProps
@@ -127,8 +126,8 @@ const FileToolbar = ({
127
  onClick: handleRemoveFile,
128
  label: (
129
  <Flex gap={10}>
130
- <span className={styles.deleteIconWrapper}>
131
- <DeleteIcon width={18} />
132
  </span>
133
  <b>{t('delete', { keyPrefix: 'common' })}</b>
134
  </Flex>
@@ -139,8 +138,8 @@ const FileToolbar = ({
139
  onClick: handleShowMoveFileModal,
140
  label: (
141
  <Flex gap={10}>
142
- <span className={styles.deleteIconWrapper}>
143
- <SvgIcon name={`move`} width={18}></SvgIcon>
144
  </span>
145
  <b>{t('move', { keyPrefix: 'common' })}</b>
146
  </Flex>
 
 
 
1
  import { useTranslate } from '@/hooks/common-hooks';
2
  import {
3
  IListResult,
 
27
  useSelectBreadcrumbItems,
28
  } from './hooks';
29
 
30
+ import { FolderInput, Trash2 } from 'lucide-react';
31
  import styles from './index.less';
32
 
33
  interface IProps
 
126
  onClick: handleRemoveFile,
127
  label: (
128
  <Flex gap={10}>
129
+ <span className="flex items-center justify-center">
130
+ <Trash2 className="size-4" />
131
  </span>
132
  <b>{t('delete', { keyPrefix: 'common' })}</b>
133
  </Flex>
 
138
  onClick: handleShowMoveFileModal,
139
  label: (
140
  <Flex gap={10}>
141
+ <span className="flex items-center justify-center">
142
+ <FolderInput className="size-4"></FolderInput>
143
  </span>
144
  <b>{t('move', { keyPrefix: 'common' })}</b>
145
  </Flex>
web/src/pages/files/files-table.tsx ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import {
4
+ ColumnDef,
5
+ ColumnFiltersState,
6
+ SortingState,
7
+ VisibilityState,
8
+ flexRender,
9
+ getCoreRowModel,
10
+ getFilteredRowModel,
11
+ getSortedRowModel,
12
+ useReactTable,
13
+ } from '@tanstack/react-table';
14
+ import { ArrowUpDown, MoreHorizontal, Pencil } from 'lucide-react';
15
+ import * as React from 'react';
16
+
17
+ import SvgIcon from '@/components/svg-icon';
18
+ import { TableEmpty, TableSkeleton } from '@/components/table-skeleton';
19
+ import { Button } from '@/components/ui/button';
20
+ import { Checkbox } from '@/components/ui/checkbox';
21
+ import {
22
+ DropdownMenu,
23
+ DropdownMenuContent,
24
+ DropdownMenuItem,
25
+ DropdownMenuLabel,
26
+ DropdownMenuSeparator,
27
+ DropdownMenuTrigger,
28
+ } from '@/components/ui/dropdown-menu';
29
+ import { Switch } from '@/components/ui/switch';
30
+ import {
31
+ Table,
32
+ TableBody,
33
+ TableCell,
34
+ TableHead,
35
+ TableHeader,
36
+ TableRow,
37
+ } from '@/components/ui/table';
38
+ import {
39
+ Tooltip,
40
+ TooltipContent,
41
+ TooltipTrigger,
42
+ } from '@/components/ui/tooltip';
43
+ import { useFetchFileList } from '@/hooks/file-manager-hooks';
44
+ import { IFile } from '@/interfaces/database/file-manager';
45
+ import { cn } from '@/lib/utils';
46
+ import { formatFileSize } from '@/utils/common-util';
47
+ import { formatDate } from '@/utils/date';
48
+ import { getExtension } from '@/utils/document-util';
49
+ import { useMemo } from 'react';
50
+ import { useTranslation } from 'react-i18next';
51
+ import { useNavigateToOtherFolder } from './hooks';
52
+
53
+ export function FilesTable() {
54
+ const [sorting, setSorting] = React.useState<SortingState>([]);
55
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
56
+ [],
57
+ );
58
+ const [columnVisibility, setColumnVisibility] =
59
+ React.useState<VisibilityState>({});
60
+ const [rowSelection, setRowSelection] = React.useState({});
61
+ const { t } = useTranslation('translation', {
62
+ keyPrefix: 'fileManager',
63
+ });
64
+ const navigateToOtherFolder = useNavigateToOtherFolder();
65
+
66
+ const { pagination, data, loading, setPagination } = useFetchFileList();
67
+
68
+ const columns: ColumnDef<IFile>[] = [
69
+ {
70
+ id: 'select',
71
+ header: ({ table }) => (
72
+ <Checkbox
73
+ checked={
74
+ table.getIsAllPageRowsSelected() ||
75
+ (table.getIsSomePageRowsSelected() && 'indeterminate')
76
+ }
77
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
78
+ aria-label="Select all"
79
+ />
80
+ ),
81
+ cell: ({ row }) => (
82
+ <Checkbox
83
+ checked={row.getIsSelected()}
84
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
85
+ aria-label="Select row"
86
+ />
87
+ ),
88
+ enableSorting: false,
89
+ enableHiding: false,
90
+ },
91
+ {
92
+ accessorKey: 'name',
93
+ header: ({ column }) => {
94
+ return (
95
+ <Button
96
+ variant="ghost"
97
+ onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
98
+ >
99
+ {t('name')}
100
+ <ArrowUpDown />
101
+ </Button>
102
+ );
103
+ },
104
+ meta: { cellClassName: 'max-w-[20vw]' },
105
+ cell: ({ row }) => {
106
+ const name: string = row.getValue('name');
107
+ const type = row.original.type;
108
+ const id = row.original.id;
109
+ const isFolder = type === 'folder';
110
+
111
+ const handleNameClick = () => {
112
+ if (isFolder) {
113
+ navigateToOtherFolder(id);
114
+ }
115
+ };
116
+
117
+ return (
118
+ <Tooltip>
119
+ <TooltipTrigger asChild>
120
+ <div className="flex gap-2">
121
+ <SvgIcon
122
+ name={`file-icon/${isFolder ? 'folder' : getExtension(name)}`}
123
+ width={24}
124
+ ></SvgIcon>
125
+ <span
126
+ className={cn('truncate', { ['cursor-pointer']: isFolder })}
127
+ onClick={handleNameClick}
128
+ >
129
+ {name}
130
+ </span>
131
+ </div>
132
+ </TooltipTrigger>
133
+ <TooltipContent>
134
+ <p>{name}</p>
135
+ </TooltipContent>
136
+ </Tooltip>
137
+ );
138
+ },
139
+ },
140
+ {
141
+ accessorKey: 'create_time',
142
+ header: ({ column }) => {
143
+ return (
144
+ <Button
145
+ variant="ghost"
146
+ onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
147
+ >
148
+ {t('uploadDate')}
149
+ <ArrowUpDown />
150
+ </Button>
151
+ );
152
+ },
153
+ cell: ({ row }) => (
154
+ <div className="lowercase">
155
+ {formatDate(row.getValue('create_time'))}
156
+ </div>
157
+ ),
158
+ },
159
+ {
160
+ accessorKey: 'size',
161
+ header: ({ column }) => {
162
+ return (
163
+ <Button
164
+ variant="ghost"
165
+ onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
166
+ >
167
+ {t('size')}
168
+ <ArrowUpDown />
169
+ </Button>
170
+ );
171
+ },
172
+ cell: ({ row }) => (
173
+ <div className="capitalize">{formatFileSize(row.getValue('size'))}</div>
174
+ ),
175
+ },
176
+ {
177
+ accessorKey: 'kbs_info',
178
+ header: t('knowledgeBase'),
179
+ cell: ({ row }) => (
180
+ <Button variant="destructive" size={'sm'}>
181
+ {row.getValue('kbs_info')}
182
+ </Button>
183
+ ),
184
+ },
185
+ {
186
+ id: 'actions',
187
+ header: t('action'),
188
+ enableHiding: false,
189
+ cell: ({ row }) => {
190
+ const payment = row.original;
191
+
192
+ return (
193
+ <section className="flex gap-4 items-center">
194
+ <Switch id="airplane-mode" />
195
+ <Button variant="secondary" size={'icon'}>
196
+ <Pencil />
197
+ </Button>
198
+ <DropdownMenu>
199
+ <DropdownMenuTrigger asChild>
200
+ <Button variant="secondary" size={'icon'}>
201
+ <MoreHorizontal />
202
+ </Button>
203
+ </DropdownMenuTrigger>
204
+ <DropdownMenuContent align="end">
205
+ <DropdownMenuLabel>Actions</DropdownMenuLabel>
206
+ <DropdownMenuItem
207
+ onClick={() => navigator.clipboard.writeText(payment.id)}
208
+ >
209
+ Copy payment ID
210
+ </DropdownMenuItem>
211
+ <DropdownMenuSeparator />
212
+ <DropdownMenuItem>View customer</DropdownMenuItem>
213
+ <DropdownMenuItem>View payment details</DropdownMenuItem>
214
+ </DropdownMenuContent>
215
+ </DropdownMenu>
216
+ </section>
217
+ );
218
+ },
219
+ },
220
+ ];
221
+
222
+ const currentPagination = useMemo(() => {
223
+ return {
224
+ pageIndex: (pagination.current || 1) - 1,
225
+ pageSize: pagination.pageSize || 10,
226
+ };
227
+ }, [pagination]);
228
+
229
+ const table = useReactTable({
230
+ data: data?.files || [],
231
+ columns,
232
+ onSortingChange: setSorting,
233
+ onColumnFiltersChange: setColumnFilters,
234
+ getCoreRowModel: getCoreRowModel(),
235
+ // getPaginationRowModel: getPaginationRowModel(),
236
+ getSortedRowModel: getSortedRowModel(),
237
+ getFilteredRowModel: getFilteredRowModel(),
238
+ onColumnVisibilityChange: setColumnVisibility,
239
+ onRowSelectionChange: setRowSelection,
240
+ onPaginationChange: (updaterOrValue: any) => {
241
+ if (typeof updaterOrValue === 'function') {
242
+ const nextPagination = updaterOrValue(currentPagination);
243
+ setPagination({
244
+ page: nextPagination.pageIndex + 1,
245
+ pageSize: nextPagination.pageSize,
246
+ });
247
+ } else {
248
+ setPagination({
249
+ page: updaterOrValue.pageIndex,
250
+ pageSize: updaterOrValue.pageSize,
251
+ });
252
+ }
253
+ },
254
+ manualPagination: true, //we're doing manual "server-side" pagination
255
+
256
+ state: {
257
+ sorting,
258
+ columnFilters,
259
+ columnVisibility,
260
+ rowSelection,
261
+ pagination: currentPagination,
262
+ },
263
+ rowCount: data?.total ?? 0,
264
+ debugTable: true,
265
+ });
266
+
267
+ return (
268
+ <div className="w-full">
269
+ <div className="rounded-md border">
270
+ <Table>
271
+ <TableHeader>
272
+ {table.getHeaderGroups().map((headerGroup) => (
273
+ <TableRow key={headerGroup.id}>
274
+ {headerGroup.headers.map((header) => {
275
+ return (
276
+ <TableHead key={header.id}>
277
+ {header.isPlaceholder
278
+ ? null
279
+ : flexRender(
280
+ header.column.columnDef.header,
281
+ header.getContext(),
282
+ )}
283
+ </TableHead>
284
+ );
285
+ })}
286
+ </TableRow>
287
+ ))}
288
+ </TableHeader>
289
+ <TableBody>
290
+ {loading ? (
291
+ <TableSkeleton columnsLength={columns.length}></TableSkeleton>
292
+ ) : table.getRowModel().rows?.length ? (
293
+ table.getRowModel().rows.map((row) => (
294
+ <TableRow
295
+ key={row.id}
296
+ data-state={row.getIsSelected() && 'selected'}
297
+ >
298
+ {row.getVisibleCells().map((cell) => (
299
+ <TableCell
300
+ key={cell.id}
301
+ className={cell.column.columnDef.meta?.cellClassName}
302
+ >
303
+ {flexRender(
304
+ cell.column.columnDef.cell,
305
+ cell.getContext(),
306
+ )}
307
+ </TableCell>
308
+ ))}
309
+ </TableRow>
310
+ ))
311
+ ) : (
312
+ <TableEmpty columnsLength={columns.length}></TableEmpty>
313
+ )}
314
+ </TableBody>
315
+ </Table>
316
+ </div>
317
+ <div className="flex items-center justify-end space-x-2 py-4">
318
+ <div className="flex-1 text-sm text-muted-foreground">
319
+ {table.getFilteredSelectedRowModel().rows.length} of {data?.total}{' '}
320
+ row(s) selected.
321
+ </div>
322
+ <div className="space-x-2">
323
+ <Button
324
+ variant="outline"
325
+ size="sm"
326
+ onClick={() => table.previousPage()}
327
+ disabled={!table.getCanPreviousPage()}
328
+ >
329
+ Previous
330
+ </Button>
331
+ <Button
332
+ variant="outline"
333
+ size="sm"
334
+ onClick={() => table.nextPage()}
335
+ disabled={!table.getCanNextPage()}
336
+ >
337
+ Next
338
+ </Button>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ );
343
+ }
web/src/pages/files/hooks.ts ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
2
+ import {
3
+ useConnectToKnowledge,
4
+ useCreateFolder,
5
+ useDeleteFile,
6
+ useFetchParentFolderList,
7
+ useMoveFile,
8
+ useRenameFile,
9
+ useUploadFile,
10
+ } from '@/hooks/file-manager-hooks';
11
+ import { IFile } from '@/interfaces/database/file-manager';
12
+ import { TableRowSelection } from 'antd/es/table/interface';
13
+ import { UploadFile } from 'antd/lib';
14
+ import { useCallback, useMemo, useState } from 'react';
15
+ import { useNavigate, useSearchParams } from 'umi';
16
+
17
+ export const useGetFolderId = () => {
18
+ const [searchParams] = useSearchParams();
19
+ const id = searchParams.get('folderId') as string;
20
+
21
+ return id ?? '';
22
+ };
23
+
24
+ export const useGetRowSelection = () => {
25
+ const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
26
+
27
+ const rowSelection: TableRowSelection<IFile> = {
28
+ selectedRowKeys,
29
+ getCheckboxProps: (record) => {
30
+ return { disabled: record.source_type === 'knowledgebase' };
31
+ },
32
+ onChange: (newSelectedRowKeys: React.Key[]) => {
33
+ setSelectedRowKeys(newSelectedRowKeys);
34
+ },
35
+ };
36
+
37
+ return { rowSelection, setSelectedRowKeys };
38
+ };
39
+
40
+ export const useNavigateToOtherFolder = () => {
41
+ const navigate = useNavigate();
42
+ const navigateToOtherFolder = useCallback(
43
+ (folderId: string) => {
44
+ navigate(`/file?folderId=${folderId}`);
45
+ },
46
+ [navigate],
47
+ );
48
+
49
+ return navigateToOtherFolder;
50
+ };
51
+
52
+ export const useRenameCurrentFile = () => {
53
+ const [file, setFile] = useState<IFile>({} as IFile);
54
+ const {
55
+ visible: fileRenameVisible,
56
+ hideModal: hideFileRenameModal,
57
+ showModal: showFileRenameModal,
58
+ } = useSetModalState();
59
+ const { renameFile, loading } = useRenameFile();
60
+
61
+ const onFileRenameOk = useCallback(
62
+ async (name: string) => {
63
+ const ret = await renameFile({
64
+ fileId: file.id,
65
+ name,
66
+ });
67
+
68
+ if (ret === 0) {
69
+ hideFileRenameModal();
70
+ }
71
+ },
72
+ [renameFile, file, hideFileRenameModal],
73
+ );
74
+
75
+ const handleShowFileRenameModal = useCallback(
76
+ async (record: IFile) => {
77
+ setFile(record);
78
+ showFileRenameModal();
79
+ },
80
+ [showFileRenameModal],
81
+ );
82
+
83
+ return {
84
+ fileRenameLoading: loading,
85
+ initialFileName: file.name,
86
+ onFileRenameOk,
87
+ fileRenameVisible,
88
+ hideFileRenameModal,
89
+ showFileRenameModal: handleShowFileRenameModal,
90
+ };
91
+ };
92
+
93
+ export const useSelectBreadcrumbItems = () => {
94
+ const parentFolderList = useFetchParentFolderList();
95
+
96
+ return parentFolderList.length === 1
97
+ ? []
98
+ : parentFolderList.map((x) => ({
99
+ title: x.name === '/' ? 'root' : x.name,
100
+ path: `/file?folderId=${x.id}`,
101
+ }));
102
+ };
103
+
104
+ export const useHandleCreateFolder = () => {
105
+ const {
106
+ visible: folderCreateModalVisible,
107
+ hideModal: hideFolderCreateModal,
108
+ showModal: showFolderCreateModal,
109
+ } = useSetModalState();
110
+ const { createFolder, loading } = useCreateFolder();
111
+ const id = useGetFolderId();
112
+
113
+ const onFolderCreateOk = useCallback(
114
+ async (name: string) => {
115
+ const ret = await createFolder({ parentId: id, name });
116
+
117
+ if (ret === 0) {
118
+ hideFolderCreateModal();
119
+ }
120
+ },
121
+ [createFolder, hideFolderCreateModal, id],
122
+ );
123
+
124
+ return {
125
+ folderCreateLoading: loading,
126
+ onFolderCreateOk,
127
+ folderCreateModalVisible,
128
+ hideFolderCreateModal,
129
+ showFolderCreateModal,
130
+ };
131
+ };
132
+
133
+ export const useHandleDeleteFile = (
134
+ fileIds: string[],
135
+ setSelectedRowKeys: (keys: string[]) => void,
136
+ ) => {
137
+ const { deleteFile: removeDocument } = useDeleteFile();
138
+ const showDeleteConfirm = useShowDeleteConfirm();
139
+ const parentId = useGetFolderId();
140
+
141
+ const handleRemoveFile = () => {
142
+ showDeleteConfirm({
143
+ onOk: async () => {
144
+ const code = await removeDocument({ fileIds, parentId });
145
+ if (code === 0) {
146
+ setSelectedRowKeys([]);
147
+ }
148
+ return;
149
+ },
150
+ });
151
+ };
152
+
153
+ return { handleRemoveFile };
154
+ };
155
+
156
+ export const useHandleUploadFile = () => {
157
+ const {
158
+ visible: fileUploadVisible,
159
+ hideModal: hideFileUploadModal,
160
+ showModal: showFileUploadModal,
161
+ } = useSetModalState();
162
+ const { uploadFile, loading } = useUploadFile();
163
+ const id = useGetFolderId();
164
+
165
+ const onFileUploadOk = useCallback(
166
+ async (fileList: UploadFile[]): Promise<number | undefined> => {
167
+ if (fileList.length > 0) {
168
+ const ret: number = await uploadFile({ fileList, parentId: id });
169
+ if (ret === 0) {
170
+ hideFileUploadModal();
171
+ }
172
+ return ret;
173
+ }
174
+ },
175
+ [uploadFile, hideFileUploadModal, id],
176
+ );
177
+
178
+ return {
179
+ fileUploadLoading: loading,
180
+ onFileUploadOk,
181
+ fileUploadVisible,
182
+ hideFileUploadModal,
183
+ showFileUploadModal,
184
+ };
185
+ };
186
+
187
+ export const useHandleConnectToKnowledge = () => {
188
+ const {
189
+ visible: connectToKnowledgeVisible,
190
+ hideModal: hideConnectToKnowledgeModal,
191
+ showModal: showConnectToKnowledgeModal,
192
+ } = useSetModalState();
193
+ const { connectFileToKnowledge: connectToKnowledge, loading } =
194
+ useConnectToKnowledge();
195
+ const [record, setRecord] = useState<IFile>({} as IFile);
196
+
197
+ const initialValue = useMemo(() => {
198
+ return Array.isArray(record?.kbs_info)
199
+ ? record?.kbs_info?.map((x) => x.kb_id)
200
+ : [];
201
+ }, [record?.kbs_info]);
202
+
203
+ const onConnectToKnowledgeOk = useCallback(
204
+ async (knowledgeIds: string[]) => {
205
+ const ret = await connectToKnowledge({
206
+ fileIds: [record.id],
207
+ kbIds: knowledgeIds,
208
+ });
209
+
210
+ if (ret === 0) {
211
+ hideConnectToKnowledgeModal();
212
+ }
213
+ return ret;
214
+ },
215
+ [connectToKnowledge, hideConnectToKnowledgeModal, record.id],
216
+ );
217
+
218
+ const handleShowConnectToKnowledgeModal = useCallback(
219
+ (record: IFile) => {
220
+ setRecord(record);
221
+ showConnectToKnowledgeModal();
222
+ },
223
+ [showConnectToKnowledgeModal],
224
+ );
225
+
226
+ return {
227
+ initialValue,
228
+ connectToKnowledgeLoading: loading,
229
+ onConnectToKnowledgeOk,
230
+ connectToKnowledgeVisible,
231
+ hideConnectToKnowledgeModal,
232
+ showConnectToKnowledgeModal: handleShowConnectToKnowledgeModal,
233
+ };
234
+ };
235
+
236
+ export const useHandleBreadcrumbClick = () => {
237
+ const navigate = useNavigate();
238
+
239
+ const handleBreadcrumbClick = useCallback(
240
+ (path?: string) => {
241
+ if (path) {
242
+ navigate(path);
243
+ }
244
+ },
245
+ [navigate],
246
+ );
247
+
248
+ return { handleBreadcrumbClick };
249
+ };
250
+
251
+ export const useHandleMoveFile = (
252
+ setSelectedRowKeys: (keys: string[]) => void,
253
+ ) => {
254
+ const {
255
+ visible: moveFileVisible,
256
+ hideModal: hideMoveFileModal,
257
+ showModal: showMoveFileModal,
258
+ } = useSetModalState();
259
+ const { moveFile, loading } = useMoveFile();
260
+ const [sourceFileIds, setSourceFileIds] = useState<string[]>([]);
261
+
262
+ const onMoveFileOk = useCallback(
263
+ async (targetFolderId: string) => {
264
+ const ret = await moveFile({
265
+ src_file_ids: sourceFileIds,
266
+ dest_file_id: targetFolderId,
267
+ });
268
+
269
+ if (ret === 0) {
270
+ setSelectedRowKeys([]);
271
+ hideMoveFileModal();
272
+ }
273
+ return ret;
274
+ },
275
+ [moveFile, hideMoveFileModal, sourceFileIds, setSelectedRowKeys],
276
+ );
277
+
278
+ const handleShowMoveFileModal = useCallback(
279
+ (ids: string[]) => {
280
+ setSourceFileIds(ids);
281
+ showMoveFileModal();
282
+ },
283
+ [showMoveFileModal],
284
+ );
285
+
286
+ return {
287
+ initialValue: '',
288
+ moveFileLoading: loading,
289
+ onMoveFileOk,
290
+ moveFileVisible,
291
+ hideMoveFileModal,
292
+ showMoveFileModal: handleShowMoveFileModal,
293
+ };
294
+ };
web/src/pages/files/index.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ListFilterBar from '@/components/list-filter-bar';
2
+ import { Upload } from 'lucide-react';
3
+ import { FilesTable } from './files-table';
4
+
5
+ export default function Files() {
6
+ return (
7
+ <section className="p-8">
8
+ <ListFilterBar title="Files">
9
+ <Upload />
10
+ Upload file
11
+ </ListFilterBar>
12
+ <FilesTable></FilesTable>
13
+ </section>
14
+ );
15
+ }
web/src/pages/flow/canvas/index.tsx CHANGED
@@ -1,7 +1,6 @@
1
  import {
2
  Tooltip,
3
  TooltipContent,
4
- TooltipProvider,
5
  TooltipTrigger,
6
  } from '@/components/ui/tooltip';
7
  import {
@@ -175,24 +174,20 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
175
  <Background />
176
  <Controls>
177
  <ControlButton onClick={handleImportJson}>
178
- <TooltipProvider>
179
- <Tooltip>
180
- <TooltipTrigger asChild>
181
- <FolderInput className={controlIconClassname} />
182
- </TooltipTrigger>
183
- <TooltipContent>Import</TooltipContent>
184
- </Tooltip>
185
- </TooltipProvider>
186
  </ControlButton>
187
  <ControlButton onClick={handleExportJson}>
188
- <TooltipProvider>
189
- <Tooltip>
190
- <TooltipTrigger asChild>
191
- <FolderOutput className={controlIconClassname} />
192
- </TooltipTrigger>
193
- <TooltipContent>Export</TooltipContent>
194
- </Tooltip>
195
- </TooltipProvider>
196
  </ControlButton>
197
  </Controls>
198
  </ReactFlow>
 
1
  import {
2
  Tooltip,
3
  TooltipContent,
 
4
  TooltipTrigger,
5
  } from '@/components/ui/tooltip';
6
  import {
 
174
  <Background />
175
  <Controls>
176
  <ControlButton onClick={handleImportJson}>
177
+ <Tooltip>
178
+ <TooltipTrigger asChild>
179
+ <FolderInput className={controlIconClassname} />
180
+ </TooltipTrigger>
181
+ <TooltipContent>Import</TooltipContent>
182
+ </Tooltip>
 
 
183
  </ControlButton>
184
  <ControlButton onClick={handleExportJson}>
185
+ <Tooltip>
186
+ <TooltipTrigger asChild>
187
+ <FolderOutput className={controlIconClassname} />
188
+ </TooltipTrigger>
189
+ <TooltipContent>Export</TooltipContent>
190
+ </Tooltip>
 
 
191
  </ControlButton>
192
  </Controls>
193
  </ReactFlow>
web/src/pages/flow/flow-tooltip.tsx CHANGED
@@ -1,7 +1,6 @@
1
  import {
2
  Tooltip,
3
  TooltipContent,
4
- TooltipProvider,
5
  TooltipTrigger,
6
  } from '@/components/ui/tooltip';
7
  import { PropsWithChildren } from 'react';
@@ -10,13 +9,11 @@ import { useTranslation } from 'react-i18next';
10
  export const RunTooltip = ({ children }: PropsWithChildren) => {
11
  const { t } = useTranslation();
12
  return (
13
- <TooltipProvider>
14
- <Tooltip>
15
- <TooltipTrigger>{children}</TooltipTrigger>
16
- <TooltipContent>
17
- <p>{t('flow.testRun')}</p>
18
- </TooltipContent>
19
- </Tooltip>
20
- </TooltipProvider>
21
  );
22
  };
 
1
  import {
2
  Tooltip,
3
  TooltipContent,
 
4
  TooltipTrigger,
5
  } from '@/components/ui/tooltip';
6
  import { PropsWithChildren } from 'react';
 
9
  export const RunTooltip = ({ children }: PropsWithChildren) => {
10
  const { t } = useTranslation();
11
  return (
12
+ <Tooltip>
13
+ <TooltipTrigger>{children}</TooltipTrigger>
14
+ <TooltipContent>
15
+ <p>{t('flow.testRun')}</p>
16
+ </TooltipContent>
17
+ </Tooltip>
 
 
18
  );
19
  };
web/src/pages/knowledge/knowledge-card/index.tsx CHANGED
@@ -40,7 +40,7 @@ const KnowledgeCard = ({ item }: IProps) => {
40
 
41
  return (
42
  <Badge.Ribbon
43
- text={item.nickname}
44
  color={userInfo.nickname === item.nickname ? '#1677ff' : 'pink'}
45
  className={classNames(styles.ribbon, {
46
  [styles.hideRibbon]: item.permission !== 'team',
 
40
 
41
  return (
42
  <Badge.Ribbon
43
+ text={item?.nickname}
44
  color={userInfo.nickname === item.nickname ? '#1677ff' : 'pink'}
45
  className={classNames(styles.ribbon, {
46
  [styles.hideRibbon]: item.permission !== 'team',
web/src/pages/login-next/index.tsx CHANGED
@@ -97,8 +97,8 @@ const Login = () => {
97
  const step = Number((searchParams.get('step') ?? Step.SignIn) as Step);
98
 
99
  return (
100
- <div className="w-full h-full flex items-center pl-[15%] bg-[url('@/assets/svg/next-login-bg.svg')]">
101
- <div className="inline-block">
102
  {step === Step.SignIn && <SignInCard></SignInCard>}
103
  {step === Step.SignUp && <SignUpCard></SignUpCard>}
104
  {step === Step.VerifyEmail && <VerifyEmailCard></VerifyEmailCard>}
 
97
  const step = Number((searchParams.get('step') ?? Step.SignIn) as Step);
98
 
99
  return (
100
+ <div className="w-full h-full flex items-center pl-[15%] bg-[url('@/assets/svg/next-login-bg.svg')] bg-cover bg-center">
101
+ <div className="inline-block bg-colors-background-neutral-standard rounded-lg">
102
  {step === Step.SignIn && <SignInCard></SignInCard>}
103
  {step === Step.SignUp && <SignUpCard></SignUpCard>}
104
  {step === Step.VerifyEmail && <VerifyEmailCard></VerifyEmailCard>}
web/src/routes.ts CHANGED
@@ -7,6 +7,7 @@ export enum Routes {
7
  Agent = '/agent',
8
  Search = '/next-search',
9
  Chat = '/next-chat',
 
10
  ProfileSetting = '/profile-setting',
11
  DatasetTesting = '/testing',
12
  DatasetSetting = '/setting',
@@ -189,6 +190,17 @@ const routes = [
189
  },
190
  ],
191
  },
 
 
 
 
 
 
 
 
 
 
 
192
  {
193
  path: Routes.DatasetBase,
194
  layout: false,
 
7
  Agent = '/agent',
8
  Search = '/next-search',
9
  Chat = '/next-chat',
10
+ Files = '/files',
11
  ProfileSetting = '/profile-setting',
12
  DatasetTesting = '/testing',
13
  DatasetSetting = '/setting',
 
190
  },
191
  ],
192
  },
193
+ {
194
+ path: Routes.Files,
195
+ layout: false,
196
+ component: '@/layouts/next',
197
+ routes: [
198
+ {
199
+ path: Routes.Files,
200
+ component: `@/pages${Routes.Files}`,
201
+ },
202
+ ],
203
+ },
204
  {
205
  path: Routes.DatasetBase,
206
  layout: false,
web/src/utils/common-util.ts CHANGED
@@ -113,3 +113,28 @@ export function hexToArrayBuffer(input: string) {
113
 
114
  return view.buffer;
115
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  return view.buffer;
115
  }
116
+
117
+ export function formatFileSize(bytes: number, si = true, dp = 1) {
118
+ let nextBytes = bytes;
119
+ const thresh = si ? 1000 : 1024;
120
+
121
+ if (Math.abs(bytes) < thresh) {
122
+ return nextBytes + ' B';
123
+ }
124
+
125
+ const units = si
126
+ ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
127
+ : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
128
+ let u = -1;
129
+ const r = 10 ** dp;
130
+
131
+ do {
132
+ nextBytes /= thresh;
133
+ ++u;
134
+ } while (
135
+ Math.round(Math.abs(nextBytes) * r) / r >= thresh &&
136
+ u < units.length - 1
137
+ );
138
+
139
+ return nextBytes.toFixed(dp) + ' ' + units[u];
140
+ }
web/typings.d.ts CHANGED
@@ -1,5 +1,13 @@
 
1
  declare module 'lodash';
2
 
3
  declare global {
4
  type Nullable<T> = T | null;
5
  }
 
 
 
 
 
 
 
 
1
+ import '@tanstack/react-table';
2
  declare module 'lodash';
3
 
4
  declare global {
5
  type Nullable<T> = T | null;
6
  }
7
+
8
+ declare module '@tanstack/react-table' {
9
+ interface ColumnMeta {
10
+ headerClassName?: string;
11
+ cellClassName?: string;
12
+ }
13
+ }