mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-06 10:46:38 +00:00
🚧 wip: paginated tables
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user