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

View File

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

View File

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

View File

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