Adjust styles to match the design system (#11118)

### What problem does this PR solve?

- Modify and adjust styles (CSS vars, components) to match the design
system
- Adjust file and directory structure of admin UI

### Type of change

- [x] Refactoring
This commit is contained in:
Jimmy Ben Klieve
2025-11-10 10:05:19 +08:00
committed by GitHub
parent 660386d3b5
commit 1cd54832b5
42 changed files with 685 additions and 539 deletions

View File

@ -2,6 +2,9 @@ import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { useIsDarkTheme } from '../theme-provider';
import noDataIcon from './no data bri.svg';
import noDataIconDark from './no data.svg';
type EmptyProps = {
className?: string;
children?: React.ReactNode;
@ -10,6 +13,14 @@ type EmptyProps = {
const EmptyIcon = () => {
const isDarkTheme = useIsDarkTheme();
return (
<img
className="h-20"
src={isDarkTheme ? noDataIconDark : noDataIcon}
alt={t('common.noData')}
/>
);
return (
<svg
width="184"
@ -67,13 +78,14 @@ const Empty = (props: EmptyProps) => {
return (
<div
className={cn(
'flex flex-col justify-center items-center text-center gap-3',
'flex flex-col justify-center items-center text-center gap-2',
className,
)}
>
<EmptyIcon />
{!children && (
<div className="empty-text mt-4 text-text-secondary">
<div className="empty-text text-text-secondary">
{t('common.noData')}
</div>
)}

View File

@ -0,0 +1,24 @@
<svg width="42" height="52" viewBox="0 0 42 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.175 16.22L6 30V42C6 43.0609 6.31607 44.0783 6.87868 44.8284C7.44129 45.5786 8.20435 46 9 46H33C33.7956 46 34.5587 45.5786 35.1213 44.8284C35.6839 44.0783 36 43.0609 36 42V30L30.825 16.22C30.5766 15.5536 30.1938 14.9927 29.7194 14.6006C29.2451 14.2084 28.6981 14.0004 28.14 14H13.86C13.3019 14.0004 12.7549 14.2084 12.2806 14.6006C11.8062 14.9927 11.4234 15.5536 11.175 16.22Z" stroke="url(#paint0_linear_493_44259)" stroke-opacity="0.2" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.175 8.22L6 22V34C6 35.0609 6.31607 36.0783 6.87868 36.8284C7.44129 37.5786 8.20435 38 9 38H33C33.7957 38 34.5587 37.5786 35.1213 36.8284C35.6839 36.0783 36 35.0609 36 34V22L30.825 8.22C30.5766 7.55357 30.1938 6.99274 29.7194 6.60056C29.2451 6.20838 28.6981 6.00039 28.14 6H13.86C13.3019 6.00039 12.7549 6.20838 12.2806 6.60056C11.8062 6.99274 11.4234 7.55357 11.175 8.22Z" fill="white" stroke="url(#paint1_linear_493_44259)" stroke-opacity="0.8" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.175 12.22L6 26V38C6 39.0609 6.31607 40.0783 6.87868 40.8284C7.44129 41.5786 8.20435 42 9 42H33C33.7957 42 34.5587 41.5786 35.1213 40.8284C35.6839 40.0783 36 39.0609 36 38V26L30.825 12.22C30.5766 11.5536 30.1938 10.9927 29.7194 10.6006C29.2451 10.2084 28.6981 10.0004 28.14 10H13.86C13.3019 10.0004 12.7549 10.2084 12.2806 10.6006C11.8062 10.9927 11.4234 11.5536 11.175 12.22Z" fill="white" stroke="url(#paint2_linear_493_44259)" stroke-opacity="0.5" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 22.1641H27L24 28.1641H18L15 22.1641H6M36 22.1641V34.1641C36 35.2249 35.6839 36.2423 35.1213 36.9925C34.5587 37.7426 33.7956 38.1641 33 38.1641H9C8.20435 38.1641 7.44129 37.7426 6.87868 36.9925C6.31607 36.2423 6 35.2249 6 34.1641V22.1641M36 22.1641L30.825 8.38406C30.5766 7.71764 30.1938 7.1568 29.7194 6.76462C29.2451 6.37244 28.6981 6.16446 28.14 6.16406H13.86C13.3019 6.16446 12.7549 6.37244 12.2806 6.76462C11.8062 7.1568 11.4234 7.71764 11.175 8.38406L6 22.1641" stroke="url(#paint3_linear_493_44259)" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<linearGradient id="paint0_linear_493_44259" x1="21" y1="14" x2="9.71922" y2="50.3541" gradientUnits="userSpaceOnUse">
<stop offset="0.195167" stop-color="#161618" stop-opacity="0"/>
<stop offset="1" stop-color="#C2C2C2"/>
</linearGradient>
<linearGradient id="paint1_linear_493_44259" x1="21" y1="6" x2="9.71922" y2="42.3541" gradientUnits="userSpaceOnUse">
<stop offset="0.195167" stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#C2C2C2"/>
</linearGradient>
<linearGradient id="paint2_linear_493_44259" x1="21" y1="10" x2="9.71922" y2="46.3541" gradientUnits="userSpaceOnUse">
<stop offset="0.195167" stop-color="#161618" stop-opacity="0"/>
<stop offset="1" stop-color="#C2C2C2"/>
</linearGradient>
<linearGradient id="paint3_linear_493_44259" x1="21" y1="6.16406" x2="21" y2="38.1641" gradientUnits="userSpaceOnUse">
<stop stop-color="#161618"/>
<stop offset="1" stop-color="#7B7B7C"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,24 @@
<svg width="42" height="52" viewBox="0 0 42 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.175 16.22L6 30V42C6 43.0609 6.31607 44.0783 6.87868 44.8284C7.44129 45.5786 8.20435 46 9 46H33C33.7956 46 34.5587 45.5786 35.1213 44.8284C35.6839 44.0783 36 43.0609 36 42V30L30.825 16.22C30.5766 15.5536 30.1938 14.9927 29.7194 14.6006C29.2451 14.2084 28.6981 14.0004 28.14 14H13.86C13.3019 14.0004 12.7549 14.2084 12.2806 14.6006C11.8062 14.9927 11.4234 15.5536 11.175 16.22Z" stroke="url(#paint0_linear_486_6708)" stroke-opacity="0.2" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.175 8.22L6 22V34C6 35.0609 6.31607 36.0783 6.87868 36.8284C7.44129 37.5786 8.20435 38 9 38H33C33.7957 38 34.5587 37.5786 35.1213 36.8284C35.6839 36.0783 36 35.0609 36 34V22L30.825 8.22C30.5766 7.55357 30.1938 6.99274 29.7194 6.60056C29.2451 6.20838 28.6981 6.00039 28.14 6H13.86C13.3019 6.00039 12.7549 6.20838 12.2806 6.60056C11.8062 6.99274 11.4234 7.55357 11.175 8.22Z" fill="#222224" stroke="url(#paint1_linear_486_6708)" stroke-opacity="0.8" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.175 12.22L6 26V38C6 39.0609 6.31607 40.0783 6.87868 40.8284C7.44129 41.5786 8.20435 42 9 42H33C33.7957 42 34.5587 41.5786 35.1213 40.8284C35.6839 40.0783 36 39.0609 36 38V26L30.825 12.22C30.5766 11.5536 30.1938 10.9927 29.7194 10.6006C29.2451 10.2084 28.6981 10.0004 28.14 10H13.86C13.3019 10.0004 12.7549 10.2084 12.2806 10.6006C11.8062 10.9927 11.4234 11.5536 11.175 12.22Z" fill="#222224" stroke="url(#paint2_linear_486_6708)" stroke-opacity="0.5" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M36 22.1641H27L24 28.1641H18L15 22.1641H6M36 22.1641V34.1641C36 35.2249 35.6839 36.2423 35.1213 36.9925C34.5587 37.7426 33.7956 38.1641 33 38.1641H9C8.20435 38.1641 7.44129 37.7426 6.87868 36.9925C6.31607 36.2423 6 35.2249 6 34.1641V22.1641M36 22.1641L30.825 8.38406C30.5766 7.71764 30.1938 7.1568 29.7194 6.76462C29.2451 6.37244 28.6981 6.16446 28.14 6.16406H13.86C13.3019 6.16446 12.7549 6.37244 12.2806 6.76462C11.8062 7.1568 11.4234 7.71764 11.175 8.38406L6 22.1641" stroke="url(#paint3_linear_486_6708)" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<linearGradient id="paint0_linear_486_6708" x1="21" y1="14" x2="9.71922" y2="50.3541" gradientUnits="userSpaceOnUse">
<stop offset="0.195167" stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#C2C2C2"/>
</linearGradient>
<linearGradient id="paint1_linear_486_6708" x1="21" y1="6" x2="9.71922" y2="42.3541" gradientUnits="userSpaceOnUse">
<stop offset="0.195167" stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#C2C2C2"/>
</linearGradient>
<linearGradient id="paint2_linear_486_6708" x1="21" y1="10" x2="9.71922" y2="46.3541" gradientUnits="userSpaceOnUse">
<stop offset="0.195167" stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="#C2C2C2"/>
</linearGradient>
<linearGradient id="paint3_linear_486_6708" x1="21" y1="6.16406" x2="21" y2="38.1641" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#7B7B7C"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -1,4 +1,5 @@
import { useIsDarkTheme } from '@/components/theme-provider';
import { cn } from '@/lib/utils';
import { parseColorToRGB } from '@/utils/common-util';
import React from 'react';
@ -36,7 +37,7 @@ const Spotlight: React.FC<SpotlightProps> = ({
: '194, 221, 243';
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
className={cn('absolute inset-0 opacity-80 rounded-lg', className)}
style={{
backdropFilter: 'blur(30px)',
zIndex: -1,

View File

@ -39,7 +39,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
'flex h-full w-full items-center justify-center rounded-full bg-bg-member',
className,
)}
{...props}

View File

@ -4,16 +4,18 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors outline-none focus:outline-none',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
'border-transparent bg-bg-primary text-text-primary hover:bg-primary/80',
secondary:
'border-transparent bg-bg-card text-text-sub-title-invert hover:bg-secondary/80 rounded-md',
'border-transparent bg-bg-card text-text-secondary rounded-md',
success:
'border-transparent bg-state-success/5 text-state-success rounded-md',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
'border-transparent bg-state-error/5 text-state-error rounded-md',
outline: 'text-foreground',
},
},

View File

@ -3,28 +3,56 @@ import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { Loader2, Plus } from 'lucide-react';
import { LucideLoader2, Plus } from 'lucide-react';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
cn(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors outline-0',
'disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0',
),
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
'bg-text-primary text-bg-base shadow-xs hover:bg-text-primary/90 focus-visible:bg-text-primary/90',
destructive: `
bg-state-error text-white shadow-xs
hover:bg-state-error/90 focus-visible:ring-state-error/20 dark:focus-visible:ring-state-error/40
`,
outline: `
text-text-secondary bg-bg-input border-0.5 border-border-button
hover:text-text-primary hover:bg-border-button hover:border-border-default
focus-visible:text-text-primary focus-visible:bg-border-button focus-visible:border-border-button
`,
secondary:
'bg-bg-input text-text-primary shadow-xs hover:bg-bg-input/80 border border-border-button',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
ghost: `
text-text-secondary
hover:bg-border-button hover:text-text-primary
focus-visible:text-text-primary focus-visible:bg-border-button
`,
link: 'text-primary underline-offset-4 hover:underline',
icon: 'bg-colors-background-inverse-standard text-foreground hover:bg-colors-background-inverse-standard/80',
dashed: 'border border-dashed border-input hover:bg-accent',
transparent: 'bg-transparent hover:bg-accent border',
danger: 'bg-transparent border border-state-error text-state-error',
transparent: `
text-text-secondary bg-transparent border-0.5 border-border-button
hover:text-text-primary hover:bg-border-button
focus-visible:text-text-primary focus-visible:bg-border-button focus-visible:border-border-button
`,
danger: `
bg-transparent border border-state-error text-state-error
hover:bg-state-error/10 focus-visible:bg-state-error/10
`,
highlighted: `
bg-text-primary text-bg-base border-b-4 border-b-accent-primary
hover:bg-text-primary/90 focus-visible:bg-text-primary/90
`,
},
size: {
default: 'h-8 px-2.5 py-1.5 ',
@ -45,46 +73,48 @@ export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
block?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
(
{
children,
className,
variant,
size,
asChild = false,
loading = false,
disabled = false,
block = false,
...props
},
ref,
) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(
'bg-bg-card',
{ 'block w-full': block },
buttonVariants({ variant, size, className }),
)}
ref={ref}
disabled={loading || disabled}
{...props}
/>
>
{loading && <LucideLoader2 className="animate-spin" />}
{children}
</Comp>
);
},
);
Button.displayName = 'Button';
export const ButtonLoading = React.forwardRef<
HTMLButtonElement,
Omit<ButtonProps, 'asChild'> & { loading?: boolean }
>(
(
{ className, variant, size, children, loading = false, disabled, ...props },
ref,
) => {
return (
<Button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
disabled={loading || disabled}
>
{loading && <Loader2 className="animate-spin" />}
{children}
</Button>
);
},
);
export const ButtonLoading = Button;
ButtonLoading.displayName = 'ButtonLoading';

View File

@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
'rounded-lg border-border-default border shadow-sm bg-bg-input',
'rounded-lg border-border-button border-0.5 shadow-sm bg-bg-input transition-shadow',
className,
)}
{...props}
@ -60,7 +60,11 @@ const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
<div
ref={ref}
className={cn('p-6 pt-0 transition-shadow', className)}
{...props}
/>
));
CardContent.displayName = 'CardContent';

