mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 09:52:49 +08:00
feat: pre-aggregation workspace stats
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user