ragflow/web/src/pages/admin/service-status.tsx
Jimmy Ben Klieve f126875ec6
Apply some tweaks on Admin UI (#11011)
### What problem does this PR solve?

- Fix selected radio button text misaligned with radio button dot
- Fix `<ScrollArea>` scrollbar z-index issue
- Add backdrop blur effect on scrollbar thumbs
- Adjust some styles to match the design 


### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-11-05 12:58:43 +08:00

457 lines
15 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
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 { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { listServices, showServiceDetails } from '@/services/admin-service';
import {
EMPTY_DATA,
createColumnFilterFn,
createFuzzySearchFn,
getSortIcon,
} from './utils';
import ServiceDetail from './service-detail';
const columnHelper = createColumnHelper<AdminService.ListServicesItem>();
const globalFilterFn = createFuzzySearchFn<AdminService.ListServicesItem>([
'name',
'service_type',
]);
const SERVICE_TYPE_FILTER_OPTIONS = [
{ value: 'ragflow_server', label: 'ragflow_server' },
{ value: 'meta_data', label: 'meta_data' },
{ value: 'file_store', label: 'file_store' },
{ value: 'retrieval', label: 'retrieval' },
{ value: 'message_queue', label: 'message_queue' },
];
function AdminServiceStatus() {
const { t } = useTranslation();
const [extraInfoModalOpen, setExtraInfoModalOpen] = useState(false);
const [detailModalOpen, setDetailModalOpen] = useState(false);
const [itemToMakeAction, setItemToMakeAction] =
useState<AdminService.ListServicesItem | null>(null);
const { data: servicesList } = useQuery({
queryKey: ['admin/listServices'],
queryFn: async () => (await listServices()).data.data,
retry: false,
});
const { data: serviceDetails, error: serviceDetailsError } = useQuery({
queryKey: ['admin/serviceDetails', itemToMakeAction?.id],
queryFn: async () =>
(await showServiceDetails(itemToMakeAction!?.id)).data.data,
enabled: !!(itemToMakeAction && detailModalOpen),
retry: false,
});
const columnDefs = useMemo(
() => [
columnHelper.accessor('id', {
header: t('admin.id'),
}),
columnHelper.accessor('name', {
header: t('admin.name'),
}),
columnHelper.accessor('service_type', {
header: t('admin.serviceType'),
filterFn: createColumnFilterFn(
(row, id, filterValue) => row.getValue(id) === filterValue,
{
autoRemove: (v) => !v,
resolveFilterValue: (v) => v || null,
},
),
enableSorting: false,
}),
columnHelper.accessor('host', {
header: t('admin.host'),
cell: ({ row }) => (
<Badge variant="secondary" className="font-normal text-text-primary">
<i>{row.getValue('host')}</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>
</Badge>
),
}),
columnHelper.accessor('status', {
header: t('admin.status'),
cell: ({ cell }) => (
<Badge
variant="secondary"
className={cn(
'pl-2 font-normal text-sm text-text-primary capitalize',
{
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()],
)}
>
<LucideDot className="size-[1em] stroke-[8] mr-1" />
{t(`admin.${cell.getValue()}`)}
</Badge>
),
enableSorting: false,
}),
columnHelper.display({
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">
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
onClick={() => {
setItemToMakeAction(row.original);
setExtraInfoModalOpen(true);
}}
>
<LucideSettings2 />
</Button>
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
onClick={() => {
setItemToMakeAction(row.original);
setDetailModalOpen(true);
}}
>
<LucideClipboardList />
</Button>
</div>
),
}),
],
[t],
);
const table = useReactTable({
data: servicesList ?? EMPTY_DATA,
columns: columnDefs,
globalFilterFn,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
useEffect(() => {
if (detailModalOpen && serviceDetailsError) {
setDetailModalOpen(false);
}
}, [detailModalOpen, serviceDetailsError]);
return (
<>
<Card className="!shadow-none relative h-full border border-border-button bg-transparent rounded-xl">
<Spotlight />
<ScrollArea className="size-full">
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
<CardTitle>{t('admin.serviceStatus')}</CardTitle>
<div className="flex items-center gap-4">
<Popover>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
className="dark:bg-bg-input dark:border-border-button text-text-secondary"
>
<LucideFilter className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
className="bg-bg-base text-text-secondary"
>
<div className="p-2 space-y-6">
<section>
<div className="font-bold mb-3">
{t('admin.serviceType')}
</div>
<RadioGroup
value={
table
.getColumn('service_type')!
?.getFilterValue() as string
}
onValueChange={
table.getColumn('service_type')!?.setFilterValue
}
>
<Label className="flex items-center space-x-2">
<RadioGroupItem
className="bg-bg-input border-border-button"
value=""
/>
<span>{t('admin.all')}</span>
</Label>
{SERVICE_TYPE_FILTER_OPTIONS.map(({ label, value }) => (
<Label
key={value}
className="flex items-center space-x-2"
>
<RadioGroupItem
className="bg-bg-input border-border-button"
value={value}
/>
<span>{label}</span>
</Label>
))}
</RadioGroup>
</section>
</div>
<div className="pt-4 flex justify-end">
<Button
variant="outline"
className="dark:bg-bg-input dark:border-border-button text-text-secondary"
onClick={() => table.resetColumnFilters()}
>
{t('admin.reset')}
</Button>
</div>
</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>
</div>
</CardHeader>
<CardContent>
<Table>
<colgroup>
<col className="w-[6%]" />
<col />
<col className="w-[22%]" />
<col className="w-[13%]" />
<col className="w-[10%]" />
<col className="w-[10%]" />
<col className="w-52" />
</colgroup>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder ? null : header.column.getCanSort() ? (
<Button
variant="ghost"
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{getSortIcon(header.column.getIsSorted())}
</Button>
) : (
flexRender(
header.column.columnDef.header,
header.getContext(),
)
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="group">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableEmpty columnsLength={columnDefs.length} />
)}
</TableBody>
</Table>
</CardContent>
<CardFooter className="flex items-center justify-end">
<RAGFlowPagination
total={servicesList?.length}
current={table.getState().pagination.pageIndex + 1}
pageSize={table.getState().pagination.pageSize}
onChange={(page, pageSize) => {
table.setPagination({
pageIndex: page - 1,
pageSize,
});
}}
/>
</CardFooter>
</ScrollArea>
</Card>
{/* Extra info modal*/}
<Dialog open={extraInfoModalOpen} onOpenChange={setExtraInfoModalOpen}>
<DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => {
if (!extraInfoModalOpen) {
setItemToMakeAction(null);
}
}}
>
<DialogHeader className="p-6 border-b 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">
<pre className="text-sm">
<code>
{JSON.stringify(itemToMakeAction?.extra ?? {}, null, 2)}
</code>
</pre>
</div>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => setExtraInfoModalOpen(false)}
>
{t('admin.close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Service details modal */}
<Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
<DialogContent
className="flex flex-col max-h-[calc(100vh-4rem)] max-w-6xl p-0 border-border-button"
onAnimationEnd={() => {
if (!detailModalOpen) {
setItemToMakeAction(null);
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>
<Trans i18nKey="admin.serviceDetail">
{{ name: itemToMakeAction?.name }}
</Trans>
</DialogTitle>
</DialogHeader>
<ScrollArea className="pt-6 pb-4 px-12 h-0 flex-1 text-text-secondary flex flex-col">
<ServiceDetail content={serviceDetails?.message} />
</ScrollArea>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => {
setDetailModalOpen(false);
}}
>
{t('admin.close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
export default AdminServiceStatus;