View File

@ -13,7 +13,11 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-3.5 w-3.5 shrink-0 rounded-sm border border-text-secondary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
'peer size-4 shrink-0 rounded-sm border border-border-button outline-0 transition-colors bg-bg-component',
'hover:border-border-default hover:bg-border-button',
'focus-visible:border-border-default focus-visible:bg-border-default',
'disabled:cursor-not-allowed disabled:opacity-50',
'data-[state=checked]:text-text-primary data-[state=checked]:border-border-default',
className,
)}
{...props}
@ -21,7 +25,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
>
<Check className="h-3.5 w-3.5" />
<Check className="size-3" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));

View File

@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'fixed inset-0 z-50 bg-black/50 backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
@ -38,13 +38,20 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-bg-base p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'outline-0 fixed left-[50%] top-[50%] z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-bg-base p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close
className="
absolute right-4 top-4 p-2 rounded-sm opacity-70 outline-none text-text-secondary transition-colors
hover:bg-border-button hover:text-text-primary
focus-visible:bg-border-button focus-visible:text-text-primary
disabled:pointer-events-none data-[state=open]:bg-bg-accent data-[state=open]:text-muted-foreground
"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
@ -102,7 +109,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
className={cn('text-sm text-text-primary', className)}
{...props}
/>
));

View File

@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-bg-base p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}

View File

@ -97,19 +97,18 @@ const FormLabel = React.forwardRef<
required?: boolean;
}
>(({ className, tooltip, required = false, ...props }, ref) => {
const { error, formItemId } = useFormField();
const { formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && 'text-destructive', className, 'flex pb-0.5')}
className={cn(className, 'flex pb-0.5')}
htmlFor={formItemId}
{...props}
>
<section>
{required && <span className="text-destructive">*</span>}
{props.children}
</section>
{required && <span className="text-state-error">*</span>}
{props.children}
{tooltip && <FormTooltip tooltip={tooltip}></FormTooltip>}
</Label>
);
@ -171,7 +170,7 @@ const FormMessage = React.forwardRef<
<p
ref={ref}
id={formMessageId}
className={cn('text-sm font-medium text-destructive', className)}
className={cn('text-sm font-medium text-state-error', className)}
{...props}
>
{body}

View File

