mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: data-loading (#364)
* chore: remove dead comment in database page * fix(data-table): manualPagination support * fix(hooks): useServerDataTable for server-side pagination * fix(database): paginated backup and restoration list actions * fix(database): server-paginate backup list * fix(database): server-paginate restoration list * fix(database): switch tabs via History API instead of router.push * fix(logs): load job logs on demand when the modal opens * perf(database): slim metadata poll and drop eager backup/log loading * fix(database): invalidate backups/restorations list keys on mutations for instant refresh * fix(logs): disable log button when row has no logs * fix(logs): fetch logs on button click, open modal already populated * fix: loading log modal --------- Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
This commit is contained in:
co-authored by
charles-gauthereau
parent
141abbae9d
commit
47d78943d8
+22
-31
@@ -2,7 +2,7 @@ import { PageParams } from "@/types/next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { Page } from "@/features/layout/components/page";
|
||||
import { db } from "@/db";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import { eq, and, inArray, isNull } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import { getOrganizationProjectDatabases } from "@/db/services/project";
|
||||
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||
@@ -49,38 +49,22 @@ export default async function RoutePage(
|
||||
redirect("/dashboard/projects");
|
||||
}
|
||||
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||
with: {
|
||||
restorations: true,
|
||||
storages: {
|
||||
with: {
|
||||
storageChannel: true,
|
||||
},
|
||||
},
|
||||
logs: true,
|
||||
},
|
||||
orderBy: (b, { desc }) => [desc(b.createdAt)],
|
||||
});
|
||||
|
||||
const restorations = await db.query.restoration.findMany({
|
||||
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
||||
with: {
|
||||
logs: true,
|
||||
},
|
||||
orderBy: (r, { desc }) => [desc(r.createdAt)],
|
||||
});
|
||||
|
||||
//const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||
|
||||
const totalBackups = await db
|
||||
.select({ count: drizzleDb.schemas.backup.id })
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
||||
.then((rows) => rows.length);
|
||||
|
||||
const availableBackups = backups.filter((b) => !b.deletedAt).length;
|
||||
const availableBackups = await db
|
||||
.select({ count: drizzleDb.schemas.backup.id })
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows.length);
|
||||
|
||||
const successfulBackups = await db
|
||||
.select({ count: drizzleDb.schemas.backup.id })
|
||||
@@ -93,6 +77,17 @@ export default async function RoutePage(
|
||||
)
|
||||
.then((rows) => rows.length);
|
||||
|
||||
const isAlreadyRestore = await db
|
||||
.select({ count: drizzleDb.schemas.restoration.id })
|
||||
.from(drizzleDb.schemas.restoration)
|
||||
.where(
|
||||
and(
|
||||
eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
|
||||
eq(drizzleDb.schemas.restoration.status, "waiting"),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows.length > 0);
|
||||
|
||||
const [settings] = await db
|
||||
.select()
|
||||
.from(drizzleDb.schemas.setting)
|
||||
@@ -109,8 +104,6 @@ export default async function RoutePage(
|
||||
const successRate =
|
||||
totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||
|
||||
//const isMember = activeMember?.role === "member";
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<LogsModalProvider>
|
||||
@@ -121,8 +114,6 @@ export default async function RoutePage(
|
||||
database={dbItem}
|
||||
databaseHealthLogs={databaseHealthLogs}
|
||||
isAlreadyRestore={isAlreadyRestore}
|
||||
restorations={restorations}
|
||||
backups={backups}
|
||||
totalBackups={totalBackups}
|
||||
availableBackups={availableBackups}
|
||||
successRate={successRate}
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
getFilteredRowModel,
|
||||
PaginationState,
|
||||
OnChangeFn,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
|
||||
@@ -42,6 +44,13 @@ interface DataTableProps<TData, TValue> {
|
||||
};
|
||||
highlightRow?: (row: TData) => boolean;
|
||||
selectedActions?: (rows: TData[]) => ReactNode;
|
||||
manualPagination?: boolean;
|
||||
rowCount?: number;
|
||||
paginationState?: { pageIndex: number; pageSize: number };
|
||||
onPaginationChange?: OnChangeFn<PaginationState>;
|
||||
sorting?: SortingState;
|
||||
onSortingChange?: OnChangeFn<SortingState>;
|
||||
isFetching?: boolean;
|
||||
|
||||
}
|
||||
|
||||
@@ -56,10 +65,19 @@ export function DataTable<TData, TValue>({
|
||||
emptyButton,
|
||||
highlightRow,
|
||||
selectedActions,
|
||||
manualPagination = false,
|
||||
rowCount,
|
||||
paginationState,
|
||||
onPaginationChange,
|
||||
sorting: controlledSorting,
|
||||
onSortingChange: controlledOnSortingChange,
|
||||
isFetching = false,
|
||||
|
||||
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [internalSorting, setInternalSorting] = useState<SortingState>([]);
|
||||
const sorting = controlledSorting ?? internalSorting;
|
||||
const setSorting = controlledOnSortingChange ?? setInternalSorting;
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
|
||||
@@ -96,7 +114,7 @@ export function DataTable<TData, TValue>({
|
||||
data,
|
||||
columns: finalColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getPaginationRowModel: manualPagination ? undefined : getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
@@ -106,10 +124,15 @@ export function DataTable<TData, TValue>({
|
||||
autoResetPageIndex: false,
|
||||
autoResetExpanded: false,
|
||||
enableRowSelection: true,
|
||||
manualPagination,
|
||||
manualSorting: manualPagination,
|
||||
rowCount: manualPagination ? rowCount : undefined,
|
||||
onPaginationChange: manualPagination ? onPaginationChange : undefined,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
rowSelection,
|
||||
...(manualPagination && paginationState ? { pagination: paginationState } : {}),
|
||||
},
|
||||
});
|
||||
const router = useRouter();
|
||||
@@ -194,18 +217,22 @@ export function DataTable<TData, TValue>({
|
||||
selected.
|
||||
</div>
|
||||
)}
|
||||
{enablePagination && table.getFilteredRowModel().rows.length >= 1 && (
|
||||
{enablePagination && (manualPagination ? (rowCount ?? 0) > 0 : table.getFilteredRowModel().rows.length >= 1) && (
|
||||
<TablePagination
|
||||
table={table}
|
||||
maxVisiblePages={paginationOptions?.pageVisible}
|
||||
pageSizeOptions={(() => {
|
||||
const rowCount = table.getFilteredRowModel().rows.length;
|
||||
const allSizes = paginationOptions.pageSize.sort((a, b) => a - b);
|
||||
const validSizes = allSizes.filter((size) => size <= rowCount);
|
||||
const nextSize = allSizes.find((size) => size > rowCount);
|
||||
if (nextSize) validSizes.push(nextSize);
|
||||
return validSizes;
|
||||
})()}
|
||||
pageSizeOptions={
|
||||
manualPagination
|
||||
? paginationOptions.pageSize
|
||||
: (() => {
|
||||
const rowCountLocal = table.getFilteredRowModel().rows.length;
|
||||
const allSizes = paginationOptions.pageSize.sort((a, b) => a - b);
|
||||
const validSizes = allSizes.filter((size) => size <= rowCountLocal);
|
||||
const nextSize = allSizes.find((size) => size > rowCountLocal);
|
||||
if (nextSize) validSizes.push(nextSize);
|
||||
return validSizes;
|
||||
})()
|
||||
}
|
||||
className={paginationOptions.className}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -142,10 +142,12 @@ export type BackupWith = Backup & {
|
||||
restorations?: Restoration[] | null;
|
||||
storages?: BackupStorage[] | null;
|
||||
logs?: JobLog[] | null;
|
||||
hasLogs?: boolean;
|
||||
};
|
||||
|
||||
export type RestorationWith = Restoration & {
|
||||
logs?: JobLog[] | null;
|
||||
hasLogs?: boolean;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {z} from "zod";
|
||||
import {db} from "@/db";
|
||||
import {and, eq, isNull} from "drizzle-orm";
|
||||
import {and, count, eq, inArray, isNull} from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {BackupWith, RestorationWith} from "@/db/schema/07_database";
|
||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
||||
@@ -31,33 +30,50 @@ export const getDatabaseDataAction = userAction
|
||||
}
|
||||
});
|
||||
|
||||
const backups = await db.query.backup.findMany({
|
||||
where: eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
with: {
|
||||
restorations: true,
|
||||
storages: {
|
||||
with: {
|
||||
storageChannel: true
|
||||
}
|
||||
},
|
||||
logs: true
|
||||
},
|
||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
||||
}) as BackupWith[];
|
||||
const [totalRow] = await db
|
||||
.select({count: count()})
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(eq(drizzleDb.schemas.backup.databaseId, databaseId));
|
||||
const totalBackups = totalRow?.count ?? 0;
|
||||
|
||||
const restorations = await db.query.restoration.findMany({
|
||||
where: eq(drizzleDb.schemas.restoration.databaseId, databaseId),
|
||||
with: {
|
||||
logs: true
|
||||
},
|
||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
||||
}) as RestorationWith[];
|
||||
const [availableRow] = await db
|
||||
.select({count: count()})
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
isNull(drizzleDb.schemas.backup.deletedAt),
|
||||
));
|
||||
const availableBackups = availableRow?.count ?? 0;
|
||||
|
||||
const [successRow] = await db
|
||||
.select({count: count()})
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
eq(drizzleDb.schemas.backup.status, "success"),
|
||||
));
|
||||
const successfulBackups = successRow?.count ?? 0;
|
||||
|
||||
const totalBackups = backups.length;
|
||||
const availableBackups = backups.filter(b => !b.deletedAt).length;
|
||||
const successfulBackups = backups.filter(b => b.status === "success").length;
|
||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||
|
||||
const [activeBackupRow] = await db
|
||||
.select({count: count()})
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"]),
|
||||
));
|
||||
const isAlreadyBackup = (activeBackupRow?.count ?? 0) > 0;
|
||||
|
||||
const [activeRestoreRow] = await db
|
||||
.select({count: count()})
|
||||
.from(drizzleDb.schemas.restoration)
|
||||
.where(and(
|
||||
eq(drizzleDb.schemas.restoration.databaseId, databaseId),
|
||||
eq(drizzleDb.schemas.restoration.status, "waiting"),
|
||||
));
|
||||
const isAlreadyRestore = (activeRestoreRow?.count ?? 0) > 0;
|
||||
|
||||
// @ts-ignore
|
||||
let activeOrganizationChannels = [];
|
||||
// @ts-ignore
|
||||
@@ -71,11 +87,8 @@ export const getDatabaseDataAction = userAction
|
||||
activeOrganizationStorageChannels = organizationStorageChannels.filter(channel => channel.enabled);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
database,
|
||||
backups,
|
||||
restorations,
|
||||
// @ts-ignore
|
||||
activeOrganizationChannels,
|
||||
// @ts-ignore
|
||||
@@ -85,6 +98,8 @@ export const getDatabaseDataAction = userAction
|
||||
availableBackups,
|
||||
successRate
|
||||
},
|
||||
health: database ? await getHealthLast12hLogs({ id: database.id }) : []
|
||||
isAlreadyRestore,
|
||||
isAlreadyBackup,
|
||||
health: database ? await getHealthLast12hLogs({id: database.id}) : []
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"use server";
|
||||
import { userAction } from "@/lib/safe-actions/actions";
|
||||
import { db } from "@/db";
|
||||
import { and, count, eq, inArray, isNotNull, isNull } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import { BackupWith, RestorationWith } from "@/db/schema/07_database";
|
||||
import {
|
||||
FetchBackupsSchema,
|
||||
FetchRestorationsSchema,
|
||||
} from "@/features/database/actions/backup-list.schema";
|
||||
|
||||
export const fetchBackupsAction = userAction
|
||||
.schema(FetchBackupsSchema)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const { databaseId, page, pageSize, filter } = parsedInput;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const deletedCondition =
|
||||
filter === "available"
|
||||
? isNull(drizzleDb.schemas.backup.deletedAt)
|
||||
: filter === "deleted"
|
||||
? isNotNull(drizzleDb.schemas.backup.deletedAt)
|
||||
: undefined;
|
||||
|
||||
const where = and(
|
||||
eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
||||
deletedCondition,
|
||||
);
|
||||
|
||||
const [totalResult] = await db
|
||||
.select({ count: count() })
|
||||
.from(drizzleDb.schemas.backup)
|
||||
.where(where);
|
||||
|
||||
const data = (await db.query.backup.findMany({
|
||||
where,
|
||||
with: {
|
||||
restorations: true,
|
||||
storages: {
|
||||
with: {
|
||||
storageChannel: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: (b, { desc }) => [desc(b.createdAt)],
|
||||
limit: pageSize,
|
||||
offset,
|
||||
})) as BackupWith[];
|
||||
|
||||
const backupIds = data.map((b) => b.id);
|
||||
const backupsWithLogs = backupIds.length
|
||||
? await db
|
||||
.select({ backupId: drizzleDb.schemas.jobLog.backupId })
|
||||
.from(drizzleDb.schemas.jobLog)
|
||||
.where(inArray(drizzleDb.schemas.jobLog.backupId, backupIds))
|
||||
.groupBy(drizzleDb.schemas.jobLog.backupId)
|
||||
: [];
|
||||
const loggedBackupIds = new Set(backupsWithLogs.map((l) => l.backupId));
|
||||
for (const b of data) {
|
||||
b.hasLogs = loggedBackupIds.has(b.id);
|
||||
}
|
||||
|
||||
const total = totalResult?.count ?? 0;
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const fetchRestorationsAction = userAction
|
||||
.schema(FetchRestorationsSchema)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const { databaseId, page, pageSize } = parsedInput;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const where = eq(drizzleDb.schemas.restoration.databaseId, databaseId);
|
||||
|
||||
const [totalResult] = await db
|
||||
.select({ count: count() })
|
||||
.from(drizzleDb.schemas.restoration)
|
||||
.where(where);
|
||||
|
||||
const data = (await db.query.restoration.findMany({
|
||||
where,
|
||||
orderBy: (r, { desc }) => [desc(r.createdAt)],
|
||||
limit: pageSize,
|
||||
offset,
|
||||
})) as RestorationWith[];
|
||||
|
||||
const restorationIds = data.map((r) => r.id);
|
||||
const restorationsWithLogs = restorationIds.length
|
||||
? await db
|
||||
.select({ restorationId: drizzleDb.schemas.jobLog.restorationId })
|
||||
.from(drizzleDb.schemas.jobLog)
|
||||
.where(inArray(drizzleDb.schemas.jobLog.restorationId, restorationIds))
|
||||
.groupBy(drizzleDb.schemas.jobLog.restorationId)
|
||||
: [];
|
||||
const loggedRestorationIds = new Set(
|
||||
restorationsWithLogs.map((l) => l.restorationId),
|
||||
);
|
||||
for (const r of data) {
|
||||
r.hasLogs = loggedRestorationIds.has(r.id);
|
||||
}
|
||||
|
||||
const total = totalResult?.count ?? 0;
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const FetchBackupsSchema = z.object({
|
||||
databaseId: z.string(),
|
||||
page: z.number().int().min(1).default(1),
|
||||
pageSize: z.number().int().min(1).max(100).default(20),
|
||||
filter: z.enum(["available", "deleted"]).optional(),
|
||||
sorting: z.any().optional(),
|
||||
});
|
||||
export type FetchBackupsSchema = z.infer<typeof FetchBackupsSchema>;
|
||||
|
||||
export const FetchRestorationsSchema = z.object({
|
||||
databaseId: z.string(),
|
||||
page: z.number().int().min(1).default(1),
|
||||
pageSize: z.number().int().min(1).max(100).default(20),
|
||||
sorting: z.any().optional(),
|
||||
});
|
||||
export type FetchRestorationsSchema = z.infer<typeof FetchRestorationsSchema>;
|
||||
@@ -101,6 +101,12 @@ export const BackupActionsForm = ({
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["database-data", backup.databaseId],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["backups", backup.databaseId],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["restorations", backup.databaseId],
|
||||
});
|
||||
router.refresh();
|
||||
if (action === "download") {
|
||||
const url = inner.value;
|
||||
@@ -121,6 +127,9 @@ export const BackupActionsForm = ({
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["database-data", backup.databaseId],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["backups", backup.databaseId],
|
||||
});
|
||||
router.refresh();
|
||||
closeModal();
|
||||
} else {
|
||||
@@ -144,6 +153,9 @@ export const BackupActionsForm = ({
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["database-data", backup.databaseId],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["backups", backup.databaseId],
|
||||
});
|
||||
router.refresh();
|
||||
closeModal();
|
||||
} else {
|
||||
|
||||
@@ -29,6 +29,9 @@ export const BackupButton = (props: BackupButtonProps) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["database-data", props.databaseId],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["backups", props.databaseId],
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(backup?.serverError || "Failed to create backup.");
|
||||
|
||||
@@ -112,8 +112,7 @@ export function backupColumns(
|
||||
accessorKey: "logs",
|
||||
header: "Logs",
|
||||
cell: ({row}) => {
|
||||
const logs = row.original.logs ?? [];
|
||||
return <LogsModalTrigger logs={logs}/>;
|
||||
return <LogsModalTrigger backupId={row.original.id} hasLogs={row.original.hasLogs}/>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,23 +13,22 @@ import {toast} from "sonner";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {deleteBackupAction} from "@/features/database/actions/backup-actions.action";
|
||||
import {ButtonWithConfirm} from "@/components/common/button-with-confirm";
|
||||
|
||||
import {useServerDataTable} from "@/hooks/use-server-data-table";
|
||||
import {fetchBackupsAction} from "@/features/database/actions/backup-list.action";
|
||||
import type {FetchBackupsSchema} from "@/features/database/actions/backup-list.schema";
|
||||
|
||||
type DatabaseBackupListProps = {
|
||||
isAlreadyRestore: boolean;
|
||||
settings: Setting;
|
||||
database: DatabaseWith;
|
||||
backups: BackupWith[];
|
||||
activeMember: MemberWithUser
|
||||
activeMember: MemberWithUser;
|
||||
}
|
||||
|
||||
|
||||
export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
|
||||
const items = [
|
||||
{label: "Deleted", value: "deleted"},
|
||||
{label: "Available", value: "available"},
|
||||
]
|
||||
];
|
||||
|
||||
const [selectedFilters, setSelectedFilters] = useState<FilterItem[]>([items[1]]);
|
||||
const [isActionsOpen, setIsActionsOpen] = useState(false);
|
||||
@@ -39,20 +38,19 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
return backupColumns(props.isAlreadyRestore, props.settings, props.database, props.activeMember);
|
||||
}, [props.isAlreadyRestore, props.activeMember.id, props.activeMember.role]);
|
||||
|
||||
const filteredBackups = useMemo(() => {
|
||||
if (!props.backups) return [];
|
||||
|
||||
return props.backups.filter(backup => {
|
||||
if (selectedFilters.length > 0) {
|
||||
const selectedValues = selectedFilters.map(f => f.value);
|
||||
const status = backup.deletedAt != null ? "deleted" : "available";
|
||||
if (!selectedValues.includes(status)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [props.backups, selectedFilters]);
|
||||
const filter = useMemo<"available" | "deleted" | undefined>(() => {
|
||||
const values = selectedFilters.map((f) => f.value);
|
||||
if (values.length === 1) return values[0] as "available" | "deleted";
|
||||
return undefined;
|
||||
}, [selectedFilters]);
|
||||
|
||||
const {data, tableProps, isFetching, isLoading} = useServerDataTable<BackupWith, FetchBackupsSchema>({
|
||||
fetchAction: fetchBackupsAction,
|
||||
queryKey: ["backups", props.database.id],
|
||||
extraParams: {databaseId: props.database.id, filter},
|
||||
initialPageSize: 20,
|
||||
refetchInterval: 4000,
|
||||
});
|
||||
|
||||
const handleSelectFilter = (item: FilterItem) => {
|
||||
setSelectedFilters(prev =>
|
||||
@@ -64,29 +62,26 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
|
||||
const clearFilters = () => setSelectedFilters([]);
|
||||
|
||||
|
||||
const mutationDeleteBackups = useMutation({
|
||||
mutationFn: async (backups: Backup[]) => {
|
||||
const results = await Promise.all(
|
||||
backups.map(async (backup) => {
|
||||
if (backup.deletedAt == null || backup.status == "ongoing") {
|
||||
|
||||
const backupDeleted = await deleteBackupAction({
|
||||
databaseId: backup.databaseId,
|
||||
backupId: backup.id,
|
||||
})
|
||||
});
|
||||
return {
|
||||
success: backupDeleted?.data?.success,
|
||||
message: backupDeleted?.data?.success
|
||||
? backupDeleted?.data?.actionSuccess?.message
|
||||
// @ts-ignore
|
||||
: restoration?.data?.actionError.message,
|
||||
: "Failed to delete backup.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: `Already deleted this backup (ref: ${backup.id}).`,
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
results.forEach((result) => {
|
||||
@@ -96,7 +91,7 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
toast.error(result.message);
|
||||
}
|
||||
});
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", props.database.id]});
|
||||
queryClient.invalidateQueries({queryKey: ["backups", props.database.id]});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -106,8 +101,10 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
<DataTable
|
||||
enableSelect={!isMember}
|
||||
columns={columns}
|
||||
data={filteredBackups}
|
||||
data={data}
|
||||
enablePagination
|
||||
isFetching={isFetching || isLoading}
|
||||
{...tableProps}
|
||||
selectedActions={(rows) => (
|
||||
<>
|
||||
<div className="flex justify-start md:justify-between gap-3 md:gap-0 items-center w-full ml-0">
|
||||
@@ -156,9 +153,8 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {DatabaseBackupActionsModal} from "@/features/database/components/backup-actions-modal";
|
||||
import {DatabaseTabs} from "@/features/database/components/database-tabs";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {BackupWith, DatabaseWith, RestorationWith} from "@/db/schema/07_database";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {useBackupModal} from "@/features/database/components/backup-modal-context";
|
||||
import {DatabaseKpi} from "@/features/database/components/database-kpi";
|
||||
@@ -27,8 +27,6 @@ import {LogsModal} from "@/features/logs/components/logs-modal";
|
||||
|
||||
export type DatabaseContentProps = {
|
||||
settings: Setting;
|
||||
backups: BackupWith[];
|
||||
restorations: RestorationWith[];
|
||||
isAlreadyRestore: boolean;
|
||||
database: DatabaseWith;
|
||||
activeMember: MemberWithUser;
|
||||
@@ -53,14 +51,11 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
|
||||
return result?.data;
|
||||
},
|
||||
initialData: {
|
||||
// TODO : to be patched
|
||||
// @ts-ignore
|
||||
database: {
|
||||
...props.database,
|
||||
project: props.database.project ?? null,
|
||||
},
|
||||
backups: props.backups,
|
||||
restorations: props.restorations,
|
||||
activeOrganizationChannels: props.activeOrganizationChannels,
|
||||
activeOrganizationStorageChannels:
|
||||
props.activeOrganizationStorageChannels,
|
||||
@@ -69,16 +64,16 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
|
||||
availableBackups: props.availableBackups,
|
||||
successRate: props.successRate,
|
||||
},
|
||||
isAlreadyRestore: props.isAlreadyRestore,
|
||||
isAlreadyBackup: false,
|
||||
health: props.databaseHealthLogs
|
||||
},
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
refetchInterval: 1000,
|
||||
refetchInterval: 4000,
|
||||
});
|
||||
|
||||
const database = data?.database ?? props.database;
|
||||
const backups = data?.backups ?? props.backups;
|
||||
const restorations = data?.restorations ?? props.restorations;
|
||||
const activeOrganizationChannels =
|
||||
data?.activeOrganizationChannels ?? props.activeOrganizationChannels;
|
||||
const activeOrganizationStorageChannels =
|
||||
@@ -90,10 +85,8 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
|
||||
successRate: props.successRate,
|
||||
};
|
||||
|
||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||
const isAlreadyBackup = backups.some(
|
||||
(b) => b.status === "waiting" || b.status === "ongoing",
|
||||
);
|
||||
const isAlreadyRestore = data?.isAlreadyRestore ?? props.isAlreadyRestore;
|
||||
const isAlreadyBackup = data?.isAlreadyBackup ?? false;
|
||||
|
||||
const isMember = props.activeMember.role === "member";
|
||||
|
||||
@@ -166,8 +159,6 @@ export const DatabaseContent = (props: DatabaseContentProps) => {
|
||||
settings={props.settings}
|
||||
database={database}
|
||||
isAlreadyRestore={isAlreadyRestore}
|
||||
backups={backups}
|
||||
restorations={restorations}
|
||||
/>
|
||||
</PageContent>
|
||||
</>
|
||||
|
||||
@@ -4,18 +4,19 @@ import {restoreColumns} from "@/features/database/components/restore-columns";
|
||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
|
||||
import {ButtonWithLoading} from "@/components/common/button-with-loading";
|
||||
import {MoreHorizontal, Trash2} from "lucide-react";
|
||||
import {Restoration} from "@/db/schema/07_database";
|
||||
import {Restoration, RestorationWith} from "@/db/schema/07_database";
|
||||
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||
import {deleteRestoreAction} from "@/features/database/actions/restore.action";
|
||||
import {toast} from "sonner";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {useMemo, useState} from "react";
|
||||
import {ButtonWithConfirm} from "@/components/common/button-with-confirm";
|
||||
|
||||
import {useServerDataTable} from "@/hooks/use-server-data-table";
|
||||
import {fetchRestorationsAction} from "@/features/database/actions/backup-list.action";
|
||||
import type {FetchRestorationsSchema} from "@/features/database/actions/backup-list.schema";
|
||||
|
||||
type DatabaseRestoreListProps = {
|
||||
isAlreadyRestore: boolean;
|
||||
restorations: Restoration[];
|
||||
activeMember: MemberWithUser;
|
||||
databaseId: string;
|
||||
}
|
||||
@@ -28,6 +29,14 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||
return restoreColumns(props.isAlreadyRestore, props.activeMember);
|
||||
}, [props.isAlreadyRestore, props.activeMember.id, props.activeMember.role]);
|
||||
|
||||
const {data, tableProps, isFetching, isLoading} = useServerDataTable<RestorationWith, FetchRestorationsSchema>({
|
||||
fetchAction: fetchRestorationsAction,
|
||||
queryKey: ["restorations", props.databaseId],
|
||||
extraParams: {databaseId: props.databaseId},
|
||||
initialPageSize: 20,
|
||||
refetchInterval: 4000,
|
||||
});
|
||||
|
||||
const mutationDeleteRestorations = useMutation({
|
||||
mutationFn: async (restorations: Restoration[]) => {
|
||||
const results = await Promise.all(
|
||||
@@ -42,8 +51,6 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||
// @ts-ignore
|
||||
: restorationDeleted?.data?.actionError.message,
|
||||
};
|
||||
|
||||
|
||||
})
|
||||
);
|
||||
results.forEach((result) => {
|
||||
@@ -53,18 +60,19 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||
toast.error(result.message);
|
||||
}
|
||||
});
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", props.databaseId]});
|
||||
queryClient.invalidateQueries({queryKey: ["restorations", props.databaseId]});
|
||||
},
|
||||
});
|
||||
const isMember = props.activeMember.role === "member";
|
||||
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
enableSelect={!isMember}
|
||||
columns={columns}
|
||||
data={props.restorations}
|
||||
data={data}
|
||||
enablePagination
|
||||
isFetching={isFetching || isLoading}
|
||||
{...tableProps}
|
||||
selectedActions={(rows) => (
|
||||
<>
|
||||
{!isMember && (
|
||||
@@ -84,7 +92,7 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||
>Actions</ButtonWithLoading>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<ButtonWithConfirm
|
||||
<ButtonWithConfirm
|
||||
onConfirm={() => {
|
||||
mutationDeleteRestorations.mutate(rows)
|
||||
setIsActionsOpen(false);
|
||||
@@ -110,5 +118,5 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {BackupWith, DatabaseWith, Restoration} from "@/db/schema/07_database";
|
||||
import {useSearchParams} from "next/navigation";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {DatabaseBackupList} from "@/features/database/components/database-backup-list";
|
||||
import {DatabaseRestoreList} from "@/features/database/components/database-restore-list";
|
||||
@@ -11,8 +11,6 @@ import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
|
||||
export type DatabaseTabsProps = {
|
||||
settings: Setting,
|
||||
backups: BackupWith[],
|
||||
restorations: Restoration[],
|
||||
isAlreadyRestore: boolean,
|
||||
database: DatabaseWith,
|
||||
activeMember: MemberWithUser
|
||||
@@ -21,24 +19,27 @@ export type DatabaseTabsProps = {
|
||||
export const backupOnly = ["redis", "valkey"];
|
||||
|
||||
export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "backup");
|
||||
|
||||
useEffect(() => {
|
||||
const newTab = searchParams.get("tab") ?? "backup";
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setTab(newTab);
|
||||
}, [searchParams]);
|
||||
const onPopState = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
setTab(params.get("tab") ?? "backup");
|
||||
};
|
||||
window.addEventListener("popstate", onPopState);
|
||||
return () => window.removeEventListener("popstate", onPopState);
|
||||
}, []);
|
||||
|
||||
const handleChangeTab = (value: string) => {
|
||||
router.push(`?tab=${value}`);
|
||||
setTab(value);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set("tab", value);
|
||||
window.history.pushState(null, "", `?${params.toString()}`);
|
||||
};
|
||||
|
||||
|
||||
const isBackupOnly = backupOnly.some((type) => props.database.dbms === type)
|
||||
|
||||
const isBackupOnly = backupOnly.some((type) => props.database.dbms === type);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -47,7 +48,6 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
||||
isAlreadyRestore={props.isAlreadyRestore}
|
||||
settings={props.settings}
|
||||
database={props.database}
|
||||
backups={props.backups}
|
||||
activeMember={props.activeMember}
|
||||
/>
|
||||
:
|
||||
@@ -61,22 +61,18 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
|
||||
isAlreadyRestore={props.isAlreadyRestore}
|
||||
settings={props.settings}
|
||||
database={props.database}
|
||||
backups={props.backups}
|
||||
activeMember={props.activeMember}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent className="h-full justify-between" value="restore">
|
||||
<DatabaseRestoreList
|
||||
isAlreadyRestore={props.isAlreadyRestore}
|
||||
restorations={props.restorations}
|
||||
activeMember={props.activeMember}
|
||||
databaseId={props.database.id}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
}
|
||||
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,6 +41,7 @@ export const UploadBackupZone = ({onSuccessAction, database}: UploadRetentionZon
|
||||
toast.success(inner.actionSuccess?.message);
|
||||
onSuccessAction?.()
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", database.id]});
|
||||
queryClient.invalidateQueries({queryKey: ["backups", database.id]});
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(inner?.actionError?.message);
|
||||
@@ -50,6 +51,7 @@ export const UploadBackupZone = ({onSuccessAction, database}: UploadRetentionZon
|
||||
toast.error("An error occurred while upload in the backup");
|
||||
} finally {
|
||||
queryClient.invalidateQueries({queryKey: ["database-data", database.id]});
|
||||
queryClient.invalidateQueries({queryKey: ["backups", database.id]});
|
||||
setIsProcessing(false);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -62,8 +62,7 @@ export function restoreColumns(
|
||||
accessorKey: "logs",
|
||||
header: "Logs",
|
||||
cell: ({row}) => {
|
||||
const logs = row.original.logs ?? [];
|
||||
return <LogsModalTrigger logs={logs}/>;
|
||||
return <LogsModalTrigger restorationId={row.original.id} hasLogs={row.original.hasLogs}/>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -86,6 +85,9 @@ export function restoreColumns(
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["database-data", rowData.databaseId],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["restorations", rowData.databaseId],
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
toast.error(restoration.data.actionError.message);
|
||||
@@ -105,6 +107,9 @@ export function restoreColumns(
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["database-data", rowData.databaseId],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["restorations", rowData.databaseId],
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
toast.error(restoration.data.actionError.message);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"use server";
|
||||
import { userAction } from "@/lib/safe-actions/actions";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import { JobLog } from "@/db/schema/17_job-log";
|
||||
|
||||
export const fetchJobLogsAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
backupId: z.string().optional(),
|
||||
restorationId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput }): Promise<JobLog[]> => {
|
||||
const { backupId, restorationId } = parsedInput;
|
||||
|
||||
if (backupId) {
|
||||
return (await db.query.jobLog.findMany({
|
||||
where: eq(drizzleDb.schemas.jobLog.backupId, backupId),
|
||||
orderBy: (l, { asc }) => [asc(l.loggedAt)],
|
||||
})) as JobLog[];
|
||||
}
|
||||
if (restorationId) {
|
||||
return (await db.query.jobLog.findMany({
|
||||
where: eq(drizzleDb.schemas.jobLog.restorationId, restorationId),
|
||||
orderBy: (l, { asc }) => [asc(l.loggedAt)],
|
||||
})) as JobLog[];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
@@ -17,7 +17,7 @@ export const LogsModalProvider = ({children}: { children: ReactNode }) => {
|
||||
const [logs, setLogs] = useState<JobLog[]>([]);
|
||||
|
||||
const openModal = (newLogs: JobLog[]) => {
|
||||
setLogs(newLogs);
|
||||
setLogs(newLogs ?? []);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
import {FileText} from "lucide-react";
|
||||
import {JobLog} from "@/db/schema/17_job-log";
|
||||
import {FileText, Loader2} from "lucide-react";
|
||||
import {useState} from "react";
|
||||
import {toast} from "sonner";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useLogsModal} from "@/features/logs/components/logs-modal-context";
|
||||
import {fetchJobLogsAction} from "@/features/logs/actions/job-logs.action";
|
||||
|
||||
export type LogsModalTriggerProps = {
|
||||
logs: JobLog[]
|
||||
backupId?: string;
|
||||
restorationId?: string;
|
||||
hasLogs?: boolean;
|
||||
}
|
||||
|
||||
export const LogsModalTrigger = ({logs}: LogsModalTriggerProps) => {
|
||||
export const LogsModalTrigger = ({backupId, restorationId, hasLogs}: LogsModalTriggerProps) => {
|
||||
const {openModal} = useLogsModal();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await fetchJobLogsAction({backupId, restorationId});
|
||||
openModal(result?.data ?? []);
|
||||
} catch {
|
||||
toast.error("Failed to load logs.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button disabled={logs.length == 0} variant="outline" size="sm" onClick={()=> {
|
||||
openModal(logs);
|
||||
}}>
|
||||
<FileText />
|
||||
<Button disabled={!hasLogs || isLoading} variant="outline" size="sm" onClick={handleClick}>
|
||||
{isLoading ? <Loader2 className="animate-spin"/> : <FileText/>}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,11 +29,17 @@ export const LogsModal = () => {
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div>
|
||||
{logs.map((entry: JobLog) => (
|
||||
<LogRow key={entry.id} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
{logs.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||
No logs available
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{logs.map((entry: JobLog) => (
|
||||
<LogRow key={entry.id} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import type { PaginationState, SortingState } from "@tanstack/react-table";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface PaginatedResult<TData> {
|
||||
data: TData[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface UseServerDataTableProps<TData, TParams> {
|
||||
fetchAction: any;
|
||||
queryKey: any[];
|
||||
extraParams?: Partial<TParams>;
|
||||
initialPageSize?: number;
|
||||
initialSorting?: SortingState;
|
||||
enabled?: boolean;
|
||||
refetchInterval?: number | false;
|
||||
}
|
||||
|
||||
const EMPTY_ARRAY: any[] = [];
|
||||
|
||||
export function useServerDataTable<TData, TParams>(
|
||||
props: UseServerDataTableProps<TData, TParams>,
|
||||
) {
|
||||
const {
|
||||
fetchAction,
|
||||
queryKey,
|
||||
extraParams,
|
||||
initialPageSize = 20,
|
||||
initialSorting = [],
|
||||
enabled = true,
|
||||
refetchInterval = false,
|
||||
} = props;
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: initialPageSize,
|
||||
});
|
||||
const [sorting, setSorting] = useState<SortingState>(initialSorting);
|
||||
|
||||
useEffect(() => {
|
||||
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||
}, [sorting]);
|
||||
|
||||
useEffect(() => {
|
||||
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||
}, [JSON.stringify(extraParams)]);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [
|
||||
...queryKey,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
sorting,
|
||||
JSON.stringify(extraParams),
|
||||
],
|
||||
queryFn: async () => {
|
||||
const response = await fetchAction({
|
||||
page: pagination.pageIndex + 1,
|
||||
pageSize: pagination.pageSize,
|
||||
sorting,
|
||||
...extraParams,
|
||||
} as any);
|
||||
|
||||
const result = response?.data as PaginatedResult<TData> | undefined;
|
||||
if (!result || !Array.isArray(result.data)) {
|
||||
throw new Error("Failed to fetch paginated data");
|
||||
}
|
||||
return result;
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
enabled,
|
||||
gcTime: 2 * 60 * 1000,
|
||||
staleTime: 30 * 1000,
|
||||
refetchInterval,
|
||||
refetchOnWindowFocus: true,
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
return {
|
||||
data: (query.data?.data as TData[]) || EMPTY_ARRAY,
|
||||
rowCount: query.data?.meta?.total || 0,
|
||||
pagination,
|
||||
onPaginationChange: setPagination,
|
||||
sorting,
|
||||
onSortingChange: setSorting,
|
||||
tableProps: {
|
||||
manualPagination: true as const,
|
||||
rowCount: query.data?.meta?.total || 0,
|
||||
paginationState: pagination,
|
||||
onPaginationChange: setPagination,
|
||||
sorting,
|
||||
onSortingChange: setSorting,
|
||||
},
|
||||
meta: query.data?.meta,
|
||||
isLoading: query.isPending,
|
||||
isFetching: query.isFetching,
|
||||
refetch: query.refetch,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user