🚧 wip: paginated tables

This commit is contained in:
Fred KISSIE
2026-01-28 04:46:54 +01:00
parent 12aea2901d
commit 38ac4c5980
4 changed files with 36 additions and 57 deletions

View File

@@ -77,7 +77,7 @@ const listSitesSchema = z.object({
limit: z limit: z
.string() .string()
.optional() .optional()
.default("1000") .default("1")
.transform(Number) .transform(Number)
.pipe(z.int().positive()), .pipe(z.int().positive()),
offset: z offset: z
@@ -130,7 +130,7 @@ type SiteWithUpdateAvailable = Awaited<ReturnType<typeof querySites>>[0] & {
export type ListSitesResponse = { export type ListSitesResponse = {
sites: SiteWithUpdateAvailable[]; sites: SiteWithUpdateAvailable[];
pagination: { total: number; limit: number; offset: number }; pagination: { total: number; limit: number; offset: number; };
}; };
registry.registerPath({ registry.registerPath({

View File

@@ -60,8 +60,6 @@ export default async function SitesPage(props: SitesPageProps) {
return ( return (
<> <>
{/* <SitesSplashCard /> */}
<SettingsSectionTitle <SettingsSectionTitle
title={t("siteManageSites")} title={t("siteManageSites")}
description={t("siteDescription")} description={t("siteDescription")}

View File

@@ -1,37 +1,33 @@
"use client"; "use client";
import { Column, ColumnDef } from "@tanstack/react-table"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { ExtendedColumnDef } from "@app/components/ui/data-table";
import { SitesDataTable } from "@app/components/SitesDataTable"; import { SitesDataTable } from "@app/components/SitesDataTable";
import { Badge } from "@app/components/ui/badge";
import { Button } from "@app/components/ui/button";
import { ExtendedColumnDef } from "@app/components/ui/data-table";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button"; import { InfoPopup } from "@app/components/ui/info-popup";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { parseDataSize } from "@app/lib/dataSize";
import { build } from "@server/build";
import { Column } from "@tanstack/react-table";
import { import {
ArrowRight, ArrowRight,
ArrowUpDown, ArrowUpDown,
ArrowUpRight, ArrowUpRight,
Check, MoreHorizontal
MoreHorizontal,
X
} from "lucide-react"; } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { AxiosResponse } from "axios"; import { useEffect, useState, useTransition } from "react";
import { useState, useEffect } from "react";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { toast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import { parseDataSize } from "@app/lib/dataSize";
import { Badge } from "@app/components/ui/badge";
import { InfoPopup } from "@app/components/ui/info-popup";
import { build } from "@server/build";
export type SiteRow = { export type SiteRow = {
id: number; id: number;
@@ -61,22 +57,13 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null); const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
const [rows, setRows] = useState<SiteRow[]>(sites); const [rows, setRows] = useState<SiteRow[]>(sites);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, startTransition] = useTransition();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext();
// Update local state when props change (e.g., after refresh)
useEffect(() => {
setRows(sites);
}, [sites]);
const refreshData = async () => { const refreshData = async () => {
console.log("Data refreshed");
setIsRefreshing(true);
try { try {
await new Promise((resolve) => setTimeout(resolve, 200));
router.refresh(); router.refresh();
} catch (error) { } catch (error) {
toast({ toast({
@@ -84,8 +71,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
description: t("refreshError"), description: t("refreshError"),
variant: "destructive" variant: "destructive"
}); });
} finally {
setIsRefreshing(false);
} }
}; };
@@ -456,7 +441,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
createSite={() => createSite={() =>
router.push(`/${orgId}/settings/sites/create`) router.push(`/${orgId}/settings/sites/create`)
} }
onRefresh={refreshData} onRefresh={() => startTransition(refreshData)}
isRefreshing={isRefreshing} isRefreshing={isRefreshing}
columnVisibility={{ columnVisibility={{
niceId: false, niceId: false,

View File

@@ -113,7 +113,7 @@ export const orgQueries = {
return res.data.data.clients; return res.data.data.clients;
} }
}), }),
users: ({ orgId }: { orgId: string }) => users: ({ orgId }: { orgId: string; }) =>
queryOptions({ queryOptions({
queryKey: ["ORG", orgId, "USERS"] as const, queryKey: ["ORG", orgId, "USERS"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -124,7 +124,7 @@ export const orgQueries = {
return res.data.data.users; return res.data.data.users;
} }
}), }),
roles: ({ orgId }: { orgId: string }) => roles: ({ orgId }: { orgId: string; }) =>
queryOptions({ queryOptions({
queryKey: ["ORG", orgId, "ROLES"] as const, queryKey: ["ORG", orgId, "ROLES"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -136,7 +136,7 @@ export const orgQueries = {
} }
}), }),
sites: ({ orgId }: { orgId: string }) => sites: ({ orgId }: { orgId: string; }) =>
queryOptions({ queryOptions({
queryKey: ["ORG", orgId, "SITES"] as const, queryKey: ["ORG", orgId, "SITES"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -147,7 +147,7 @@ export const orgQueries = {
} }
}), }),
domains: ({ orgId }: { orgId: string }) => domains: ({ orgId }: { orgId: string; }) =>
queryOptions({ queryOptions({
queryKey: ["ORG", orgId, "DOMAINS"] as const, queryKey: ["ORG", orgId, "DOMAINS"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -169,7 +169,7 @@ export const orgQueries = {
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<{ AxiosResponse<{
idps: { idpId: number; name: string }[]; idps: { idpId: number; name: string; }[];
}> }>
>( >(
build === "saas" || useOrgOnlyIdp build === "saas" || useOrgOnlyIdp
@@ -188,23 +188,19 @@ export const logAnalyticsFiltersSchema = z.object({
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {
error: "timeStart must be a valid ISO date string" error: "timeStart must be a valid ISO date string"
}) })
.optional(), .optional().catch(undefined),
timeEnd: z timeEnd: z
.string() .string()
.refine((val) => !isNaN(Date.parse(val)), { .refine((val) => !isNaN(Date.parse(val)), {
error: "timeEnd must be a valid ISO date string" error: "timeEnd must be a valid ISO date string"
}) })
.optional(), .optional().catch(undefined),
resourceId: z resourceId: z.coerce.number().optional().catch(undefined)
.string()
.optional()
.transform(Number)
.pipe(z.int().positive())
.optional()
}); });
export type LogAnalyticsFilters = z.TypeOf<typeof logAnalyticsFiltersSchema>; export type LogAnalyticsFilters = z.TypeOf<typeof logAnalyticsFiltersSchema>;
export const logQueries = { export const logQueries = {
requestAnalytics: ({ requestAnalytics: ({
orgId, orgId,
@@ -234,7 +230,7 @@ export const logQueries = {
}; };
export const resourceQueries = { export const resourceQueries = {
resourceUsers: ({ resourceId }: { resourceId: number }) => resourceUsers: ({ resourceId }: { resourceId: number; }) =>
queryOptions({ queryOptions({
queryKey: ["RESOURCES", resourceId, "USERS"] as const, queryKey: ["RESOURCES", resourceId, "USERS"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -244,7 +240,7 @@ export const resourceQueries = {
return res.data.data.users; return res.data.data.users;
} }
}), }),
resourceRoles: ({ resourceId }: { resourceId: number }) => resourceRoles: ({ resourceId }: { resourceId: number; }) =>
queryOptions({ queryOptions({
queryKey: ["RESOURCES", resourceId, "ROLES"] as const, queryKey: ["RESOURCES", resourceId, "ROLES"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -255,7 +251,7 @@ export const resourceQueries = {
return res.data.data.roles; return res.data.data.roles;
} }
}), }),
siteResourceUsers: ({ siteResourceId }: { siteResourceId: number }) => siteResourceUsers: ({ siteResourceId }: { siteResourceId: number; }) =>
queryOptions({ queryOptions({
queryKey: ["SITE_RESOURCES", siteResourceId, "USERS"] as const, queryKey: ["SITE_RESOURCES", siteResourceId, "USERS"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -265,7 +261,7 @@ export const resourceQueries = {
return res.data.data.users; return res.data.data.users;
} }
}), }),
siteResourceRoles: ({ siteResourceId }: { siteResourceId: number }) => siteResourceRoles: ({ siteResourceId }: { siteResourceId: number; }) =>
queryOptions({ queryOptions({
queryKey: ["SITE_RESOURCES", siteResourceId, "ROLES"] as const, queryKey: ["SITE_RESOURCES", siteResourceId, "ROLES"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -276,7 +272,7 @@ export const resourceQueries = {
return res.data.data.roles; return res.data.data.roles;
} }
}), }),
siteResourceClients: ({ siteResourceId }: { siteResourceId: number }) => siteResourceClients: ({ siteResourceId }: { siteResourceId: number; }) =>
queryOptions({ queryOptions({
queryKey: ["SITE_RESOURCES", siteResourceId, "CLIENTS"] as const, queryKey: ["SITE_RESOURCES", siteResourceId, "CLIENTS"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -287,7 +283,7 @@ export const resourceQueries = {
return res.data.data.clients; return res.data.data.clients;
} }
}), }),
resourceTargets: ({ resourceId }: { resourceId: number }) => resourceTargets: ({ resourceId }: { resourceId: number; }) =>
queryOptions({ queryOptions({
queryKey: ["RESOURCES", resourceId, "TARGETS"] as const, queryKey: ["RESOURCES", resourceId, "TARGETS"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -298,7 +294,7 @@ export const resourceQueries = {
return res.data.data.targets; return res.data.data.targets;
} }
}), }),
resourceWhitelist: ({ resourceId }: { resourceId: number }) => resourceWhitelist: ({ resourceId }: { resourceId: number; }) =>
queryOptions({ queryOptions({
queryKey: ["RESOURCES", resourceId, "WHITELISTS"] as const, queryKey: ["RESOURCES", resourceId, "WHITELISTS"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -371,7 +367,7 @@ export const approvalQueries = {
} }
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<{ approvals: ApprovalItem[] }> AxiosResponse<{ approvals: ApprovalItem[]; }>
>(`/org/${orgId}/approvals?${sp.toString()}`, { >(`/org/${orgId}/approvals?${sp.toString()}`, {
signal signal
}); });
@@ -383,7 +379,7 @@ export const approvalQueries = {
queryKey: ["APPROVALS", orgId, "COUNT", "pending"] as const, queryKey: ["APPROVALS", orgId, "COUNT", "pending"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<{ count: number }> AxiosResponse<{ count: number; }>
>(`/org/${orgId}/approvals/count?approvalState=pending`, { >(`/org/${orgId}/approvals/count?approvalState=pending`, {
signal signal
}); });