refactor(ui): update ui for user settings, etc. (#13532)

### What problem does this PR solve?

Update UI styles:
- **User settings**
- Component styles: 
   - `ui/button.tsx`
   - `ui/checkbox.tsx`
   - `avatar-upload.tsx`
   - `file-uploader.tsx`
   - `icon-font.tsx`

### Type of change

- [x] Refactoring
This commit is contained in:
Jimmy Ben Klieve
2026-03-12 13:33:36 +08:00
committed by GitHub
parent 0da9c4618d
commit 31a8184f63
25 changed files with 687 additions and 621 deletions

View File

@@ -318,7 +318,7 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
data-testid={uploadInputTestId}
/>
</div>
<div className="margin-1 text-sm text-text-secondary">
<div className="ms-1 text-xs text-text-secondary">
{tips ?? t('knowledgeConfiguration.photoTip')}
</div>

View File

@@ -76,7 +76,7 @@ function UploadForm({ submit, showParseOnCreation }: UploadFormProps) {
data-testid="parse-on-creation-toggle"
onCheckedChange={field.onChange}
checked={field.value}
></Switch>
/>
)}
</RAGFlowFormItem>
)}
@@ -85,7 +85,7 @@ function UploadForm({ submit, showParseOnCreation }: UploadFormProps) {
<FileUploader
value={field.value}
onValueChange={field.onChange}
accept={{ '*': [] }}
accept={{}}
data-testid="dataset-upload-dropzone"
/>
)}
@@ -124,10 +124,7 @@ export function FileUploadDialog({
</TabsContent>
<TabsContent value="password">{t('common.comingSoon')}</TabsContent>
</Tabs> */}
<UploadForm
submit={onOk!}
showParseOnCreation={showParseOnCreation}
></UploadForm>
<UploadForm submit={onOk!} showParseOnCreation={showParseOnCreation} />
<DialogFooter>
<ButtonLoading type="submit" loading={loading} form={UploadFormId}>
{t('common.save')}

View File

@@ -2,7 +2,7 @@
'use client';
import { FileText, FolderUp, Upload, X } from 'lucide-react';
import { FileText, FolderUp, LucideTrash2, Upload } from 'lucide-react';
import * as React from 'react';
import Dropzone, {
type DropzoneProps,
@@ -80,12 +80,12 @@ function FileCard({ file, progress, onRemove }: FileCardProps) {
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
variant="delete"
size="icon"
className="size-7"
onClick={onRemove}
>
<X className="size-4" aria-hidden="true" />
<LucideTrash2 className="size-4" aria-hidden="true" />
<span className="sr-only">Remove file</span>
</Button>
</div>
@@ -300,71 +300,67 @@ export function FileUploader(props: FileUploaderProps) {
const isDisabled = disabled || (files?.length ?? 0) >= maxFileCount;
const renderDropzone = (isFolderMode: boolean = false) => (
<Dropzone
onDrop={onDrop}
accept={isFolderMode ? undefined : accept}
maxSize={maxSize}
maxFiles={maxFileCount}
multiple={maxFileCount > 1 || multiple}
disabled={isDisabled}
noClick={isFolderMode}
noDrag={isFolderMode}
>
{({ getRootProps, getInputProps, isDragActive }) => (
<div
{...getRootProps()}
className={cn(
'group relative grid h-72 w-full cursor-pointer place-items-center rounded-lg border border-dashed border-border-default px-5 py-2.5 text-center transition hover:bg-border-button bg-bg-card',
'ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
isDragActive && 'border-border-button',
isDisabled && 'pointer-events-none opacity-60',
className,
)}
{...dropzoneProps}
>
{!isFolderMode && <input {...getInputProps()} />}
{isDragActive && !isFolderMode ? (
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<Upload
className="size-7 text-text-secondary transition-colors group-hover:text-text-primary"
aria-hidden="true"
/>
</div>
<p className="font-medium text-text-secondary">
{t('fileManager.dropFilesHere', 'Drop the files here')}
</p>
</div>
) : (
<div
className="flex flex-col items-center justify-center gap-4 sm:px-5"
onClick={() => {
if (isFolderMode && !isDisabled) {
folderInputRef.current?.click();
}
}}
>
<div className="rounded-full border border-dashed p-3">
{isFolderMode ? (
<FolderUp
className="size-7 text-text-secondary transition-colors group-hover:text-text-primary"
aria-hidden="true"
/>
) : (
const renderDropzone = (isFolderMode: boolean = false) => {
const IconComponent = isFolderMode ? FolderUp : Upload;
return (
<Dropzone
onDrop={onDrop}
accept={isFolderMode ? undefined : accept}
maxSize={maxSize}
maxFiles={maxFileCount}
multiple={maxFileCount > 1 || multiple}
disabled={isDisabled}
noClick={isFolderMode}
noDrag={isFolderMode}
>
{({ getRootProps, getInputProps, isDragActive }) => (
<div
{...getRootProps()}
className={cn(
'group relative grid h-72 w-full cursor-pointer place-items-center rounded-lg border border-dashed border-border-default',
'px-5 py-2.5 text-center transition hover:bg-bg-card outline-none',
'focus-visible:border-accent-primary focus-visible:bg-bg-card',
isDragActive && 'border-border-button',
isDisabled && 'pointer-events-none opacity-60',
className,
)}
{...dropzoneProps}
>
{!isFolderMode && <input {...getInputProps()} />}
{isDragActive && !isFolderMode ? (
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div>
<Upload
className="size-7 text-text-secondary transition-colors group-hover:text-text-primary"
aria-hidden="true"
/>
)}
</div>
<p className="font-medium text-text-secondary">
{t('fileManager.dropFilesHere', 'Drop the files here')}
</p>
</div>
<div className="flex flex-col gap-px">
<p className="font-medium text-text-secondary ">
) : (
<div
className="flex flex-col items-center justify-center gap-4 sm:px-5"
onClick={() => {
if (isFolderMode && !isDisabled) {
folderInputRef.current?.click();
}
}}
>
<IconComponent
className="size-12 stroke-1 text-text-secondary transition-colors group-hover:text-text-primary"
aria-hidden="true"
/>
<p className="font-medium text-text-secondary">
{title ||
(isFolderMode
? t('fileManager.uploadFolderTitle', 'Upload Folder')
: t('knowledgeDetails.uploadTitle'))}
</p>
<p className="text-sm text-text-disabled">
{description ||
(isFolderMode
@@ -375,12 +371,12 @@ export function FileUploader(props: FileUploaderProps) {
: t('knowledgeDetails.uploadDescription'))}
</p>
</div>
</div>
)}
</div>
)}
</Dropzone>
);
)}
</div>
)}
</Dropzone>
);
};
return (
<div className="relative flex flex-col gap-4 overflow-hidden">
@@ -417,7 +413,7 @@ export function FileUploader(props: FileUploaderProps) {
)}
{files?.length ? (
<div className="h-fit w-full px-3">
<div className="h-fit w-full">
<div className="flex max-h-48 flex-col gap-4 overflow-auto scrollbar-auto">
{files?.map((file, index) => (
<FileCard

View File

@@ -21,14 +21,12 @@ export function IconFontFill({
isFill = true,
}: IconFontType & { isFill?: boolean }) {
return (
<span className={cn('size-4', className)}>
<svg
className={cn('size-4', className)}
style={{ fill: isFill ? 'currentColor' : '' }}
>
<use xlinkHref={`#icon-${name}`} />
</svg>
</span>
<svg
className={cn('size-4', className)}
style={{ fill: isFill ? 'currentColor' : '' }}
>
<use xlinkHref={`#icon-${name}`} />
</svg>
);
}

View File

@@ -68,6 +68,13 @@ const buttonVariants = cva(
hover:bg-state-error/10 focus-visible:bg-state-error/10
`,
'danger-hover': `
bg-bg-input border-border-button
hover:bg-state-error/10 focus-visible:bg-state-error/10
hover:text-state-error focus-visible:text-state-error
hover:border-state-error focus-visible:border-state-error
`,
// Ghost variant series
// Button has transparent background, without borders
ghost: `
@@ -91,11 +98,11 @@ const buttonVariants = cva(
size: {
auto: '',
xl: 'h-12 rounded-xl px-5',
xl: 'h-12 rounded-xl px-5 gap-3',
lg: 'h-10 rounded-lg px-4',
default: 'h-8 rounded px-3',
sm: 'h-7 rounded-sm px-2',
xs: 'h-6 rounded-xs px-1',
sm: 'h-7 rounded-sm px-2 gap-1',
xs: 'h-6 rounded-xs px-1 gap-0.5',
'icon-xl': 'size-12 rounded-xl',
'icon-lg': 'size-10 rounded-lg',

View File

@@ -1,7 +1,7 @@
'use client';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { LucideCheck } from 'lucide-react';
import { LucideCheck, LucideMinus } from 'lucide-react';
import * as React from 'react';
import { cn } from '@/lib/utils';
@@ -23,7 +23,12 @@ const Checkbox = React.forwardRef<
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
<LucideCheck className="size-2.5 stroke-[3]" />
{props.checked === 'indeterminate' && (
<LucideMinus className="size-2.5 stroke-[3]" />
)}
{props.checked === true && (
<LucideCheck className="size-2.5 stroke-[3]" />
)}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));

View File

@@ -41,6 +41,7 @@ export const fileIconMap = {
xml: 'xml.svg',
};
// TODO: Need to migrate to standard BCP 47 language tag
export const LanguageList = [
'English',
'Chinese',
@@ -68,7 +69,7 @@ export const LanguageMap = {
Vietnamese: 'Tiếng việt',
Japanese: '日本語',
'Portuguese BR': 'Português BR',
German: 'German',
German: 'Deutsch',
French: 'Français',
Italian: 'Italiano',
Bulgarian: 'Български',

View File

@@ -360,11 +360,11 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
filesSelected: 'Files selected',
upload: 'Upload',
run: 'Parse',
runningStatus0: 'PENDING',
runningStatus1: 'PARSING',
runningStatus2: 'CANCELED',
runningStatus3: 'SUCCESS',
runningStatus4: 'FAIL',
runningStatus0: 'Pending',
runningStatus1: 'Parsing',
runningStatus2: 'Cancelled',
runningStatus3: 'Success',
runningStatus4: 'Fail',
pageRanges: 'Page ranges',
pageRangesTip:
'Range of pages to be parsed; pages outside this range will not be processed.',

View File

@@ -1,4 +1,3 @@
import FileStatusBadge from '@/components/file-status-badge';
import { FilterCollection } from '@/components/list-filter-bar/interface';
import SvgIcon from '@/components/svg-icon';
import { useIsDarkTheme } from '@/components/theme-provider';
@@ -192,12 +191,7 @@ const FileLogsPage: FC = () => {
return {
id: value,
// label: RunningStatusMap[value].label,
label: (
<FileStatusBadge
status={value as RunningStatus}
name={RunningStatusMap[value as RunningStatus]}
/>
),
label: RunningStatusMap[value],
};
}),
},

View File

@@ -72,9 +72,9 @@ export function SideBar({ refreshCount }: PropType) {
}, [t, routerData]);
return (
<aside className="w-64 relative px-5 space-y-8">
<aside className="flex flex-col w-64 relative">
<header
className="grid grid-cols-[auto_1fr] grid-rows-[auto_auto] gap-x-3"
className="px-5 pb-4 grid grid-cols-[auto_1fr] grid-rows-[auto_auto] gap-x-3"
style={{
gridTemplateAreas: '"avatar title" "avatar stats"',
}}
@@ -110,27 +110,31 @@ export function SideBar({ refreshCount }: PropType) {
</div>
</header>
<div className="flex flex-col gap-5">
{items.map((item, itemIdx) => {
const active = '/' + pathName === item.key;
<nav className="px-5 pt-1 pb-5 overflow-y-auto">
<ul className="space-y-5">
{items.map((item) => {
const active = '/' + pathName === item.key;
return (
<Button
key={itemIdx}
asLink
variant="ghost"
className={cn(
'w-full justify-start gap-2.5 px-3 relative h-10 text-base',
active && 'bg-bg-card text-text-primary',
)}
to={`${Routes.DatasetBase}${item.key}/${id}`}
>
{item.icon}
<span>{item.label}</span>
</Button>
);
})}
</div>
return (
<li key={item.key}>
<Button
asLink
block
variant="ghost"
className={cn(
'justify-start gap-2.5 px-3 relative h-10 text-base',
active && 'bg-bg-card text-text-primary',
)}
to={`${Routes.DatasetBase}${item.key}/${id}`}
>
{item.icon}
<span>{item.label}</span>
</Button>
</li>
);
})}
</ul>
</nav>
</aside>
);
}

View File

@@ -78,12 +78,12 @@ export function ActionCell({
}, [handleRemoveFile, documentId]);
return (
<section className="flex gap-4 items-center text-text-sub-title-invert opacity-0 group-hover:opacity-100 transition-opacity">
<section className="flex gap-2 items-center text-text-sub-title-invert opacity-0 group-hover:opacity-100 transition-opacity">
{isKnowledgeBase || (
<Button
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
size={'sm'}
size="icon-sm"
onClick={handleShowConnectToKnowledgeModal}
>
<Link2 />
@@ -93,7 +93,7 @@ export function ActionCell({
<Button
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
size={'sm'}
size="icon-sm"
onClick={handleShowMoveFileModal}
>
<FolderInput />
@@ -103,7 +103,7 @@ export function ActionCell({
<Button
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
size={'sm'}
size="icon-sm"
onClick={handleShowFileRenameModal}
>
<FolderPen />
@@ -113,7 +113,7 @@ export function ActionCell({
<Button
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
size={'sm'}
size="icon-sm"
onClick={onDownloadDocument}
>
<ArrowDownToLine />
@@ -129,7 +129,7 @@ export function ActionCell({
<Button
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
size={'sm'}
size="icon-sm"
>
<Eye />
</Button>
@@ -185,7 +185,7 @@ export function ActionCell({
<Button
variant="transparent"
className="border-none hover:bg-bg-card text-text-primary"
size={'sm'}
size="icon-sm"
>
<Trash2 />
</Button>

View File

@@ -36,7 +36,6 @@ import {
import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection';
import { useFetchFileList } from '@/hooks/use-file-request';
import { IFile } from '@/interfaces/database/file-manager';
import { cn } from '@/lib/utils';
import { formatFileSize } from '@/utils/common-util';
import { formatDate } from '@/utils/date';
import { pick } from 'lodash';
@@ -122,13 +121,18 @@ export function FilesTable({
accessorKey: 'name',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
<div className="flex items-center gap-1">
{t('name')}
<ArrowUpDown />
</Button>
<Button
size="icon-xs"
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === 'asc')
}
>
<ArrowUpDown />
</Button>
</div>
);
},
meta: { cellClassName: 'max-w-[20vw]' },
@@ -147,21 +151,18 @@ export function FilesTable({
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex gap-2">
<span className="size-4">
<FileIcon name={name} type={type}></FileIcon>
</span>
<span
className={cn('truncate', { ['cursor-pointer']: isFolder })}
onClick={handleNameClick}
>
{name}
</span>
</div>
<Button
variant="static"
onClick={handleNameClick}
className="max-w-full p-0 flex justify-start gap-2 text-text-primary"
>
<FileIcon name={name} type={type} />
<span className="truncate">{name}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{name}</p>
</TooltipContent>
<TooltipContent>{name}</TooltipContent>
</Tooltip>
);
},
@@ -170,13 +171,18 @@ export function FilesTable({
accessorKey: 'create_time',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
<div className="flex items-center gap-1">
{t('uploadDate')}
<ArrowUpDown />
</Button>
<Button
variant="ghost"
size="icon-xs"
onClick={() =>
column.toggleSorting(column.getIsSorted() === 'asc')
}
>
<ArrowUpDown />
</Button>
</div>
);
},
cell: ({ row }) => (
@@ -189,13 +195,18 @@ export function FilesTable({
accessorKey: 'size',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
<div className="flex items-center gap-1">
{t('size')}
<ArrowUpDown />
</Button>
<Button
variant="ghost"
size="icon-xs"
onClick={() =>
column.toggleSorting(column.getIsSorted() === 'asc')
}
>
<ArrowUpDown />
</Button>
</div>
);
},
cell: ({ row }) => (
@@ -213,6 +224,9 @@ export function FilesTable({
{
id: 'actions',
header: t('action'),
meta: {
headerCellClassName: 'w-0',
},
enableHiding: false,
enablePinning: true,
cell: ({ row }) => {
@@ -222,7 +236,7 @@ export function FilesTable({
showConnectToKnowledgeModal={showConnectToKnowledgeModal}
showFileRenameModal={showFileRenameModal}
showMoveFileModal={showMoveFileModal}
></ActionCell>
/>
);
},
},
@@ -271,7 +285,12 @@ export function FilesTable({
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
<TableHead
key={header.id}
className={
header.column.columnDef.meta?.headerCellClassName
}
>
{header.isPlaceholder
? null
: flexRender(

View File

@@ -11,7 +11,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { useRowSelection } from '@/hooks/logic-hooks/use-row-selection';
import { useFetchFileList } from '@/hooks/use-file-request';
import { Upload } from 'lucide-react';
import { LucidePlus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { CreateFolderDialog } from './create-folder-dialog';
import { FileBreadcrumb } from './file-breadcrumb';
@@ -87,35 +87,38 @@ export default function Files() {
);
return (
<section className="p-8">
<ListFilterBar
leftPanel={leftPanel}
searchString={searchString}
onSearchChange={handleInputChange}
showFilter={false}
icon={'file'}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>
<Upload />
{t('knowledgeDetails.addFile')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem onClick={showFileUploadModal}>
{t('fileManager.uploadFile')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={showFolderCreateModal}>
{t('fileManager.newFolder')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ListFilterBar>
{!rowSelectionIsEmpty && (
<BulkOperateBar list={list} count={selectedCount}></BulkOperateBar>
)}
<article className="p-8">
<header>
<ListFilterBar
className="mb-4"
leftPanel={leftPanel}
searchString={searchString}
onSearchChange={handleInputChange}
showFilter={false}
icon={'file'}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>
<LucidePlus />
{t('knowledgeDetails.addFile')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem onClick={showFileUploadModal}>
{t('fileManager.uploadFile')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={showFolderCreateModal}>
{t('fileManager.newFolder')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ListFilterBar>
{!rowSelectionIsEmpty && (
<BulkOperateBar className="mb-4" list={list} count={selectedCount} />
)}
</header>
<FilesTable
files={files}
total={total}
@@ -148,6 +151,6 @@ export default function Files() {
loading={moveFileLoading}
></MoveDialog>
)}
</section>
</article>
);
}

View File

@@ -1,26 +1,7 @@
import Spotlight from '@/components/spotlight';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { PropsWithChildren } from 'react';
export const UserSettingHeader = ({
name,
description,
}: {
name: string;
description?: string;
}) => {
return (
<>
<header className="flex flex-col gap-1.5 justify-between items-start p-0">
<div className="text-2xl font-medium text-text-primary">{name}</div>
{description && (
<div className="text-sm text-text-secondary ">{description}</div>
)}
</header>
{/* <Separator className="border-border-button bg-border-button h-[0.5px]" /> */}
</>
);
};
export function Title({ children }: PropsWithChildren) {
return <span className="font-bold text-xl">{children}</span>;
}
@@ -34,11 +15,17 @@ export function ProfileSettingWrapperCard({
children,
}: ProfileSettingWrapperCardProps) {
return (
<Card className="w-full border-border-button bg-transparent relative border-[0.5px]">
<CardHeader className="border-b-[0.5px] border-border-button p-5 ">
<Card
as="article"
className="relative w-full border-border-button bg-transparent border-0.5 flex flex-col"
>
<CardHeader className="flex-0 border-b-0.5 border-border-button p-5">
{header}
</CardHeader>
<CardContent className="p-5">{children}</CardContent>
<CardContent className="flex-1 h-0 p-0">{children}</CardContent>
<Spotlight />
</Card>
);
}

View File

@@ -1,19 +1,62 @@
import { CardTitle } from '@/components/ui/card';
import { useTranslation } from 'react-i18next';
import Spotlight from '@/components/spotlight';
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';
import {
ProfileSettingWrapperCard,
UserSettingHeader,
} from '../components/user-setting-header';
import { ProfileSettingWrapperCard } from '../components/user-setting-header';
import AddDataSourceModal from './add-datasource-modal';
import { AddedSourceCard } from './component/added-source-card';
import { DataSourceKey, useDataSourceInfo } from './constant';
import { useAddDataSource, useListDataSource } from './hooks';
import { IDataSorceInfo } from './interface';
const AvailableSourceCard = ({
name,
description,
icon,
onAdd,
}: IDataSorceInfo & { onAdd: () => void }) => {
const { t } = useTranslation();
return (
<article
className="
size-full p-2.5 border-0.5 border-border-button rounded-lg relative group hover:bg-bg-card focus-within:bg-bg-card
grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-x-2.5 gap-y-1"
style={{
gridTemplateAreas: '"icon title" "icon description"',
}}
onClick={() => onAdd()}
>
<span className="w-6" style={{ gridArea: 'icon' }}>
{icon}
</span>
<header className="flex items-center gap-2" style={{ gridArea: 'title' }}>
<h3 className="text-base text-text-primary">{name}</h3>
<Button
size="auto"
className="ml-auto px-1 py-0.5 gap-0.5 text-xs items-center opacity-0 transition-all group-hover:opacity-100 group-focus-within:opacity-100"
onClick={(e: any) => {
e.stopPropagation();
onAdd();
}}
>
<Plus className="size-[1em]" />
{t('setting.add')}
</Button>
</header>
<p
style={{ gridArea: 'description' }}
className="text-xs text-text-secondary"
>
{description}
</p>
</article>
);
};
const DataSource = () => {
const { t } = useTranslation();
const { dataSourceInfo } = useDataSourceInfo();
@@ -38,97 +81,68 @@ const DataSource = () => {
showAddingModal,
} = useAddDataSource();
const AbailableSourceCard = ({
id,
name,
description,
icon,
}: IDataSorceInfo) => {
return (
<div
className="p-[10px] border border-border-button rounded-lg relative group hover:bg-bg-card"
onClick={() =>
showAddingModal({
id,
name,
description,
icon,
})
}
>
<div className="flex gap-2">
<div className="w-6 h-6">{icon}</div>
<div className="flex flex-1 flex-col items-start gap-2">
<div className="text-base text-text-primary">{name}</div>
<div className="text-xs text-text-secondary">{description}</div>
</div>
</div>
<div className=" absolute top-2 right-2">
<Button className=" rounded-md px-1 text-bg-base gap-1 bg-text-primary text-xs py-0 h-6 items-center hidden group-hover:flex">
<Plus size={12} />
{t('setting.add')}
</Button>
</div>
</div>
);
};
return (
<ProfileSettingWrapperCard
header={
<UserSettingHeader
name={t('setting.dataSources')}
description={t('setting.datasourceDescription')}
/>
<header>
<h2 className="text-2xl font-medium text-text-primary">
{t('setting.dataSources')}
</h2>
<p className="mt-1 text-sm text-text-secondary ">
{t('setting.datasourceDescription')}
</p>
</header>
}
>
<Spotlight />
<div className="relative">
<div className=" flex flex-col gap-4 max-h-[calc(100vh-235px)] overflow-y-auto overflow-x-hidden scrollbar-auto">
<div className="flex flex-col gap-3">
{categorizedList?.length <= 0 && (
<div className="text-text-secondary w-full flex justify-center items-center h-20">
{t('setting.sourceEmptyTip')}
</div>
)}
{categorizedList.map((item, index) => (
<AddedSourceCard key={index} {...item} />
))}
</div>
<section className="bg-transparent border-none mt-8">
<header className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-2xl font-semibold ">
{t('setting.availableSources')}
<div className="text-sm text-text-secondary font-normal mt-1.5">
{t('setting.availableSourcesDescription')}
</div>
</CardTitle>
</header>
<main className="p-0">
{/* <TenantTable searchTerm={searchTerm}></TenantTable> */}
<div className="grid sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-4 3xl:grid-cols-4 gap-4">
{dataSourceTemplates.map((item, index) => (
<AbailableSourceCard {...item} key={index} />
))}
</div>
</main>
</section>
</div>
<div className="h-full p-5 overflow-x-hidden overflow-y-auto">
<section className="flex flex-col gap-3">
{categorizedList?.length <= 0 && (
<div className="text-text-secondary w-full flex justify-center items-center h-20">
{t('setting.sourceEmptyTip')}
</div>
)}
{categorizedList.map((item, index) => (
<AddedSourceCard key={index} {...item} />
))}
</section>
{addingModalVisible && (
<AddDataSourceModal
visible
loading={addLoading}
hideModal={hideAddingModal}
onOk={(data) => {
console.log(data);
handleAddOk(data);
}}
sourceData={addSource}
></AddDataSourceModal>
)}
<section className="mt-8">
<header className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<h2 className="text-2xl font-medium">
{t('setting.availableSources')}
<div className="text-sm text-text-secondary font-normal mt-1.5">
{t('setting.availableSourcesDescription')}
</div>
</h2>
</header>
{/* <TenantTable searchTerm={searchTerm}></TenantTable> */}
<ul className="@container grid sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-4 3xl:grid-cols-4 gap-4">
{dataSourceTemplates.map((item) => (
<li key={item.id} className="h-full">
<AvailableSourceCard
{...item}
onAdd={() => showAddingModal(item)}
/>
</li>
))}
</ul>
</section>
</div>
{addingModalVisible && (
<AddDataSourceModal
visible
loading={addLoading}
hideModal={hideAddingModal}
onOk={(data) => {
console.log(data);
handleAddOk(data);
}}
sourceData={addSource}
/>
)}
</ProfileSettingWrapperCard>
);
};

View File

@@ -196,7 +196,7 @@ export function EditMcpDialog({
form={form}
setFieldChanged={setFieldChanged}
></EditMcpForm>
<Card className="bg-transparent">
<Card className="bg-transparent mt-4">
<CardContent className="p-3">
<Collapse
title={

View File

@@ -3,7 +3,6 @@ import {
ConfirmDeleteDialog,
ConfirmDeleteDialogNode,
} from '@/components/confirm-delete-dialog';
import Spotlight from '@/components/spotlight';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { SearchInput } from '@/components/ui/input';
@@ -61,112 +60,119 @@ export default function McpServer() {
return (
<ProfileSettingWrapperCard
header={
<>
<div className="text-text-primary text-2xl font-semibold">
{t('mcp.mcpServers')}
</div>
<section className="flex items-center justify-between">
<div className="text-text-secondary">
<header className="flex flex-row gap-1.5 justify-between items-end">
<div>
<h2 className="text-text-primary text-2xl font-medium">
{t('mcp.mcpServers')}
</h2>
<p className="mt-1 text-text-secondary text-sm">
{t('mcp.customizeTheListOfMcpServers')}
</div>
<div className="flex gap-5">
<SearchInput
className="w-40"
value={searchString}
onChange={handleInputChange}
placeholder={t('common.search')}
></SearchInput>
<Button variant={'secondary'} onClick={switchSelectionMode}>
{isSelectionMode ? (
<ListChecks className="size-3.5" />
) : (
<LayoutList className="size-3.5" />
)}
{t(`mcp.${isSelectionMode ? 'exitBulkManage' : 'bulkManage'}`)}
</Button>
<Button variant={'secondary'} onClick={showImportModal}>
<Download className="size-3.5" />
{t('mcp.import')}
</Button>
<Button onClick={showEditModal('')}>
<Plus className="size-3.5 font-medium" /> {t('mcp.addMCP')}
</Button>
</div>
</section>
</>
</p>
</div>
<div className="flex gap-4" role="toolbar">
<SearchInput
className="w-40"
value={searchString}
onChange={handleInputChange}
placeholder={t('common.search')}
/>
<Button variant="outline" onClick={switchSelectionMode}>
{isSelectionMode ? (
<ListChecks className="size-3.5" />
) : (
<LayoutList className="size-3.5" />
)}
{t(`mcp.${isSelectionMode ? 'exitBulkManage' : 'bulkManage'}`)}
</Button>
<Button variant="outline" onClick={showImportModal}>
<Download className="size-3.5" />
{t('mcp.import')}
</Button>
<Button onClick={showEditModal('')}>
<Plus /> {t('mcp.addMCP')}
</Button>
</div>
</header>
}
>
{!data.mcp_servers?.length && (
<div
className="flex items-center justify-between border border-dashed border-border-button rounded-md p-10 cursor-pointer w-[590px]"
onClick={showEditModal('')}
>
<div className="text-text-secondary text-sm">{t('empty.noMCP')}</div>
<Button variant={'ghost'} className="border border-border-button">
<Plus className="size-3.5 font-medium" /> {t('empty.addNow')}
</Button>
</div>
)}
{!!data.mcp_servers?.length && (
<>
{isSelectionMode && (
<section className="pb-5 flex items-center">
<Checkbox id="all" onCheckedChange={handleSelectAll} />
<Label
className="pl-2 text-text-primary cursor-pointer"
htmlFor="all"
>
{t('common.selectAll')}
</Label>
<span className="text-text-secondary pr-10 pl-5">
{t('mcp.selected')} {selectedList.length}
</span>
<div className="flex gap-10 items-center">
<Button variant={'secondary'} onClick={handleExportMcp}>
<Upload className="size-3.5"></Upload>
{t('mcp.export')}
</Button>
<ConfirmDeleteDialog
onOk={handleDelete}
title={t('common.delete') + ' ' + t('mcp.mcpServers')}
content={{
title: t('common.deleteThem'),
node: (
<ConfirmDeleteDialogNode
name={`${t('mcp.selected')} ${selectedList.length} ${t('mcp.mcpServers')}`}
></ConfirmDeleteDialogNode>
),
}}
<div className="h-full p-5 overflow-x-hidden overflow-y-auto">
{data.mcp_servers?.length ? (
<>
{isSelectionMode && (
<section className="pb-5 flex items-center">
<Checkbox id="all" onCheckedChange={handleSelectAll} />
<Label
className="pl-2 text-text-primary cursor-pointer"
htmlFor="all"
>
<Button variant={'danger'}>
<Trash2 className="size-3.5 cursor-pointer" />
{t('common.delete')}
{t('common.selectAll')}
</Label>
<span className="text-text-secondary pr-10 pl-5">
{t('mcp.selected')} {selectedList.length}
</span>
<div className="flex gap-10 items-center">
<Button variant={'secondary'} onClick={handleExportMcp}>
<Upload className="size-3.5"></Upload>
{t('mcp.export')}
</Button>
</ConfirmDeleteDialog>
</div>
</section>
)}
<CardContainer>
{data.mcp_servers.map((item) => (
<McpCard
key={item.id}
data={item}
selectedList={selectedList}
handleSelectChange={handleSelectChange}
showEditModal={showEditModal}
isSelectionMode={isSelectionMode}
></McpCard>
))}
</CardContainer>
<div className="mt-8">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={pagination.total || 0}
onChange={handlePageChange}
></RAGFlowPagination>
<ConfirmDeleteDialog
onOk={handleDelete}
title={t('common.delete') + ' ' + t('mcp.mcpServers')}
content={{
title: t('common.deleteThem'),
node: (
<ConfirmDeleteDialogNode
name={`${t('mcp.selected')} ${selectedList.length} ${t('mcp.mcpServers')}`}
></ConfirmDeleteDialogNode>
),
}}
>
<Button variant={'danger'}>
<Trash2 className="size-3.5 cursor-pointer" />
{t('common.delete')}
</Button>
</ConfirmDeleteDialog>
</div>
</section>
)}
<CardContainer>
{data.mcp_servers.map((item) => (
<McpCard
key={item.id}
data={item}
selectedList={selectedList}
handleSelectChange={handleSelectChange}
showEditModal={showEditModal}
isSelectionMode={isSelectionMode}
></McpCard>
))}
</CardContainer>
<div className="mt-8">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={pagination.total || 0}
onChange={handlePageChange}
></RAGFlowPagination>
</div>
</>
) : (
<div
className="flex items-center justify-between border border-dashed border-border-button rounded-md p-10 cursor-pointer w-[590px]"
onClick={showEditModal('')}
>
<div className="text-text-secondary text-sm">
{t('empty.noMCP')}
</div>
<Button variant="outline">
<Plus />
{t('empty.addNow')}
</Button>
</div>
</>
)}
)}
</div>
{editVisible && (
<EditMcpDialog
hideModal={hideEditModal}
@@ -181,7 +187,6 @@ export default function McpServer() {
onOk={onImportOk}
></ImportMcpDialog>
)}
<Spotlight />
</ProfileSettingWrapperCard>
);
}

View File

@@ -22,10 +22,7 @@ import { Loader2Icon, PenLine } from 'lucide-react';
import { FC, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
ProfileSettingWrapperCard,
UserSettingHeader,
} from '../components/user-setting-header';
import { ProfileSettingWrapperCard } from '../components/user-setting-header';
import { EditType, modalTitle, useProfile } from './hooks/use-profile';
const baseSchema = z.object({
@@ -123,10 +120,14 @@ const ProfilePage: FC = () => {
// <div className="h-full w-full text-text-secondary relative flex flex-col gap-4">
<ProfileSettingWrapperCard
header={
<UserSettingHeader
name={t('profile')}
description={t('profileDescription')}
/>
<header>
<h2 className="text-2xl font-medium text-text-primary">
{t('profile')}
</h2>
<p className="mt-1 text-sm text-text-secondary ">
{t('profileDescription')}
</p>
</header>
}
>
<Spotlight />
@@ -142,11 +143,11 @@ const ProfilePage: FC = () => {
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
{profile.userName}
</div>
<Button
variant={'ghost'}
variant="outline"
type="button"
onClick={() => handleEditClick(EditType.editName)}
className="text-sm text-text-secondary flex gap-1 px-1 border border-border-button"
>
<PenLine size={12} /> {t('edit')}
</Button>
@@ -175,10 +176,9 @@ const ProfilePage: FC = () => {
{profile.timeZone}
</div>
<Button
variant={'ghost'}
variant="outline"
type="button"
onClick={() => handleEditClick(EditType.editTimeZone)}
className="text-sm text-text-secondary flex gap-1 px-1 border border-border-button"
>
<PenLine size={12} /> {t('edit')}
</Button>
@@ -208,10 +208,9 @@ const ProfilePage: FC = () => {
{profile.currPasswd ? '********' : ''}
</div>
<Button
variant={'ghost'}
variant="outline"
type="button"
onClick={() => handleEditClick(EditType.editPassword)}
className="text-sm text-text-secondary flex gap-1 px-1 border border-border-button"
>
<PenLine size={12} /> {t('edit')}
</Button>

View File

@@ -93,26 +93,22 @@ export const ModelProviderCard: FC<IModelCardProps> = ({
<div className="flex items-center space-x-2">
<Button
variant={'ghost'}
variant="outline"
onClick={(e) => {
e.stopPropagation();
handleApiKeyClick();
}}
className="px-3 py-1 text-sm rounded-md transition-colors flex items-center space-x-1 border border-border-default"
>
<SettingOutlined />
<span>
{isLocalLlmFactory(item.name) ? t('addTheModel') : 'API-Key'}
</span>
{isLocalLlmFactory(item.name) ? t('addTheModel') : 'API-Key'}
</Button>
<Button
variant={'ghost'}
variant="outline"
onClick={(e) => {
e.stopPropagation();
handleShowMoreClick();
}}
className="px-3 py-1 text-sm rounded-md transition-colors flex items-center space-x-1 border border-border-default"
>
<span>{visible ? t('hideModels') : t('showMoreModels')}</span>
{!visible ? <ChevronsDown /> : <ChevronsUp />}
@@ -133,12 +129,12 @@ export const ModelProviderCard: FC<IModelCardProps> = ({
}}
>
<Button
variant={'ghost'}
size="icon"
variant="danger-hover"
// onClick={(e) => {
// e.stopPropagation();
// handleDeleteFactory(item);
// }}
className=" hover:text-state-error hover:bg-state-error-5 transition-colors border border-border-default"
>
<Trash2 />
</Button>
@@ -161,9 +157,9 @@ export const ModelProviderCard: FC<IModelCardProps> = ({
))}
</div>
<div className="m-4 bg-bg-card rounded-lg max-h-96 overflow-auto scrollbar-auto">
<div className="">
<ul>
{item.llm.map((model) => (
<div
<li
key={model.name}
className="flex items-center border-b-[0.5px] border-border-button justify-between p-3 hover:bg-bg-card transition-colors"
>
@@ -176,36 +172,38 @@ export const ModelProviderCard: FC<IModelCardProps> = ({
</span>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center gap-3">
{isLocalLlmFactory(item.name) && (
<Button
variant={'secondary'}
variant="secondary"
size="icon-sm"
onClick={() => handleEditModel(model, item)}
className="p-1 text-text-primary transition-colors"
>
<EditOutlined />
</Button>
)}
<Switch
checked={model.status === '1'}
onCheckedChange={(value) => {
handleEnableLlm(model.name, value);
}}
/>
<Button
variant={'ghost'}
size="icon-sm"
variant="danger-hover"
onClick={(e) => {
e.stopPropagation();
handleDeleteLlm(model.name);
}}
className="p-1 hover:text-state-error hover:bg-state-error-5 transition-colors border-none"
>
<Trash2 size={16} />
</Button>
</div>
</div>
</li>
))}
</div>
</ul>
</div>
</div>
)}

View File

@@ -173,13 +173,16 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
};
return (
<div className="rounded-lg w-full">
<div className="flex flex-col py-4">
<div className="text-2xl font-medium">{t('systemModelSettings')}</div>
<div className="text-sm text-text-secondary">
<article className="rounded-lg w-full">
<header className="py-5">
<h2 className="text-2xl font-medium text-text-primary">
{t('systemModelSettings')}
</h2>
<p className="mt-1 text-sm text-text-secondary ">
{t('systemModelDescription')}
</div>
</div>
</p>
</header>
<div className="px-7 py-6 space-y-6 max-h-[70vh] overflow-y-auto border border-border-button rounded-lg">
{llmList.map((item) => (
<Items key={item.id} {...item} />
@@ -194,7 +197,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
{t('common:cancel')}
</Button>
</div> */}
</div>
</article>
);
};

View File

@@ -79,97 +79,97 @@ export const AvailableModels: FC<{
};
return (
<div
className=" text-text-primary h-full p-4"
<aside
className="text-text-primary h-full flex flex-col"
data-testid="available-models-section"
>
<div className="text-text-primary text-base mb-4">
{t('availableModels')}
</div>
{/* Search Bar */}
<div className="mb-6">
{/* <div className="relative"> */}
<SearchInput
data-testid="model-providers-search"
type="text"
placeholder={t('search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-4 py-2 pl-10 bg-bg-input border border-border-default rounded-lg focus:outline-none focus:ring-1 focus:ring-border-button transition-colors"
/>
{/* <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-text-secondary" /> */}
{/* </div> */}
</div>
<header className="p-4 space-y-3">
<h3 className="text-text-primary text-base">{t('availableModels')}</h3>
{/* Search Bar */}
<div>
{/* <div className="relative"> */}
<SearchInput
data-testid="model-providers-search"
type="text"
placeholder={t('search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-4 py-2 pl-10 bg-bg-input border border-border-default rounded-lg focus:outline-none focus:ring-1 focus:ring-border-button transition-colors"
/>
{/* <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-text-secondary" /> */}
{/* </div> */}
</div>
{/* Tags Filter */}
<div className="flex flex-wrap gap-2 mb-6">
<Button
variant={'secondary'}
onClick={() => setSelectedTag(null)}
className={`px-1 py-1 text-xs rounded-sm bg-bg-card h-5 transition-colors ${
selectedTag === null
? ' text-bg-base bg-text-primary '
: 'text-text-secondary bg-bg-card border-none'
}`}
>
All
</Button>
{allTags.map((tag) => (
{/* Tags Filter */}
<div className="flex flex-wrap gap-2">
<Button
variant={'secondary'}
key={tag}
onClick={() => handleTagClick(tag)}
className={`px-1 py-1 text-xs rounded-sm bg-bg-card h-5 transition-colors ${
selectedTag === tag
? ' text-bg-base bg-text-primary '
: 'text-text-secondary border-none bg-bg-card'
}`}
variant={selectedTag === null ? 'default' : 'secondary'}
size="auto"
className="leading-none px-1 py-0.5 rounded-sm text-xs"
onClick={() => setSelectedTag(null)}
>
{mapModelKey[tag.trim() as keyof typeof mapModelKey] || tag.trim()}
All
</Button>
))}
</div>
{allTags.map((tag) => (
<Button
variant={selectedTag === tag ? 'default' : 'secondary'}
size="auto"
className="leading-none px-1 py-0.5 rounded-sm text-xs"
key={tag}
onClick={() => handleTagClick(tag)}
>
{mapModelKey[tag.trim() as keyof typeof mapModelKey] ||
tag.trim()}
</Button>
))}
</div>
</header>
{/* Models List */}
<div className="flex flex-col gap-4 overflow-auto h-[calc(100vh-300px)] scrollbar-auto">
<div className="p-4 pt-0 flex flex-col gap-4 overflow-auto h-full scrollbar-auto">
{filteredModels.map((model) => (
<div
key={model.name}
data-testid="available-model-card"
data-provider={model.name}
className=" border border-border-button rounded-lg p-3 hover:bg-bg-input transition-colors group"
className="group border border-border-button rounded-lg p-3 hover:bg-bg-input transition-colors"
onClick={() => handleAddModel(model.name)}
>
<div className="flex items-center space-x-3 mb-3">
<LlmIcon name={model.name} imgClass="h-8 w-8 text-text-primary" />
<div className="flex flex-1 gap-1 items-center">
<div className="flex flex-1 gap-1.5 items-center">
<div className="font-normal text-base truncate">
{model.name}
</div>
{!!APIMapUrl[model.name as keyof typeof APIMapUrl] && (
<Button
variant={'ghost'}
className=" bg-transparent w-4 h-5"
onClick={(e) => {
e.stopPropagation();
window.open(
APIMapUrl[model.name as keyof typeof APIMapUrl],
);
}}
// target="_blank"
asLink
variant="ghost"
size="icon-xs"
className="size-4"
to={APIMapUrl[model.name as keyof typeof APIMapUrl]}
target="_blank"
rel="noopener noreferrer"
onClick={(e: React.MouseEvent<HTMLButtonElement>) =>
e.stopPropagation()
}
>
<ArrowUpRight size={16} />
</Button>
)}
</div>
<Button className=" px-2 items-center gap-0 text-xs h-6 rounded-md transition-colors hidden group-hover:flex">
<Button
size="xs"
className="px-2 opacity-0 transition-all group-hover:opacity-100 group-focus-within:opacity-100"
>
<Plus size={12} />
{t('addTheModel')}
</Button>
</div>
<div className="flex flex-wrap gap-1 mb-3">
<div className="flex flex-wrap gap-1">
{sortTags(model.tags).map((tag, index) => (
<span
key={index}
@@ -184,6 +184,6 @@ export const AvailableModels: FC<{
</div>
))}
</div>
</div>
</aside>
);
};

View File

@@ -10,10 +10,7 @@ import Spotlight from '@/components/spotlight';
import { SearchInput } from '@/components/ui/input';
import { UserPlus } from 'lucide-react';
import { useState } from 'react';
import {
ProfileSettingWrapperCard,
UserSettingHeader,
} from '../components/user-setting-header';
import { ProfileSettingWrapperCard } from '../components/user-setting-header';
import AddingUserModal from './add-user-modal';
import { useAddUser } from './hooks';
import TenantTable from './tenant-table';
@@ -40,53 +37,60 @@ const UserSettingTeam = () => {
// />
<ProfileSettingWrapperCard
header={
<UserSettingHeader
name={userInfo?.nickname + ' ' + t('setting.workspace')}
/>
<header>
<h2 className="text-2xl font-medium text-text-primary">
{userInfo?.nickname + ' ' + t('setting.workspace')}
</h2>
</header>
}
>
<Spotlight />
<Card className="bg-transparent border-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-4">
{/* <User className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-base">
{t('setting.teamMembers')}
</CardTitle>
<section className="flex gap-4 items-center">
<div className="h-full overflow-x-hidden overflow-y-auto">
<Card className="bg-transparent border-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4">
{/* <User className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-base">
{t('setting.teamMembers')}
</CardTitle>
<section className="flex gap-4 items-center">
<SearchInput
className="bg-bg-input border-border-default w-32"
placeholder={t('common.search')}
value={searchUser}
onChange={(e) => setSearchUser(e.target.value)}
/>
<Button onClick={showAddingTenantModal}>
<UserPlus className=" h-4 w-4" />
{t('setting.invite')}
</Button>
</section>
</CardHeader>
<CardContent className="p-4 pt-0">
<UserTable searchUser={searchUser}></UserTable>
</CardContent>
</Card>
<Card className="bg-transparent border-none mt-8">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-base w-fit">
{t('setting.joinedTeams')}
</CardTitle>
<SearchInput
className="bg-bg-input border-border-default w-32"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder={t('common.search')}
value={searchUser}
onChange={(e) => setSearchUser(e.target.value)}
/>
<Button onClick={showAddingTenantModal}>
<UserPlus className=" h-4 w-4" />
{t('setting.invite')}
</Button>
</section>
</CardHeader>
<CardContent className="p-4">
<UserTable searchUser={searchUser}></UserTable>
</CardContent>
</Card>
<Card className="bg-transparent border-none mt-8">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-4">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-base w-fit">
{t('setting.joinedTeams')}
</CardTitle>
<SearchInput
className="bg-bg-input border-border-default w-32"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder={t('common.search')}
/>
</CardHeader>
<CardContent className="p-4">
<TenantTable searchTerm={searchTerm}></TenantTable>
</CardContent>
</Card>
</CardHeader>
<CardContent className="p-4 pt-0">
<TenantTable searchTerm={searchTerm}></TenantTable>
</CardContent>
</Card>
</div>
{addingTenantModalVisible && (
<AddingUserModal

View File

@@ -63,11 +63,11 @@ const TenantTable = ({ searchTerm }: { searchTerm: string }) => {
const renderSortIcon = () => {
if (sortOrder === 'asc') {
return <ArrowUp className="ml-1 h-4 w-4" />;
return <ArrowUp className="size-[1em]" />;
} else if (sortOrder === 'desc') {
return <ArrowDown className="ml-1 h-4 w-4" />;
return <ArrowDown className="size-[1em]" />;
} else {
return <ArrowUpDown className="ml-1 h-4 w-4" />;
return <ArrowUpDown className="size-[1em]" />;
}
};
@@ -77,13 +77,16 @@ const TenantTable = ({ searchTerm }: { searchTerm: string }) => {
<TableHeader className="bg-bg-title">
<TableRow className="hover:bg-bg-title">
<TableHead className="h-12 px-4">{t('common.name')}</TableHead>
<TableHead
className="h-12 px-4 cursor-pointer"
onClick={toggleSortOrder}
>
<div className="flex items-center">
<TableHead className="h-12 px-4">
<div className="flex items-center gap-1">
{t('setting.updateDate')}
{renderSortIcon()}
<Button
variant="ghost"
size="icon-xs"
onClick={toggleSortOrder}
>
{renderSortIcon()}
</Button>
</div>
</TableHead>
<TableHead className="h-12 px-4">{t('setting.email')}</TableHead>

View File

@@ -71,11 +71,11 @@ const UserTable = ({ searchUser }: { searchUser: string }) => {
const renderSortIcon = () => {
if (sortOrder === 'asc') {
return <ArrowUp className="ml-1 h-4 w-4 " />;
return <ArrowUp className="size-[1em] " />;
} else if (sortOrder === 'desc') {
return <ArrowDown className="ml-1 h-4 w-4" />;
return <ArrowDown className="size-[1em]" />;
} else {
return <ArrowUpDown className="ml-1 h-4 w-4" />;
return <ArrowUpDown className="size-[1em]" />;
}
};
return (
@@ -84,13 +84,16 @@ const UserTable = ({ searchUser }: { searchUser: string }) => {
<TableHeader className="bg-bg-title">
<TableRow className="hover:bg-bg-title">
<TableHead className="h-12 px-4">{t('common.name')}</TableHead>
<TableHead
className="h-12 px-4 cursor-pointer"
onClick={toggleSortOrder}
>
<div className="flex items-center">
<TableHead className="h-12 px-4">
<div className="flex items-center gap-1">
{t('setting.updateDate')}
{renderSortIcon()}
<Button
variant="ghost"
size="icon-xs"
onClick={toggleSortOrder}
>
{renderSortIcon()}
</Button>
</div>
</TableHead>
<TableHead className="h-12 px-4">{t('setting.email')}</TableHead>
@@ -110,7 +113,7 @@ const UserTable = ({ searchUser }: { searchUser: string }) => {
) : sortedData && sortedData.length > 0 ? (
sortedData.map((record) => (
<TableRow key={record.user_id} className="hover:bg-bg-card">
<TableCell className="p-4 ">
<TableCell className="p-4">
<div className="flex gap-1 items-center">
<RAGFlowAvatar
isPerson

View File

@@ -3,7 +3,6 @@ import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import ThemeSwitch from '@/components/theme-switch';
import { Button } from '@/components/ui/button';
import { Domain } from '@/constants/common';
import { useSecondPathName } from '@/hooks/route-hook';
import { useLogout } from '@/hooks/use-login-request';
import {
useFetchSystemVersion,
@@ -12,18 +11,49 @@ import {
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { TFunction } from 'i18next';
import { Banknote, Box, Server, Unplug, User, Users } from 'lucide-react';
import {
LucideBox,
LucideServer,
LucideUnplug,
LucideUser,
LucideUsers,
} from 'lucide-react';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useHandleMenuClick } from './hooks';
const menuItems = (t: TFunction) => [
{ icon: Server, label: t('setting.dataSources'), key: Routes.DataSource },
{ icon: Box, label: t('setting.model'), key: Routes.Model },
{ icon: Banknote, label: 'MCP', key: Routes.Mcp },
{ icon: Users, label: t('setting.team'), key: Routes.Team },
{ icon: User, label: t('setting.profile'), key: Routes.Profile },
{ icon: Unplug, label: t('setting.api'), key: Routes.Api },
{
icon: <LucideServer className="size-[1em]" />,
label: t('setting.dataSources'),
key: Routes.DataSource,
},
{
icon: <LucideBox className="size-[1em]" />,
label: t('setting.model'),
key: Routes.Model,
'data-testid': 'settings-nav-model-providers',
},
{
icon: <IconFontFill name="mcp" className="size-[1em]" />,
label: 'MCP',
key: Routes.Mcp,
},
{
icon: <LucideUsers className="size-[1em]" />,
label: t('setting.team'),
key: Routes.Team,
},
{
icon: <LucideUser className="size-[1em]" />,
label: t('setting.profile'),
key: Routes.Profile,
},
{
icon: <LucideUnplug className="size-[1em]" />,
label: t('setting.api'),
key: Routes.Api,
},
// {
// icon: MessageSquareQuote,
// label: 'Prompt Templates',
@@ -33,10 +63,10 @@ const menuItems = (t: TFunction) => [
// { icon: Cog, label: t('setting.system'), key: Routes.System },
// { icon: Banknote, label: 'Plan', key: Routes.Plan },
];
export function SideBar() {
const pathName = useSecondPathName();
const { data: userInfo } = useFetchUserInfo();
const { handleMenuClick, active } = useHandleMenuClick();
const { handleMenuClick, active: activeItemKey } = useHandleMenuClick();
const { version, fetchSystemVersion } = useFetchSystemVersion();
const { t } = useTranslation();
useEffect(() => {
@@ -48,40 +78,38 @@ export function SideBar() {
return (
<aside className="w-[303px] bg-bg-base flex flex-col">
<div className="px-6 flex gap-2 items-center">
<RAGFlowAvatar
avatar={userInfo?.avatar}
name={userInfo?.nickname}
isPerson
/>
<p className="text-sm text-text-primary">{userInfo?.email}</p>
</div>
<div className="flex-1 overflow-auto">
{menuItems(t).map((item, idx) => {
const hoverKey = pathName === item.key;
return (
<div key={idx}>
<div key={idx} className="mx-6 my-5 ">
<header>
<h1 className="px-6 flex gap-2.5 items-center font-normal">
<RAGFlowAvatar
avatar={userInfo?.avatar}
name={userInfo?.nickname}
isPerson
/>
<p className="text-sm text-text-primary">{userInfo?.email}</p>
</h1>
</header>
<nav className="flex-1 overflow-auto mt-4 py-1">
<ul className="px-6 flex flex-col gap-5">
{menuItems(t).map((item) => {
const { key, icon, label, ...rest } = item;
return (
<li key={key}>
<Button
variant={hoverKey ? 'secondary' : 'ghost'}
className={cn('w-full justify-between gap-2.5 p-3 relative', {
'bg-bg-card text-text-primary': active === item.key,
'bg-bg-base text-text-secondary': active !== item.key,
})}
data-testid={
item.key === Routes.Model
? 'settings-nav-model-providers'
: undefined
}
onClick={handleMenuClick(item.key)}
{...rest}
block
variant="ghost"
className={cn(
'justify-start gap-2.5 px-3 relative h-10 text-base',
activeItemKey === key && 'bg-bg-card text-text-primary',
)}
onClick={handleMenuClick(key)}
>
<section className="flex items-center gap-2.5">
{item.key === Routes.Mcp ? (
<IconFontFill name={'mcp'} className="size-4 w-4 h-4" />
) : (
<item.icon className="w-6 h-6" />
)}
<span>{item.label}</span>
{icon}
<span>{label}</span>
</section>
{/* {item.key === Routes.System && (
<div className="mr-2 px-2 bg-accent-primary-5 text-accent-primary rounded-md">
@@ -92,24 +120,22 @@ export function SideBar() {
<div className="absolute right-0 w-[5px] h-[66px] bg-primary rounded-l-xl shadow-[0_0_5.94px_#7561ff,0_0_11.88px_#7561ff,0_0_41.58px_#7561ff,0_0_83.16px_#7561ff,0_0_142.56px_#7561ff,0_0_249.48px_#7561ff]" />
)} */}
</Button>
</div>
</div>
);
})}
</div>
</li>
);
})}
</ul>
</nav>
<div className="p-6 mt-auto ">
<footer className="p-6 mt-auto">
<div className="flex items-center gap-2 mb-6 justify-between">
<div className="mr-2 px-2 text-accent-primary rounded-md">
{version}
</div>
<span className="text-accent-primary">{version}</span>
<ThemeSwitch />
</div>
<Button block size="lg" variant="transparent" onClick={() => logout()}>
{t('setting.logout')}
</Button>
</div>
</footer>
</aside>
);
}