@ -3,14 +3,17 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
import { Eye, EyeOff, Search } from 'lucide-react';
import { useState } from 'react';
import { Button } from './button';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
value?: string | number | readonly string[] | undefined;
prefix?: React.ReactNode;
suffix?: React.ReactNode;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, value, onChange, ...props }, ref) => {
({ className, type, value, onChange, prefix, suffix, ...props }, ref) => {
const isControlled = value !== undefined;
const { defaultValue, ...restProps } = props;
const inputValue = isControlled ? value : defaultValue;
@ -29,99 +32,80 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
onChange?.(e);
}
};
return (
<>
{type !== 'password' && (
<input
type={type === 'password' && showPassword ? 'text' : type}
className={cn(
'flex h-8 w-full rounded-md border border-input bg-bg-input px-2 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-text-primary',
className,
)}
ref={ref}
value={inputValue ?? ''}
onChange={handleChange}
{...restProps}
/>
const isPasswordInput = type === 'password';
const inputEl = (
<input
ref={ref}
type={isPasswordInput && showPassword ? 'text' : type}
className={cn(
'flex h-8 w-full rounded-md border-0.5 border-input bg-bg-input px-3 py-2 outline-none text-sm text-text-primary',
'file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent-primary',
'disabled:cursor-not-allowed disabled:opacity-50 transition-colors',
{
'pl-12': !!prefix,
'pr-12': !!suffix || isPasswordInput,
'pr-24': !!suffix && isPasswordInput,
},
className,
)}
{type === 'password' && (
<div className="relative w-full">
<input
type={type === 'password' && showPassword ? 'text' : type}
className={cn(
'flex h-8 w-full rounded-md border border-input bg-bg-input px-2 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-text-primary',
className,
)}
ref={ref}
value={inputValue ?? ''}
onChange={handleChange}
{...restProps}
/>
<button
value={inputValue ?? ''}
onChange={handleChange}
{...restProps}
/>
);
if (prefix || suffix || isPasswordInput) {
return (
<div className="relative">
{prefix && (
<span className="absolute left-0 top-[50%] translate-y-[-50%]">
{prefix}
</span>
)}
{inputEl}
{suffix && (
<span
className={cn('absolute right-0 top-[50%] translate-y-[-50%]', {
'right-14': isPasswordInput,
})}
>
{suffix}
</span>
)}
{isPasswordInput && (
<Button
variant="transparent"
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
className="border-0 absolute right-1 top-[50%] translate-y-[-50%]"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-text-secondary" />
<EyeOff className="size-[1em]" />
) : (
<Eye className="h-4 w-4 text-text-secondary" />
<Eye className="size-[1em]" />
)}
</button>
</div>
)}
</>
);
</Button>
)}
</div>
);
}
return inputEl;
},
);
Input.displayName = 'Input';
export interface ExpandedInputProps extends Omit<InputProps, 'prefix'> {
prefix?: React.ReactNode;
suffix?: React.ReactNode;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ExpandedInputProps extends InputProps {}
const ExpandedInput = ({
suffix,
prefix,
className,
...props
}: ExpandedInputProps) => {
return (
<div className="relative">
<span
className={cn({
['absolute left-3 top-[50%] translate-y-[-50%]']: prefix,
})}
>
{prefix}
</span>
<Input
className={cn(
{ 'pr-8': !!suffix, 'pl-8': !!prefix },
'bg-bg-base',
className,
)}
{...props}
></Input>
<span
className={cn({
['absolute right-3 top-[50%] translate-y-[-50%]']: suffix,
})}
>
{suffix}
</span>
</div>
);
};
const ExpandedInput = Input;
const SearchInput = (props: InputProps) => {
return (
<ExpandedInput
prefix={<Search className="size-3.5" />}
{...props}
></ExpandedInput>
);
return <Input {...props} prefix={<Search className="ml-3 size-[1em]" />} />;
};
type Value = string | readonly string[] | number | undefined;

View File

@ -46,6 +46,7 @@ const PaginationLink = ({
...props
}: PaginationLinkProps) => (
<a
href="#"
aria-current={isActive ? 'page' : undefined}
className={cn(
'size-8',
@ -70,7 +71,7 @@ const PaginationPrevious = ({
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<ChevronLeft className="size-4" />
</PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';
@ -85,7 +86,7 @@ const PaginationNext = ({
className={cn('gap-1 pr-2.5', className)}
{...props}
>
<ChevronRight className="h-4 w-4" />
<ChevronRight className="size-4" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';
@ -96,7 +97,7 @@ const PaginationEllipsis = ({
}: React.ComponentProps<'span'>) => (
<span
aria-hidden
className={cn('flex h-9 w-9 items-center justify-center', className)}
className={cn('flex items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />

View File

@ -39,7 +39,11 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 w-72 rounded-md border-0.5 border-border-button bg-bg-base p-4 text-text-primary shadow-lg outline-none',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-9',
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}

View File

@ -27,7 +27,12 @@ function RadioGroupItem({
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
'text-primary aspect-square size-4 shrink-0 rounded-full',
'transition-all outline-none border border-border-button bg-bg-component',
'hover:border-border-default hover:bg-border-default',
'focus-visible:border-border-default focus-visible:bg-border-default',
'aria-[invalid]:border-state-error aria-[invalid]:bg-state-error/5 aria-[invalid]:text-state-error',
'disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
@ -36,7 +41,7 @@ function RadioGroupItem({
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
<CircleIcon className="fill-current absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);

View File

@ -49,7 +49,7 @@ function Radio({ value, checked, disabled, onChange, children }: RadioProps) {
<span
className={cn(
'flex h-4 w-4 items-center justify-center rounded-full border border-border transition-colors',
'peer ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'peer outline-none focus-visible:border-border-button',
isChecked && 'border-primary bg-primary/10',
mergedDisabled && 'border-muted',
)}

View File

@ -23,7 +23,7 @@ export type RAGFlowPaginationType = {
export function RAGFlowPagination({
current = 1,
pageSize = 10,
pageSize = 5,
total = 0,
onChange,
showSizeChanger = true,
@ -172,13 +172,14 @@ export function RAGFlowPagination({
</PaginationItem>
</PaginationContent>
</Pagination>
{showSizeChanger && (
<RAGFlowSelect
options={sizeChangerOptions}
value={currentPageSize}
onChange={handlePageSizeChange}
triggerClassName="bg-bg-card"
></RAGFlowSelect>
triggerClassName="bg-bg-card border-transparent"
/>
)}
</section>
);

View File

@ -26,7 +26,11 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-8 w-full items-center bg-bg-input justify-between rounded-md border border-input px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
'flex h-8 w-full items-center bg-bg-input justify-between rounded-md border-0.5 border-border-button',
'px-3 py-2 text-sm outline-none transition-colors text-text-secondary placeholder:text-muted-foreground',
'hover:text-text-primary hover:bg-border-button',
'focus-visible:text-text-primary focus-visible:bg-border-button',
'disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className,
)}
{...props}
@ -91,7 +95,11 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border-0.5 border-border-card bg-bg-base text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
@ -134,7 +142,8 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-2 text-sm outline-none text-text-secondary',
'focus:bg-border-button focus:text-text-primary data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}

View File

@ -11,16 +11,23 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-3.5 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-accent-primary data-[state=unchecked]:bg-text-sub-title',
'group/switch inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full',
'border-2 border-transparent overflow-hidden transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
'disabled:cursor-not-allowed disabled:opacity-50',
'data-[state=checked]:bg-accent-primary data-[state=unchecked]:bg-text-sub-title',
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block size-3 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-2 data-[state=unchecked]:translate-x-0',
)}
className="
pointer-events-none block w-3 h-3 rounded-full bg-white shadow-lg ring-0 transition-all ease-out
group-hover/switch:w-4 group-focus-visible/switch:w-4
data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0
group-hover/switch:data-[state=checked]:translate-x-2 group-focus-visible/switch:data-[state=checked]:translate-x-2
"
/>
</SwitchPrimitives.Root>
));

View File

@ -27,7 +27,10 @@ const TableHeader = React.forwardRef<
>(({ className, ...props }, ref) => (
<thead
ref={ref}
className={cn('[&_tr]:border-b top-0 sticky bg-bg-title z-10', className)}
className={cn(
'[&_tr]:border-b-0.5 top-0 sticky bg-bg-title z-10',
className,
)}
{...props}
/>
));
@ -52,7 +55,7 @@ const TableFooter = React.forwardRef<
<tfoot
ref={ref}
className={cn(
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
'border-t-0.5 border-border-button bg-muted/50 font-medium [&>tr]:last:border-b-0',
className,
)}
{...props}
@ -67,7 +70,7 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
'border-b border-border-button transition-colors hover:bg-bg-card data-[state=selected]:bg-bg-card',
'border-b-0.5 border-border-button transition-colors hover:bg-bg-card data-[state=selected]:bg-bg-card',
className,
)}
{...props}
@ -82,7 +85,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
'h-12 px-4 text-left align-middle font-normal text-text-secondary [&:has([role=checkbox])]:pr-0 ',
'first-of-type:pl-6 last-of-type:pr-6 h-14 px-4 text-left align-middle font-normal text-text-secondary [&:has([role=checkbox])]:pr-0 ',
className,
)}
{...props}
@ -97,7 +100,7 @@ const TableCell = React.forwardRef<
<td
ref={ref}
className={cn(
'p-4 align-middle [&:has([role=checkbox])]:pr-0 text-text-primary font-normal',
'first-of-type:pl-6 last-of-type:pr-6 px-4 py-3 align-middle [&:has([role=checkbox])]:pr-0 text-text-primary font-normal',
className,
)}
{...props}

View File

@ -29,7 +29,11 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-bg-base data-[state=active]:text-text-primary data-[state=active]:shadow-sm',
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5',
'text-sm font-medium ring-offset-background transition-all',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
'data-[state=active]:bg-bg-base data-[state=active]:text-text-primary data-[state=active]:shadow-sm',
className,
)}
{...props}

View File

@ -1931,13 +1931,14 @@ Important structured information may include: names, dates, locations, events, k
roles: 'Roles',
monitoring: 'Monitoring',
back: 'Back',
active: 'Active',
inactive: 'Inactive',
enable: 'Enable',
disable: 'Disable',
all: 'All',
actions: 'Actions',
newUser: 'New User',
newUser: 'New user',
email: 'Email',
name: 'Name',
nickname: 'Nickname',
@ -1956,6 +1957,7 @@ Important structured information may include: names, dates, locations, events, k
lastUpdateTime: 'Last update time',
isAnonymous: 'Is Anonymous',
isSuperuser: 'Is Superuser',
deleteUser: 'Delete user',
deleteUserConfirmation: 'Are you sure you want to delete this user?',
@ -1990,7 +1992,7 @@ Important structured information may include: names, dates, locations, events, k
deleteWhitelistEmailConfirmation:
'Are you sure you want to delete this email from whitelist? This action cannot be undone.',
importWhitelist: 'Import whitelist (excel)',
importWhitelist: 'Import whitelist (Excel)',
importSelectExcelFile: 'Excel file (.xlsx)',
importOverwriteExistingEmails: 'Overwrite existing emails',
importInvalidExcelFile: 'Please select a valid Excel file',

View File

@ -15,15 +15,18 @@ const ThemeSwitch = forwardRef<
return (
<Root
ref={ref}
className={cn('relative rounded-full', className)}
className={cn(
'group/theme-switch relative rounded-full outline-none self-center focus-visible:ring-1 focus-visible:ring-accent-primary',
className,
)}
{...props}
checked={isDark}
onCheckedChange={(value) =>
setTheme(value ? ThemeEnum.Dark : ThemeEnum.Light)
}
>
<div className="px-3 py-2 rounded-full border border-border-button bg-bg-card transition-[background-color] duration-200">
<div className="flex items-center justify-between gap-4 relative z-[1] text-text-disabled transition-[text-color] duration-200">
<div className="self-center p-3 py-2 rounded-full bg-bg-card transition-[background-color] duration-300">
<div className="h-full flex items-center justify-between gap-4 relative z-[1] text-text-disabled transition-[text-color] duration-300 delay-75">
<LucideSun
className={cn('size-[1em]', !isDark && 'text-text-primary')}
/>
@ -35,11 +38,22 @@ const ThemeSwitch = forwardRef<
<Thumb
className={cn(
'absolute top-0 left-0 w-[calc(50%+.25rem)] h-full rounded-full bg-bg-base border border-border-button',
'transition-all duration-200',
{ 'left-[calc(50%-.25rem)]': isDark },
'absolute top-0 left-0 w-[calc(50%+.25rem)] p-0.5 h-full rounded-full overflow-hidden',
'transition-all ease-out duration-300',
'group-hover/theme-switch:w-[calc(50%+.66rem)] group-focus-visible/theme-switch:w-[calc(50%+.66rem)]',
{
'left-[calc(50%-.25rem)] group-hover/theme-switch:left-[calc(50%-.66rem)] group-focus-visible/theme-switch:left-[calc(50%-.66rem)]':
isDark,
},
)}
/>
>
<div
className="
size-full rounded-full bg-bg-base shadow-md
transition-colors ease-out duration-300 delay-75
"
/>
</Thumb>
</Root>
);
});

View File

@ -18,12 +18,7 @@ import {
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs-underlined';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { listResources } from '@/services/admin-service';
import { PERMISSION_TYPES, formMergeDefaultValues } from '../utils';
@ -106,12 +101,12 @@ export const CreateRoleForm = ({
<Label>{t('admin.resources')}</Label>
<Tabs defaultValue={resourceTypes?.[0]} className="w-full mt-2">
<TabsList className="p-0 mb-2 gap-4 bg-transparent">
<TabsList className="p-0 mb-2 gap-4 bg-transparent justify-start">
{resourceTypes?.map((resourceType) => (
<TabsTrigger
key={resourceType}
value={resourceType}
className="text-text-secondary !border-border-button data-[state=active]:bg-bg-card data-[state=active]:text-text-primary"
className="text-text-secondary border-0.5 border-border-button data-[state=active]:bg-bg-card"
>
{t(`admin.resourceType.${resourceType.toLowerCase()}`)}
</TabsTrigger>

View File

@ -153,7 +153,11 @@ export const CreateUserForm = ({
<SelectItem key={role.id} value={role.role_name}>
{role.role_name}
</SelectItem>
))}
)) ?? (
<div className="text-text-secondary px-2 py-6 text-sm text-center">
{t('common.noData')}
</div>
)}
</SelectGroup>
</SelectContent>
</Select>

View File

@ -1,10 +1,9 @@
import { Button } from '@/components/ui/button';
import message from '@/components/ui/message';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { logout } from '@/services/admin-service';
import authorizationUtil from '@/utils/authorization-util';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { NavLink, Outlet, useNavigate } from 'umi';
import { useMutation } from '@tanstack/react-query';
import {
LucideMonitor,
LucideServerCrash,
@ -12,13 +11,18 @@ import {
LucideUserCog,
LucideUserStar,
} from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { NavLink, Outlet, useNavigate } from 'umi';
import ThemeSwitch from './components/theme-switch';
import { IS_ENTERPRISE } from './utils';
const AdminLayout = () => {
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { logout } from '@/services/admin-service';
import authorizationUtil from '@/utils/authorization-util';
import ThemeSwitch from '../components/theme-switch';
import { IS_ENTERPRISE } from '../utils';
const AdminNavigationLayout = () => {
const { t } = useTranslation();
const navigate = useNavigate();
@ -61,8 +65,6 @@ const AdminLayout = () => {
mutationKey: ['adminLogout'],
mutationFn: async () => {
await logout();
message.success(t('message.logout'));
authorizationUtil.removeAll();
navigate(Routes.Admin);
},
@ -90,6 +92,7 @@ const AdminLayout = () => {
'hover:bg-bg-card focus:bg-bg-card focus-visible:bg-bg-card',
'hover:text-text-primary focus:text-text-primary focus-visible:text-text-primary',
'active:text-text-primary',
'transition-colors',
{
'bg-bg-card text-text-primary': isActive,
},
@ -105,14 +108,18 @@ const AdminLayout = () => {
</nav>
<div className="mt-auto space-y-4">
<div className="text-right">
<div className="flex justify-between items-center">
<span className="text-accent-primary">
vmm.ss.rr-nnn-commithash
</span>
<ThemeSwitch />
</div>
<Button
size="lg"
variant="transparent"
className="block w-full dark:border-border-button"
block
onClick={() => logoutMutation.mutate()}
>
{t('header.logout')}
@ -127,4 +134,4 @@ const AdminLayout = () => {
);
};
export default AdminLayout;
export default AdminNavigationLayout;

View File

@ -0,0 +1,7 @@
import { Outlet } from 'umi';
const AdminRootLayout = () => {
return <Outlet />;
};
export default AdminRootLayout;

View File

@ -1,18 +1,16 @@
import { type AxiosResponseHeaders } from 'axios';
import { useEffect, useId, useState } from 'react';
import { useEffect, useId } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'umi';
import { LucideEye, LucideEyeOff } from 'lucide-react';
import { useMutation } from '@tanstack/react-query';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import Spotlight from '@/components/spotlight';
import { ButtonLoading } from '@/components/ui/button';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardFooter } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import {
@ -43,8 +41,6 @@ function AdminLogin() {
const { t } = useTranslation('translation', { keyPrefix: 'login' });
const { isLogin } = useAuth();
const [showPassword, setShowPassword] = useState(false);
const loginMutation = useMutation({
mutationKey: ['adminLogin'],
mutationFn: async (params: { email: string; password: string }) => {
@ -113,7 +109,7 @@ function AdminLogin() {
return (
<ScrollArea className="w-screen h-screen">
<div className="relative">
<div className="relative h-max min-h-[100vh]">
<Spotlight opcity={0.4} coverage={60} color="rgb(128, 255, 248)" />
<Spotlight
opcity={0.3}
@ -143,9 +139,9 @@ function AdminLogin() {
</h1>
</div>
<div className="flex items-center justify-center w-screen min-h-[1050px]">
<div className="w-full max-w-[540px]">
<Card className="w-full bg-bg-component backdrop-blur-sm rounded-2xl border border-border-button">
<div className="flex items-center justify-center w-screen">
<div className="w-full max-w-[540px] mt-72 mb-48">
<Card className="w-full bg-bg-component rounded-2xl shadow-none backdrop-blur-sm">
<CardContent className="px-10 pt-14 pb-10">
<Form {...form}>
<form
@ -164,7 +160,7 @@ function AdminLogin() {
<FormControl>
<Input
className="h-10 px-2.5"
className="h-10"
placeholder={t('emailPlaceholder')}
autoComplete="email"
{...field}
@ -184,26 +180,13 @@ function AdminLogin() {
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl>
<div className="relative">
<Input
className="h-10 px-2.5"
type={showPassword ? 'text' : 'password'}
placeholder={t('passwordPlaceholder')}
autoComplete="password"
{...field}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<LucideEyeOff className="h-4 w-4 text-gray-500" />
) : (
<LucideEye className="h-4 w-4 text-gray-500" />
)}
</button>
</div>
<Input
{...field}
className="h-10"
type="password"
placeholder={t('passwordPlaceholder')}
autoComplete="password"
/>
</FormControl>
<FormMessage />
@ -218,10 +201,10 @@ function AdminLogin() {
<FormItem className="!mt-5">
<FormLabel
className={cn(
'flex items-center hover:text-text-primary',
'transition-colors',
field.value
? 'text-text-primary'
: 'text-text-disabled',
: 'text-text-secondary',
)}
>
<FormControl>
@ -241,19 +224,17 @@ function AdminLogin() {
</CardContent>
<CardFooter className="px-10 pt-8 pb-14">
<ButtonLoading
<Button
form={formId}
variant="highlighted"
size="lg"
className="
w-full h-10
bg-metallic-gradient border-b-[#00BEB4] border-b-2
hover:bg-metallic-gradient hover:border-b-[#02bcdd]
"
block
type="submit"
className="font-medium"
loading={loading}
>
{t('login')}
</ButtonLoading>
</Button>
</CardFooter>
</Card>

View File

@ -3,11 +3,14 @@ import { Card, CardContent } from '@/components/ui/card';
function AdminMonitoring() {
return (
<Card className="!shadow-none relative h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
<Card className="!shadow-none relative h-full border-0.5 border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
<Spotlight />
<CardContent className="size-full p-0">
<iframe />
<iframe
className="size-full"
src={`${location.protocol}//${location.hostname}:9090/alerts`}
/>
</CardContent>
</Card>
);

View File

@ -18,15 +18,9 @@ import {
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { LoadingButton } from '@/components/ui/loading-button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Switch } from '@/components/ui/switch';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs-underlined';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { LucideEdit3, LucideTrash2, LucideUserPlus } from 'lucide-react';
import {
@ -149,7 +143,7 @@ function AdminRoles() {
return (
<>
<Card className="!shadow-none relative w-full h-full border border-border-button bg-transparent rounded-xl">
<Card className="!shadow-none relative w-full h-full border-0.5 border-border-button bg-transparent rounded-xl">
<Spotlight />
<ScrollArea className="size-full">
@ -170,9 +164,9 @@ function AdminRoles() {
roleList.map((role) => (
<Card
key={role.id}
className="group border border-border-default bg-transparent dark:hover:bg-bg-card transition-color duration-150"
className="group/role border-0.5 border-border-default bg-transparent dark:hover:bg-bg-card transition-color duration-150"
>
<CardHeader className="space-y-0 flex flex-row gap-4 items-center border-b border-border-button">
<CardHeader className="space-y-0 flex flex-row gap-4 items-center border-b-0.5 border-border-button">
<div className="space-y-1.5 w-0 flex-1">
<CardTitle className="font-normal text-xl">
{role.role_name}
@ -187,7 +181,10 @@ function AdminRoles() {
<Button
variant="transparent"
className="ml-2 p-0 border-0 size-[1em] align-middle opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"
className="
ml-2 p-0 border-0 size-[1em] align-middle opacity-0
group-hover/role:opacity-100 group-focus-within/role:opacity-100
"
onClick={() => {
setEditRoleDescriptionModalOpen(true);
setRoleToMakeAction(role);
@ -202,7 +199,7 @@ function AdminRoles() {
<Button
variant="ghost"
size="icon"
className="ml-auto opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"
className="ml-auto opacity-0 group-hover/role:opacity-100 group-focus-within/role:opacity-100"
disabled={deleteRoleMutation.isPending}
onClick={() => {
setDeleteModalOpen(true);
@ -218,12 +215,12 @@ function AdminRoles() {
className="h-full flex flex-col"
defaultValue={resourceTypes?.[0]}
>
<TabsList className="p-0 mb-2 gap-4 bg-transparent">
<TabsList className="p-0 mb-2 gap-4 bg-transparent justify-start">
{resourceTypes?.map((resourceName) => (
<TabsTrigger
key={resourceName}
value={resourceName}
className="text-text-secondary !border-border-button data-[state=active]:bg-bg-card data-[state=active]:text-text-primary"
className="text-text-secondary border-0.5 border-border-button data-[state=active]:bg-bg-card"
>
{t(
`admin.resourceType.${resourceName.toLowerCase()}`,
@ -290,7 +287,7 @@ function AdminRoles() {
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.addNewRole')}</DialogTitle>
</DialogHeader>
@ -309,14 +306,14 @@ function AdminRoles() {
{t('admin.cancel')}
</Button>
<LoadingButton
<Button
type="submit"
form={createRoleForm.id}
className="px-4 h-10"
variant="default"
>
{t('admin.confirm')}
</LoadingButton>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@ -334,7 +331,7 @@ function AdminRoles() {
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.editRoleDescription')}</DialogTitle>
</DialogHeader>
@ -372,13 +369,13 @@ function AdminRoles() {
{t('admin.cancel')}
</Button>
<LoadingButton
<Button
type="submit"
form={editRoleDescriptionFormId}
className="px-4 h-10"
>
{t('admin.confirm')}
</LoadingButton>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@ -393,7 +390,7 @@ function AdminRoles() {
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.deleteRole')}</DialogTitle>
</DialogHeader>
@ -402,7 +399,7 @@ function AdminRoles() {
{t('admin.deleteRoleConfirmation')}
</DialogDescription>
<div className="rounded-lg mt-6 p-4 border border-border-button">
<div className="rounded-lg mt-6 p-4 border-0.5 border-border-button">
{roleToMakeAction?.role_name}
</div>
</section>
@ -417,7 +414,7 @@ function AdminRoles() {
{t('admin.cancel')}
</Button>
<LoadingButton
<Button
className="px-4 h-10"
variant="destructive"
onClick={() =>
@ -428,7 +425,7 @@ function AdminRoles() {
loading={deleteRoleMutation.isPending}
>
{t('admin.delete')}
</LoadingButton>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -44,15 +44,18 @@ function ServiceDetail({ content }: ServiceDetailProps) {
if (isPlainObject(content)) {
return (
<dl className="text-sm text-text-primary grid grid-cols-[auto,1fr] border border-card rounded-xl overflow-hidden bg-bg-card">
<dl className="text-sm text-text-primary grid grid-cols-[minmax(20%,auto),1fr] rounded-xl overflow-hidden bg-bg-card">
{Object.entries<any>(content).map(([key, value]) => (
<div key={key} className="contents">
<dt className="px-3 py-2 bg-bg-card">
<div
key={key}
className="contents [:not(:last-child)]>*]border-b-0.5 [:not(:last-child)>*]:border-border-button"
>
<dt className="px-4 py-2.5 bg-bg-card">
<pre>
<code>{key}</code>
</pre>
</dt>
<dd className="px-3 py-2">
<dd className="px-4 py-2.5">
<pre>
<code>{JSON.stringify(value)}</code>
</pre>
@ -65,8 +68,8 @@ function ServiceDetail({ content }: ServiceDetailProps) {
if (typeof content === 'string') {
return (
<div className="rounded-lg p-4 border border-border-button bg-bg-input">
<pre className="text-sm">
<div className="rounded-lg p-4 bg-bg-card text-sm text-text-primary">
<pre>
<code>
{typeof content === 'string'
? content

View File

@ -15,17 +15,14 @@ import {
LucideClipboardList,
LucideDot,
LucideFilter,
LucideSearch,
LucideSettings2,
} from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { cn } from '@/lib/utils';
import Spotlight from '@/components/spotlight';
import { TableEmpty } from '@/components/table-skeleton';
import { Badge } from '@/components/ui/badge';
import { Badge, BadgeProps } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
@ -41,7 +38,7 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { SearchInput } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Popover,
@ -127,17 +124,17 @@ function AdminServiceStatus() {
}),
columnHelper.accessor('host', {
header: t('admin.host'),
cell: ({ row }) => (
<Badge variant="secondary" className="font-normal text-text-primary">
<i>{row.getValue('host')}</i>
cell: ({ cell }) => (
<Badge variant="secondary">
<i>{cell.getValue()}</i>
</Badge>
),
}),
columnHelper.accessor('port', {
header: t('admin.port'),
cell: ({ row }) => (
<Badge variant="secondary" className="font-normal text-text-primary">
<i>{row.getValue('port')}</i>
cell: ({ cell }) => (
<Badge variant="secondary">
<i>{cell.getValue()}</i>
</Badge>
),
}),
@ -145,15 +142,14 @@ function AdminServiceStatus() {
header: t('admin.status'),
cell: ({ cell }) => (
<Badge
variant="secondary"
className={cn(
'pl-2 font-normal text-sm text-text-primary capitalize',
variant={
{
alive: 'bg-state-success-5 text-state-success',
timeout: 'bg-state-error-5 text-state-error',
fail: 'bg-gray-500/5 text-text-disable',
}[cell.getValue()],
)}
alive: 'success',
timeout: 'destructive',
fail: 'grey',
}[cell.getValue()] as BadgeProps['variant']
}
className="pl-[.5em] capitalize"
>
<LucideDot className="size-[1em] stroke-[8] mr-1" />
{t(`admin.${cell.getValue()}`)}
@ -165,7 +161,7 @@ function AdminServiceStatus() {
id: 'actions',
header: t('admin.actions'),
cell: ({ row }) => (
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-100">
<div className="opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100 transition-opacity">
<Button
variant="transparent"
size="icon"
@ -206,6 +202,8 @@ function AdminServiceStatus() {
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
enableSorting: false,
});
useEffect(() => {
@ -216,7 +214,7 @@ function AdminServiceStatus() {
return (
<>
<Card className="!shadow-none relative h-full border border-border-button bg-transparent rounded-xl">
<Card className="!shadow-none relative h-full bg-transparent rounded-xl overflow-hidden">
<Spotlight />
<ScrollArea className="size-full">
@ -229,16 +227,20 @@ function AdminServiceStatus() {
<Button
size="icon"
variant="outline"
className="dark:bg-bg-input dark:border-border-button text-text-secondary"
className="border-0.5"
// className="
// text-text-secondary
// dark:bg-bg-input dark:border-border-button
// hover:bg-border-button dark:hover:bg-border-button
// focus-visible:ring-0 focus-visible:text-text-primary
// focus-visible:bg-border-button focus-visible:border-border-button
// "
>
<LucideFilter className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
className="bg-bg-base text-text-secondary"
>
<PopoverContent align="end">
<div className="p-2 space-y-6">
<section>
<div className="font-bold mb-3">
@ -247,9 +249,9 @@ function AdminServiceStatus() {
<RadioGroup
value={
table
(table
.getColumn('service_type')!
?.getFilterValue() as string
?.getFilterValue() as string) ?? ''
}
onValueChange={
table.getColumn('service_type')!?.setFilterValue
@ -291,15 +293,12 @@ function AdminServiceStatus() {
</PopoverContent>
</Popover>
<div className="relative w-56">
<LucideSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
className="pl-10 h-10 bg-bg-input border-border-button"
placeholder={t('header.search')}
value={table.getState().globalFilter}
onChange={(e) => table.setGlobalFilter(e.target.value)}
/>
</div>
<SearchInput
className="w-56 h-10 bg-bg-input border-border-button"
placeholder={t('header.search')}
value={table.getState().globalFilter}
onChange={(e) => table.setGlobalFilter(e.target.value)}
/>
</div>
</CardHeader>
@ -345,7 +344,7 @@ function AdminServiceStatus() {
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="group">
<TableRow key={row.id} className="group/row">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
@ -389,12 +388,12 @@ function AdminServiceStatus() {
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.extraInfo')}</DialogTitle>
</DialogHeader>
<section className="px-12 pt-6 pb-4">
<div className="rounded-lg p-4 border border-border-button bg-bg-input">
<div className="rounded-lg p-4 bg-bg-input">
<pre className="text-sm">
<code>
{JSON.stringify(itemToMakeAction?.extra ?? {}, null, 2)}
@ -425,7 +424,7 @@ function AdminServiceStatus() {
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>
<Trans i18nKey="admin.serviceDetail">
{{ name: itemToMakeAction?.name }}

View File

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'umi';
import { LucideArrowLeft, LucideDot, LucideUser2 } from 'lucide-react';
import { LucideArrowLeft, LucideDot } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import {
@ -13,11 +13,10 @@ import {
useReactTable,
} from '@tanstack/react-table';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import Spotlight from '@/components/spotlight';
import { Avatar } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
@ -27,16 +26,11 @@ import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
// TableHead,
// TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs-underlined';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
getUserDetails,
@ -44,8 +38,12 @@ import {
listUserDatasets,
} from '@/services/admin-service';
import { TableEmpty } from '@/components/table-skeleton';
import EnterpriseFeature from './components/enterprise-feature';
import { getSortIcon, parseBooleanish } from './utils';
import {
// getSortIcon,
parseBooleanish,
} from './utils';
const ASSET_NAMES = ['dataset', 'flow'];
@ -60,6 +58,20 @@ function UserDatasetTable(props: {
const columnDefs = useMemo(
() => [
datasetColumnHelper.accessor('name', {
header: t('admin.name'),
cell: ({ row, cell }) => (
<div className="flex items-center gap-2">
<RAGFlowAvatar
avatar={row.original.avatar}
name={cell.getValue()}
/>
<span>{cell.getValue()}</span>
</div>
),
}),
// #region
/*
datasetColumnHelper.accessor('name', {
header: t('admin.name'),
enableSorting: false,
@ -69,20 +81,15 @@ function UserDatasetTable(props: {
cell: ({ cell }) => {
return (
<Badge
variant="secondary"
className={cn(
'font-normal text-sm pl-2',
parseBooleanish(cell.getValue())
? 'bg-state-success-5 text-state-success'
: 'bg-state-error-5 text-state-error',
)}
variant={parseBooleanish(cell.getValue()) ? 'success' : 'destructive'}
className="pl-[.35em]"
>
<LucideDot className="size-[1em] stroke-[8] mr-1" />
{t(
parseBooleanish(cell.getValue())
? 'admin.active'
: 'admin.inactive',
)}
)}"
</Badge>
);
},
@ -111,6 +118,8 @@ function UserDatasetTable(props: {
header: t('admin.permission'),
enableSorting: false,
}),
*/
// #endregion
],
[t],
);
@ -121,12 +130,14 @@ function UserDatasetTable(props: {
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableSorting: false,
});
return (
<section className="space-y-4">
<Table>
<TableHeader>
{/* <TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
@ -152,7 +163,8 @@ function UserDatasetTable(props: {
))}
</TableRow>
))}
</TableHeader>
</TableHeader> */}
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
@ -165,14 +177,7 @@ function UserDatasetTable(props: {
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={table.getAllColumns().length}
className="h-24 text-center"
>
{t('common.noData')}
</TableCell>
</TableRow>
<TableEmpty columnsLength={columnDefs.length} />
)}
</TableBody>
</Table>
@ -197,6 +202,20 @@ function UserAgentTable(props: { data?: AdminService.ListUserAgentItem[] }) {
const columnDefs = useMemo(
() => [
agentColumnHelper.accessor('title', {
header: t('admin.agentTitle'),
cell: ({ row, cell }) => (
<div className="flex items-center gap-2">
<RAGFlowAvatar
avatar={row.original.avatar}
name={cell.getValue()}
/>
<span>{cell.getValue()}</span>
</div>
),
}),
// #region
/*
agentColumnHelper.accessor('title', {
header: t('admin.agentTitle'),
}),
@ -206,6 +225,8 @@ function UserAgentTable(props: { data?: AdminService.ListUserAgentItem[] }) {
agentColumnHelper.accessor('canvas_category', {
header: t('admin.canvasCategory'),
}),
*/
// #endregion
],
[t],
);
@ -216,12 +237,14 @@ function UserAgentTable(props: { data?: AdminService.ListUserAgentItem[] }) {
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableSorting: false,
});
return (
<section className="space-y-4">
<Table>
<TableHeader>
{/* <TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
@ -232,20 +255,13 @@ function UserAgentTable(props: { data?: AdminService.ListUserAgentItem[] }) {
header.column.columnDef.header,
header.getContext(),
)}
{/* {header.column.getCanFilter() && (
<Button
variant="ghost"
>
<LucideFilter />
</Button>
)} */}
</>
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
</TableHeader> */}
<TableBody>
{table.getRowModel().rows?.length ? (
@ -259,14 +275,7 @@ function UserAgentTable(props: { data?: AdminService.ListUserAgentItem[] }) {
</TableRow>
))
) : (
<TableRow key="empty">
<TableCell
colSpan={table.getAllColumns().length}
className="h-24 text-center"
>
{t('common.noData')}
</TableCell>
</TableRow>
<TableEmpty columnsLength={columnDefs.length} />
)}
</TableBody>
</Table>
@ -319,32 +328,28 @@ function AdminUserDetail() {
onClick={() => navigate(`${Routes.AdminUserManagement}`)}
>
<LucideArrowLeft />
<span>{t('admin.userManagement')}</span>
<span>{t('admin.back')}</span>
</Button>
</nav>
<Card className="!shadow-none relative h-0 basis-0 grow flex flex-col bg-transparent border dark:border-border-button overflow-hidden">
<Card className="!shadow-none relative h-0 basis-0 grow flex flex-col bg-transparent border-0.5 border-border-button overflow-hidden">
<Spotlight />
<CardHeader className="pb-10 border-b dark:border-border-button space-y-8">
<CardHeader className="pb-10 border-b-0.5 dark:border-border-button space-y-8">
<section className="flex items-center gap-4 text-base">
<Avatar className="justify-center items-center bg-bg-group uppercase">
{detail?.email
.split('@')[0]
.replace(/[^0-9a-z]/gi, '')
.slice(0, 2) || <LucideUser2 />}
</Avatar>
<RAGFlowAvatar
avatar={detail?.avatar}
name={detail?.email}
isPerson
/>
<span>{detail?.email}</span>
<Badge
variant="secondary"
className={cn(
'font-normal text-sm pl-2',
parseBooleanish(detail?.is_active)
? 'bg-state-success-5 text-state-success'
: '',
)}
variant={
parseBooleanish(detail?.is_active) ? 'success' : 'destructive'
}
className="pl-[.5em]"
>
<LucideDot className="size-[1em] stroke-[8] mr-1" />
{t(
@ -355,11 +360,11 @@ function AdminUserDetail() {
</Badge>
<EnterpriseFeature>
{() => (
<Badge variant="secondary" className="font-normal text-sm">
{detail?.role}
</Badge>
)}
{() =>
detail?.role && (
<Badge variant="secondary">{detail?.role}</Badge>
)
}
</EnterpriseFeature>
</section>
@ -398,16 +403,23 @@ function AdminUserDetail() {
</div>
<div>{t(detail?.is_anonymous ? 'admin.yes' : 'admin.no')}</div>
</div>
<div>
<div className="text-sm text-text-secondary mb-2">
{t('admin.isSuperuser')}
</div>
<div>{t(detail?.is_superuser ? 'admin.yes' : 'admin.no')}</div>
</div>
</section>
</CardHeader>
<CardContent className="h-0 basis-0 grow pt-6">
<Tabs className="h-full flex flex-col" defaultValue="dataset">
<TabsList className="p-0 mb-2 gap-4 bg-transparent">
<TabsList className="p-0 mb-2 gap-4 bg-transparent justify-start">
{ASSET_NAMES.map((name) => (
<TabsTrigger
key={name}
className="border-border-button data-[state=active]:bg-bg-card"
className="text-text-secondary border-0.5 border-border-button data-[state=active]:bg-bg-card"
value={name}
>
{t(`header.${name}`)}

View File

@ -22,7 +22,6 @@ import {
LucideUserPlus,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { rsaPsw } from '@/utils';
import Spotlight from '@/components/spotlight';
@ -39,14 +38,18 @@ import {
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { LoadingButton } from '@/components/ui/loading-button';
import {
Popover,
PopoverContent,
@ -55,13 +58,6 @@ import {
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import {
Table,
@ -213,6 +209,15 @@ function AdminUserManagement() {
}),
columnHelper.accessor('nickname', {
header: t('admin.nickname'),
cell: ({ row, cell }) => (
<div className="flex items-center gap-2">
<span>{cell.getValue()}</span>
{row.original.is_superuser ? (
<Badge variant="secondary">{t('admin.superuser')}</Badge>
) : null}
</div>
),
}),
...(IS_ENTERPRISE
@ -220,30 +225,29 @@ function AdminUserManagement() {
columnHelper.accessor('role', {
header: t('admin.role'),
cell: ({ row, cell }) => (
<Select
value={cell.getValue()}
onValueChange={(value) => {
if (!updateUserRoleMutation.isPending) {
updateUserRoleMutation.mutate({
email: row.original.email,
role: value,
});
}
}}
disabled={updateUserRoleMutation.isPending}
>
<SelectTrigger className="h-10">
<SelectValue />
</SelectTrigger>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="min-w-16">
{cell.getValue()}
</Button>
</DropdownMenuTrigger>
<SelectContent className="bg-bg-base">
<DropdownMenuContent>
{roleList?.map(({ id, role_name }) => (
<SelectItem key={id} value={role_name}>
<DropdownMenuItem
key={id}
onClick={() => {
updateUserRoleMutation.mutate({
email: row.original.email,
role: role_name,
});
}}
>
{role_name}
</SelectItem>
</DropdownMenuItem>
))}
</SelectContent>
</Select>
</DropdownMenuContent>
</DropdownMenu>
),
filterFn: createColumnFilterFn(
(row, id, filterValue) => row.getValue(id) === filterValue,
@ -275,13 +279,8 @@ function AdminUserManagement() {
header: t('admin.status'),
cell: ({ cell }) => (
<Badge
variant="secondary"
className={cn(
'pl-2 font-normal text-sm',
parseBooleanish(cell.getValue())
? 'bg-state-success-5 text-state-success'
: '',
)}
variant={parseBooleanish(cell.getValue()) ? 'success' : 'secondary'}
className="pl-[.5em]"
>
<LucideDot className="size-[1em] stroke-[8] mr-1" />
{t(
@ -304,11 +303,11 @@ function AdminUserManagement() {
id: 'actions',
header: t('admin.actions'),
cell: ({ row }) => (
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-100">
<div className="opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100 transition-opacity">
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
className="border-0"
onClick={() =>
navigate(`${Routes.AdminUserManagement}/${row.original.email}`)
}
@ -318,7 +317,7 @@ function AdminUserManagement() {
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
className="border-0"
onClick={() => {
setUserToMakeAction(row.original);
setPasswordModalOpen(true);
@ -327,9 +326,9 @@ function AdminUserManagement() {
<LucideUserLock />
</Button>
<Button
variant="transparent"
variant="danger"
size="icon"
className="border-0 text-text-secondary"
className="border-0"
onClick={() => {
setUserToMakeAction(row.original);
setDeleteModalOpen(true);
@ -358,7 +357,7 @@ function AdminUserManagement() {
return (
<>
<Card className="!shadow-none relative h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
<Card className="!shadow-none relative h-full bg-transparent overflow-hidden">
<Spotlight />
<ScrollArea className="size-full">
@ -493,8 +492,8 @@ function AdminUserManagement() {
{() => <col className="w-[12%]" />}
</EnterpriseFeature>
<col className="w-[10%]" />
<col className="w-[12%]" />
<col className="w-[8%]" />
<col className="w-[15%]" />
<col className="w-52" />
</colgroup>
@ -518,7 +517,7 @@ function AdminUserManagement() {
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="group">
<TableRow key={row.id} className="group/row">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
@ -555,18 +554,16 @@ function AdminUserManagement() {
{/* Delete Confirmation Modal */}
<Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}>
<DialogContent className="p-0 border-border-button">
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.deleteUser')}</DialogTitle>
</DialogHeader>
<section className="px-12 py-4">
<DialogDescription className="text-text-primary">
{t('admin.deleteUserConfirmation')}
<section className="px-12 py-4 text-text-primary text-sm">
{t('admin.deleteUserConfirmation')}
<div className="rounded-lg mt-6 p-4 border border-border-button">
{userToMakeAction?.email}
</div>
</DialogDescription>
<div className="rounded-lg mt-6 p-4 border-0.5 border-border-button">
{userToMakeAction?.email}
</div>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
@ -579,7 +576,7 @@ function AdminUserManagement() {
{t('admin.cancel')}
</Button>
<LoadingButton
<Button
className="px-4 h-10"
variant="destructive"
onClick={() =>
@ -590,7 +587,7 @@ function AdminUserManagement() {
loading={deleteUserMutation.isPending}
>
{t('admin.delete')}
</LoadingButton>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@ -605,7 +602,7 @@ function AdminUserManagement() {
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.changePassword')}</DialogTitle>
</DialogHeader>
@ -637,7 +634,7 @@ function AdminUserManagement() {
{t('admin.cancel')}
</Button>
<LoadingButton
<Button
form={changePasswordForm.id}
className="px-4 h-10"
variant="default"
@ -646,7 +643,7 @@ function AdminUserManagement() {
loading={changePasswordMutation.isPending}
>
{t('admin.changePassword')}
</LoadingButton>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@ -660,7 +657,7 @@ function AdminUserManagement() {
}}
>
<DialogContent className="p-0 border-border-button">
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.createNewUser')}</DialogTitle>
</DialogHeader>
@ -684,7 +681,7 @@ function AdminUserManagement() {
{t('admin.cancel')}
</Button>
<LoadingButton
<Button
form={createUserForm.id}
type="submit"
className="px-4 h-10"
@ -693,7 +690,7 @@ function AdminUserManagement() {
loading={createUserMutation.isPending}
>
{t('admin.confirm')}
</LoadingButton>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -40,8 +40,7 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { LoadingButton } from '@/components/ui/loading-button';
import { SearchInput } from '@/components/ui/input';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
@ -63,6 +62,7 @@ import {
import { EMPTY_DATA, createFuzzySearchFn, getSortIcon } from './utils';
import dayjs from 'dayjs';
import useCreateEmailForm from './forms/email-form';
import useImportExcelForm, {
ImportExcelFormData,
@ -157,18 +157,14 @@ function AdminWhitelist() {
email: item.email,
}));
const now = new Date();
const YYYY = String(now.getFullYear()).padStart(4, '0');
const MM = String(now.getMonth()).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const HH = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
const worksheet = XLSX.utils.json_to_sheet(columnData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
XLSX.writeFile(workbook, `whitelist_${YYYY}${MM}${dd}${HH}${mm}${ss}.xlsx`);
XLSX.writeFile(
workbook,
`whitelist_${dayjs(new Date()).format('YYYYMMDDHHmmss')}.xlsx`,
);
};
const columnDefs = useMemo(
@ -187,11 +183,11 @@ function AdminWhitelist() {
id: 'actions',
header: t('admin.actions'),
cell: ({ row }) => (
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-100">
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity">
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
className="border-0"
onClick={() => {
setItemToMakeAction(row.original);
setEditModalOpen(true);
@ -200,9 +196,9 @@ function AdminWhitelist() {
<LucideUserPen />
</Button>
<Button
variant="transparent"
variant="danger"
size="icon"
className="border-0 text-text-secondary"
className="border-0"
onClick={() => {
setItemToMakeAction(row.original);
setDeleteModalOpen(true);
@ -227,11 +223,13 @@ function AdminWhitelist() {
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
enableSorting: false,
});
return (
<>
<Card className="!shadow-none relative h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
<Card className="!shadow-none relative h-full border-0.5 border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
<Spotlight />
<ScrollArea className="size-full">
@ -239,15 +237,13 @@ function AdminWhitelist() {
<CardTitle>{t('admin.whitelistManagement')}</CardTitle>
<div className="flex items-center gap-4">
<div className="relative w-56">
<LucideSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
className="pl-10 h-10 bg-bg-input border-border-button"
placeholder={t('header.search')}
value={table.getState().globalFilter}
onChange={(e) => table.setGlobalFilter(e.target.value)}
/>
</div>
<SearchInput
className="w-56 h-10 bg-bg-input border-border-button"
placeholder={t('header.search')}
value={table.getState().globalFilter}
onChange={(e) => table.setGlobalFilter(e.target.value)}
prefix={<LucideSearch className="size-3.5" />}
/>
<Button
variant="outline"
@ -272,7 +268,7 @@ function AdminWhitelist() {
onClick={() => setCreateModalOpen(true)}
>
<LucidePlus />
{t('admin.createEmail')}
{t('admin.newUser')}
</Button>
</div>
</CardHeader>
@ -360,18 +356,18 @@ function AdminWhitelist() {
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.deleteEmail')}</DialogTitle>
</DialogHeader>
<section className="px-12 py-4">
<DialogDescription className="text-text-primary">
{t('admin.deleteWhitelistEmailConfirmation')}
<div className="rounded-lg mt-6 p-4 border border-border-button">
{itemToMakeAction?.email}
</div>
</DialogDescription>
<div className="rounded-lg mt-6 p-4 border-0.5 border-border-button">
{itemToMakeAction?.email}
</div>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
@ -384,7 +380,7 @@ function AdminWhitelist() {
{t('admin.cancel')}
</Button>
<LoadingButton
<Button
className="px-4 h-10"
variant="destructive"
onClick={() => {
@ -398,7 +394,7 @@ function AdminWhitelist() {
loading={deleteWhitelistEntryMutation.isPending}
>
{t('admin.delete')}
</LoadingButton>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@ -413,7 +409,7 @@ function AdminWhitelist() {
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.createEmail')}</DialogTitle>
</DialogHeader>
@ -434,7 +430,7 @@ function AdminWhitelist() {
{t('admin.cancel')}
</Button>
<LoadingButton
<Button
form={createEmailForm.id}
type="submit"
className="px-4 h-10"
@ -443,7 +439,7 @@ function AdminWhitelist() {
loading={createWhitelistEntryMutation.isPending}
>
{t('admin.confirm')}
</LoadingButton>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@ -459,7 +455,7 @@ function AdminWhitelist() {
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.editEmail')}</DialogTitle>
</DialogHeader>
@ -487,7 +483,7 @@ function AdminWhitelist() {
{t('admin.cancel')}
</Button>
<LoadingButton
<Button
form={editEmailForm.id}
type="submit"
className="px-4 h-10"
@ -496,7 +492,7 @@ function AdminWhitelist() {
loading={updateWhitelistEntryMutation.isPending}
>
{t('admin.confirm')}
</LoadingButton>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@ -511,7 +507,7 @@ function AdminWhitelist() {
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle>{t('admin.importWhitelist')}</DialogTitle>
</DialogHeader>
@ -532,7 +528,7 @@ function AdminWhitelist() {
{t('admin.cancel')}
</Button>
<LoadingButton
<Button
form={importExcelForm.id}
type="submit"
className="px-4 h-10"
@ -541,7 +537,7 @@ function AdminWhitelist() {
loading={importExcelMutation.isPending}
>
{t('admin.import')}
</LoadingButton>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -2,8 +2,8 @@ import { Routes } from '@/routes';
import authorizationUtil from '@/utils/authorization-util';
import { Navigate, Outlet } from 'umi';
export default () => {
export default function AuthorizedAdminWrapper() {
const isLogin = !!authorizationUtil.getAuthorization();
return isLogin ? <Outlet /> : <Navigate to={Routes.Admin} />;
};
}

View File

@ -418,50 +418,50 @@ const routes = [
// Admin routes
{
path: Routes.Admin,
component: `@/pages/admin`,
layout: false,
},
{
path: `${Routes.AdminUserManagement}/:id`,
layout: false,
wrappers: ['@/wrappers/authAdmin'],
component: `@/pages/admin/user-detail`,
},
{
path: Routes.Admin,
component: `@/pages/admin/layout`,
layout: false,
component: `@/pages/admin/layouts/root-layout`,
routes: [
{
path: Routes.AdminServices,
component: `@/pages/admin/service-status`,
wrappers: ['@/wrappers/authAdmin'],
path: '',
component: `@/pages/admin/login`,
},
{
path: Routes.AdminUserManagement,
component: `@/pages/admin/users`,
wrappers: ['@/wrappers/authAdmin'],
path: `${Routes.AdminUserManagement}/:id`,
wrappers: ['@/pages/admin/wrappers/authorized'],
component: `@/pages/admin/user-detail`,
},
{
path: Routes.Admin,
component: `@/pages/admin/layouts/navigation-layout`,
wrappers: ['@/pages/admin/wrappers/authorized'],
routes: [
{
path: Routes.AdminServices,
component: `@/pages/admin/service-status`,
},
{
path: Routes.AdminUserManagement,
component: `@/pages/admin/users`,
},
...(IS_ENTERPRISE
? [
{
path: Routes.AdminWhitelist,
component: `@/pages/admin/whitelist`,
wrappers: ['@/wrappers/authAdmin'],
},
{
path: Routes.AdminRoles,
component: `@/pages/admin/roles`,
wrappers: ['@/wrappers/authAdmin'],
},
{
path: Routes.AdminMonitoring,
component: `@/pages/admin/monitoring`,
wrappers: ['@/wrappers/authAdmin'],
},
]
: []),
...(IS_ENTERPRISE
? [
{
path: Routes.AdminWhitelist,
component: `@/pages/admin/whitelist`,
},
{
path: Routes.AdminRoles,
component: `@/pages/admin/roles`,
},
{
path: Routes.AdminMonitoring,
component: `@/pages/admin/monitoring`,
},
]
: []),
],
},
],
},
];

View File

@ -101,6 +101,8 @@ request.interceptors.response.use(
);
const {
getSystemVersion: _getSystemVersion,
adminLogin,
adminLogout,
adminListUsers,
@ -257,15 +259,5 @@ export const importWhitelistFromExcel = (file: File) => {
return request.post<ResponseData<never>>(adminImportWhitelist, fd);
};
export default {
login,
logout,
listUsers,
createUser,
getUserDetails,
updateUserStatus,
updateUserPassword,
deleteUser,
listUserDatasets,
listUserAgents,
};
export const getSystemVersion = () =>
request.get<ResponseData<string>>(_getSystemVersion);

View File

@ -32,6 +32,7 @@ declare module AdminService {
};
export type UserDetail = {
avatar?: string;
create_date: string;
email: string;
is_active: '0' | '1';
@ -46,6 +47,7 @@ declare module AdminService {
};
export type ListUserDatasetItem = {
avatar?: string;
chunk_num: number;
create_date: string;
doc_num: number;
@ -58,6 +60,7 @@ declare module AdminService {
};
export type ListUserAgentItem = {
avatar?: string;
canvas_category: 'agent';
permission: 'string';
title: string;

View File

@ -27,6 +27,9 @@ module.exports = {
'4xl': '1980px',
},
extend: {
borderWidth: {
0.5: '0.5px',
},
colors: {
border: 'var(--border-default)',
input: 'hsl(var(--input))',
@ -75,7 +78,9 @@ module.exports = {
'bg-list': {
DEFAULT: 'rgb(var(--bg-list) / <alpha-value>)',
},
'text-primary': 'var(--text-primary)',
'text-primary': {
DEFAULT: 'rgb(var(--text-primary) / <alpha-value>)',
},
'text-secondary': 'var(--text-secondary)',
'text-disabled': 'var(--text-disabled)',
'text-input-tip': 'var(--text-input-tip)',
@ -175,7 +180,7 @@ module.exports = {
},
backgroundImage: {
'metallic-gradient':
'linear-gradient(104deg, var(--text-primary) 30%, var(--metallic) 50%, var(--text-primary) 70%)',
'linear-gradient(104deg, rgb(var(--text-primary)) 30%, var(--metallic) 50%, rgb(var(--text-primary)) 70%)',
},
borderRadius: {
lg: `var(--radius)`,

View File

@ -99,7 +99,9 @@
--bg-canvas: 246 246 247;
--bg-list: 246 246 247;
/* Button ,Body text, Input completed text */
--text-primary: #161618;
/* #161618 */
--text-primary: 22 22 24;
--text-secondary: #75787a;
--text-disabled: #b2b5b7;
/* input placeholder color */
@ -231,7 +233,9 @@
--bg-input: rgba(255, 255, 255, 0.05);
--bg-canvas: 0 0 0;
--bg-list: 56 56 58;
--text-primary: #f6f6f7;
/* #f6f6f7 */
--text-primary: 246 246 247;
--text-secondary: #b2b5b7;
--text-disabled: #75787a;
--text-input-tip: #75787a;