feat: pre-aggregation workspace stats

This commit is contained in:
DarkSky
2026-01-01 05:01:52 +08:00
parent 97507e7043
commit f745f7b669
16 changed files with 722 additions and 245 deletions

View File

@@ -17,10 +17,12 @@ import { useCallback, useTransition } from 'react';
interface DataTablePaginationProps<TData> {
table: Table<TData>;
disabled?: boolean;
}
export function DataTablePagination<TData>({
table,
disabled = false,
}: DataTablePaginationProps<TData>) {
const [, startTransition] = useTransition();
@@ -63,6 +65,7 @@ export function DataTablePagination<TData>({
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={onPageSizeChange}
disabled={disabled}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
@@ -86,7 +89,7 @@ export function DataTablePagination<TData>({
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={handleFirstPage}
disabled={!table.getCanPreviousPage()}
disabled={disabled || !table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeftIcon className="h-4 w-4" />
@@ -95,7 +98,7 @@ export function DataTablePagination<TData>({
variant="outline"
className="h-8 w-8 p-0"
onClick={handlePreviousPage}
disabled={!table.getCanPreviousPage()}
disabled={disabled || !table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeftIcon className="h-4 w-4" />
@@ -104,7 +107,7 @@ export function DataTablePagination<TData>({
variant="outline"
className="h-8 w-8 p-0"
onClick={handleNextPage}
disabled={!table.getCanNextPage()}
disabled={disabled || !table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRightIcon className="h-4 w-4" />
@@ -113,7 +116,7 @@ export function DataTablePagination<TData>({
variant="outline"
className="h-8 w-8 p-0"
onClick={handleLastPage}
disabled={!table.getCanNextPage()}
disabled={disabled || !table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRightIcon className="h-4 w-4" />

View File

@@ -27,6 +27,8 @@ interface DataTableProps<TData, TValue> {
totalCount: number;
pagination: PaginationState;
onPaginationChange: OnChangeFn<PaginationState>;
loading?: boolean;
disablePagination?: boolean;
// Row Selection
rowSelection?: RowSelectionState;
@@ -51,6 +53,8 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
totalCount,
pagination,
onPaginationChange,
loading = false,
disablePagination = false,
rowSelection,
onRowSelectionChange,
renderToolbar,
@@ -83,9 +87,34 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
});
return (
<div className="flex flex-col gap-4 py-5 px-6 h-full overflow-auto">
<div className="flex flex-col gap-4 py-5 px-6 h-full overflow-auto relative">
{renderToolbar?.(table)}
<div className="rounded-md border h-full flex flex-col overflow-auto">
<div className="rounded-md border h-full flex flex-col overflow-auto relative">
{loading ? (
<div className="absolute inset-0 z-10 bg-gray-50/70 backdrop-blur-[1px] flex flex-col items-center justify-center gap-2 text-sm text-gray-600">
<svg
className="h-5 w-5 animate-spin text-gray-500"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
/>
</svg>
<span>Loading...</span>
</div>
) : null}
<Table>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
@@ -160,7 +189,10 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
</Table>
</div>
</div>
<DataTablePagination table={table} />
<DataTablePagination
table={table}
disabled={disablePagination || loading}
/>
</div>
);
}

View File

@@ -14,6 +14,7 @@ type FeatureFilterPopoverProps = {
onChange: (features: FeatureType[]) => void;
align?: 'start' | 'center' | 'end';
buttonLabel?: string;
disabled?: boolean;
};
export const FeatureFilterPopover = ({
@@ -22,29 +23,37 @@ export const FeatureFilterPopover = ({
onChange,
align = 'start',
buttonLabel = 'Features',
disabled = false,
}: FeatureFilterPopoverProps) => {
const handleFeatureToggle = useCallback(
(feature: FeatureType, checked: boolean) => {
if (disabled) {
return;
}
if (checked) {
onChange([...new Set([...selectedFeatures, feature])]);
} else {
onChange(selectedFeatures.filter(enabled => enabled !== feature));
}
},
[onChange, selectedFeatures]
[disabled, onChange, selectedFeatures]
);
const handleClearFeatures = useCallback(() => {
if (disabled) {
return;
}
onChange([]);
}, [onChange]);
}, [disabled, onChange]);
return (
<Popover>
<Popover open={disabled ? false : undefined}>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 px-2 lg:px-3 space-x-1"
disabled={disabled}
>
<span>{buttonLabel}</span>
{selectedFeatures.length > 0 ? (
@@ -70,6 +79,7 @@ export const FeatureFilterPopover = ({
onCheckedChange={checked =>
handleFeatureToggle(feature, !!checked)
}
disabled={disabled}
/>
<span className="text-sm truncate">{feature}</span>
</label>
@@ -80,7 +90,7 @@ export const FeatureFilterPopover = ({
variant="ghost"
size="sm"
onClick={handleClearFeatures}
disabled={selectedFeatures.length === 0}
disabled={disabled || selectedFeatures.length === 0}
>
Clear
</Button>

View File

@@ -27,12 +27,16 @@ interface DataTableToolbarProps<TData> {
onFeaturesChange: (features: FeatureType[]) => void;
sort: AdminWorkspaceSort | undefined;
onSortChange: (sort: AdminWorkspaceSort | undefined) => void;
disabled?: boolean;
}
const sortOptions: { value: AdminWorkspaceSort; label: string }[] = [
{ value: AdminWorkspaceSort.SnapshotSize, label: 'Snapshot size' },
{ value: AdminWorkspaceSort.BlobCount, label: 'Blob count' },
{ value: AdminWorkspaceSort.BlobSize, label: 'Blob size' },
{ value: AdminWorkspaceSort.SnapshotCount, label: 'Snapshot count' },
{ value: AdminWorkspaceSort.MemberCount, label: 'Member count' },
{ value: AdminWorkspaceSort.PublicPageCount, label: 'Public pages' },
{ value: AdminWorkspaceSort.CreatedAt, label: 'Created time' },
];
@@ -43,6 +47,7 @@ export function DataTableToolbar<TData>({
onFeaturesChange,
sort,
onSortChange,
disabled = false,
}: DataTableToolbarProps<TData>) {
const [value, setValue] = useState(keyword);
const debouncedValue = useDebouncedValue(value, 400);
@@ -82,12 +87,18 @@ export function DataTableToolbar<TData>({
availableFeatures={availableFeatures}
onChange={onFeaturesChange}
align="start"
disabled={disabled}
/>
<div className="flex items-center gap-y-2 flex-wrap justify-end gap-2">
<Popover>
<Popover open={disabled ? false : undefined}>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 px-2 lg:px-3">
<Button
variant="outline"
size="sm"
className="h-8 px-2 lg:px-3"
disabled={disabled}
>
Sort: {selectedSortLabel}
</Button>
</PopoverTrigger>
@@ -99,6 +110,7 @@ export function DataTableToolbar<TData>({
variant="ghost"
className="justify-start"
size="sm"
disabled={disabled}
onClick={() => handleSortChange(option.value)}
>
{option.label}
@@ -113,6 +125,7 @@ export function DataTableToolbar<TData>({
value={value}
onChange={onValueChange}
className="h-8 w-[150px] lg:w-[250px]"
disabled={disabled}
/>
</div>
</div>

View File

@@ -16,6 +16,7 @@ interface DataTableProps<TData, TValue> {
onFeaturesChange: (features: FeatureType[]) => void;
sort: AdminWorkspaceSort | undefined;
onSortChange: (sort: AdminWorkspaceSort | undefined) => void;
loading?: boolean;
onPaginationChange: Dispatch<
SetStateAction<{
pageIndex: number;
@@ -36,6 +37,7 @@ export function DataTable<TData extends { id: string }, TValue>({
sort,
onSortChange,
onPaginationChange,
loading = false,
}: DataTableProps<TData, TValue>) {
return (
<SharedDataTable
@@ -54,8 +56,11 @@ export function DataTable<TData extends { id: string }, TValue>({
onFeaturesChange={onFeaturesChange}
sort={sort}
onSortChange={onSortChange}
disabled={loading}
/>
)}
loading={loading}
disablePagination={loading}
/>
);
}

View File

@@ -13,7 +13,7 @@ export function WorkspacePage() {
AdminWorkspaceSort.CreatedAt
);
const { workspaces, pagination, setPagination, workspacesCount } =
const { workspaces, pagination, setPagination, workspacesCount, loading } =
useWorkspaceList({
keyword,
features: featureFilters,
@@ -38,6 +38,7 @@ export function WorkspacePage() {
onFeaturesChange={setFeatureFilters}
sort={sort}
onSortChange={setSort}
loading={loading}
/>
</div>
);

View File

@@ -51,30 +51,43 @@ export const useWorkspaceList = (filter?: {
]
);
const { data: listData } = useQuery(
const { data: listData, isValidating: isListValidating } = useQuery(
{
query: adminWorkspacesQuery,
variables,
},
{
keepPreviousData: true,
revalidateOnFocus: false,
revalidateIfStale: true,
revalidateOnReconnect: true,
}
);
const { data: countData } = useQuery(
const { data: countData, isValidating: isCountValidating } = useQuery(
{
query: adminWorkspacesCountQuery,
variables,
},
{
keepPreviousData: true,
revalidateOnFocus: false,
revalidateIfStale: true,
revalidateOnReconnect: true,
}
);
const loading =
isListValidating ||
isCountValidating ||
listData === undefined ||
countData === undefined;
return {
workspaces: listData?.adminWorkspaces ?? [],
workspacesCount: countData?.adminWorkspacesCount ?? 0,
pagination,
setPagination,
loading,
};
};