mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-08 03:36:37 +00:00
🚧 POC: pagination in sites table
This commit is contained in:
@@ -74,18 +74,20 @@ const listSitesParamsSchema = z.strictObject({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const listSitesSchema = z.object({
|
const listSitesSchema = z.object({
|
||||||
limit: z
|
pageSize: z.coerce
|
||||||
.string()
|
.number<string>() // for prettier formatting
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
.optional()
|
.optional()
|
||||||
.default("1")
|
.catch(20)
|
||||||
.transform(Number)
|
.default(20),
|
||||||
.pipe(z.int().positive()),
|
page: z.coerce
|
||||||
offset: z
|
.number<string>() // for prettier formatting
|
||||||
.string()
|
.int()
|
||||||
|
.min(0)
|
||||||
.optional()
|
.optional()
|
||||||
.default("0")
|
.catch(1)
|
||||||
.transform(Number)
|
.default(1)
|
||||||
.pipe(z.int().nonnegative())
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function querySites(orgId: string, accessibleSiteIds: number[]) {
|
function querySites(orgId: string, accessibleSiteIds: number[]) {
|
||||||
@@ -130,7 +132,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; pageSize: number; page: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
@@ -160,7 +162,7 @@ export async function listSites(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { limit, offset } = parsedQuery.data;
|
const { pageSize, page } = parsedQuery.data;
|
||||||
|
|
||||||
const parsedParams = listSitesParamsSchema.safeParse(req.params);
|
const parsedParams = listSitesParamsSchema.safeParse(req.params);
|
||||||
if (!parsedParams.success) {
|
if (!parsedParams.success) {
|
||||||
@@ -216,7 +218,9 @@ export async function listSites(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const sitesList = await baseQuery.limit(limit).offset(offset);
|
const sitesList = await baseQuery
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(pageSize * (page - 1));
|
||||||
const totalCountResult = await countQuery;
|
const totalCountResult = await countQuery;
|
||||||
const totalCount = totalCountResult[0].count;
|
const totalCount = totalCountResult[0].count;
|
||||||
|
|
||||||
@@ -267,8 +271,8 @@ export async function listSites(
|
|||||||
sites: sitesWithUpdates,
|
sites: sitesWithUpdates,
|
||||||
pagination: {
|
pagination: {
|
||||||
total: totalCount,
|
total: totalCount,
|
||||||
limit,
|
pageSize,
|
||||||
offset
|
page
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -9,19 +9,30 @@ import { getTranslations } from "next-intl/server";
|
|||||||
|
|
||||||
type SitesPageProps = {
|
type SitesPageProps = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
|
searchParams: Promise<Record<string, string>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function SitesPage(props: SitesPageProps) {
|
export default async function SitesPage(props: SitesPageProps) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(await props.searchParams);
|
||||||
|
|
||||||
let sites: ListSitesResponse["sites"] = [];
|
let sites: ListSitesResponse["sites"] = [];
|
||||||
|
let pagination: ListSitesResponse["pagination"] = {
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const res = await internal.get<AxiosResponse<ListSitesResponse>>(
|
const res = await internal.get<AxiosResponse<ListSitesResponse>>(
|
||||||
`/org/${params.orgId}/sites`,
|
`/org/${params.orgId}/sites?${searchParams.toString()}`,
|
||||||
await authCookieHeader()
|
await authCookieHeader()
|
||||||
);
|
);
|
||||||
sites = res.data.data.sites;
|
const responseData = res.data.data;
|
||||||
|
sites = responseData.sites;
|
||||||
|
pagination = responseData.pagination;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
@@ -67,7 +78,17 @@ export default async function SitesPage(props: SitesPageProps) {
|
|||||||
|
|
||||||
<SitesBanner />
|
<SitesBanner />
|
||||||
|
|
||||||
<SitesTable sites={siteRows} orgId={params.orgId} />
|
<SitesTable
|
||||||
|
sites={siteRows}
|
||||||
|
orgId={params.orgId}
|
||||||
|
pagination={{
|
||||||
|
pageCount: Math.ceil(
|
||||||
|
pagination.total / pagination.pageSize
|
||||||
|
),
|
||||||
|
pageIndex: pagination.page - 1,
|
||||||
|
pageSize: pagination.pageSize
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
|
||||||
import { DataTable } from "@app/components/ui/data-table";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
|
||||||
columns: ColumnDef<TData, TValue>[];
|
|
||||||
data: TData[];
|
|
||||||
createSite?: () => void;
|
|
||||||
onRefresh?: () => void;
|
|
||||||
isRefreshing?: boolean;
|
|
||||||
columnVisibility?: Record<string, boolean>;
|
|
||||||
enableColumnVisibility?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SitesDataTable<TData, TValue>({
|
|
||||||
columns,
|
|
||||||
data,
|
|
||||||
createSite,
|
|
||||||
onRefresh,
|
|
||||||
isRefreshing,
|
|
||||||
columnVisibility,
|
|
||||||
enableColumnVisibility
|
|
||||||
}: DataTableProps<TData, TValue>) {
|
|
||||||
const t = useTranslations();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataTable
|
|
||||||
columns={columns}
|
|
||||||
data={data}
|
|
||||||
persistPageSize="sites-table"
|
|
||||||
title={t("sites")}
|
|
||||||
searchPlaceholder={t("searchSitesProgress")}
|
|
||||||
searchColumn="name"
|
|
||||||
onAdd={createSite}
|
|
||||||
addButtonText={t("siteAdd")}
|
|
||||||
onRefresh={onRefresh}
|
|
||||||
isRefreshing={isRefreshing}
|
|
||||||
defaultSort={{
|
|
||||||
id: "name",
|
|
||||||
desc: false
|
|
||||||
}}
|
|
||||||
columnVisibility={columnVisibility}
|
|
||||||
enableColumnVisibility={enableColumnVisibility}
|
|
||||||
stickyLeftColumn="name"
|
|
||||||
stickyRightColumn="actions"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import { SitesDataTable } from "@app/components/SitesDataTable";
|
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import { Badge } from "@app/components/ui/badge";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
import {
|
||||||
|
DataTable,
|
||||||
|
ExtendedColumnDef,
|
||||||
|
type DataTablePaginationState
|
||||||
|
} from "@app/components/ui/data-table";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -26,7 +30,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
|
|
||||||
export type SiteRow = {
|
export type SiteRow = {
|
||||||
@@ -48,15 +52,21 @@ export type SiteRow = {
|
|||||||
|
|
||||||
type SitesTableProps = {
|
type SitesTableProps = {
|
||||||
sites: SiteRow[];
|
sites: SiteRow[];
|
||||||
|
pagination: DataTablePaginationState;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
export default function SitesTable({
|
||||||
|
sites,
|
||||||
|
orgId,
|
||||||
|
pagination
|
||||||
|
}: SitesTableProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
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 [isRefreshing, startTransition] = useTransition();
|
const [isRefreshing, startTransition] = useTransition();
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
@@ -87,10 +97,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
router.refresh();
|
router.refresh();
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
|
|
||||||
const newRows = rows.filter((row) => row.id !== siteId);
|
|
||||||
|
|
||||||
setRows(newRows);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -413,6 +419,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
sites,
|
||||||
|
pagination
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{selectedSite && (
|
{selectedSite && (
|
||||||
@@ -429,27 +440,50 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t("siteConfirmDelete")}
|
buttonText={t("siteConfirmDelete")}
|
||||||
onConfirm={async () => deleteSite(selectedSite!.id)}
|
onConfirm={async () =>
|
||||||
|
startTransition(() => deleteSite(selectedSite!.id))
|
||||||
|
}
|
||||||
string={selectedSite.name}
|
string={selectedSite.name}
|
||||||
title={t("siteDelete")}
|
title={t("siteDelete")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SitesDataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={sites}
|
||||||
createSite={() =>
|
persistPageSize="sites-table"
|
||||||
router.push(`/${orgId}/settings/sites/create`)
|
title={t("sites")}
|
||||||
}
|
searchPlaceholder={t("searchSitesProgress")}
|
||||||
|
manualFiltering
|
||||||
|
pagination={pagination}
|
||||||
|
onPaginationChange={(newPage) => {
|
||||||
|
console.log({
|
||||||
|
newPage
|
||||||
|
});
|
||||||
|
const sp = new URLSearchParams(searchParams);
|
||||||
|
sp.set("page", (newPage.pageIndex + 1).toString());
|
||||||
|
sp.set("pageSize", newPage.pageSize.toString());
|
||||||
|
startTransition(() =>
|
||||||
|
router.push(`${pathname}?${sp.toString()}`)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onAdd={() => router.push(`/${orgId}/settings/sites/create`)}
|
||||||
|
addButtonText={t("siteAdd")}
|
||||||
onRefresh={() => startTransition(refreshData)}
|
onRefresh={() => startTransition(refreshData)}
|
||||||
isRefreshing={isRefreshing}
|
isRefreshing={isRefreshing}
|
||||||
|
defaultSort={{
|
||||||
|
id: "name",
|
||||||
|
desc: false
|
||||||
|
}}
|
||||||
columnVisibility={{
|
columnVisibility={{
|
||||||
niceId: false,
|
niceId: false,
|
||||||
nice: false,
|
nice: false,
|
||||||
exitNode: false,
|
exitNode: false,
|
||||||
address: false
|
address: false
|
||||||
}}
|
}}
|
||||||
enableColumnVisibility={true}
|
enableColumnVisibility
|
||||||
|
stickyLeftColumn="name"
|
||||||
|
stickyRightColumn="actions"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -151,11 +151,20 @@ type DataTableFilter = {
|
|||||||
label: string;
|
label: string;
|
||||||
options: FilterOption[];
|
options: FilterOption[];
|
||||||
multiSelect?: boolean;
|
multiSelect?: boolean;
|
||||||
filterFn: (row: any, selectedValues: (string | number | boolean)[]) => boolean;
|
filterFn: (
|
||||||
|
row: any,
|
||||||
|
selectedValues: (string | number | boolean)[]
|
||||||
|
) => boolean;
|
||||||
defaultValues?: (string | number | boolean)[];
|
defaultValues?: (string | number | boolean)[];
|
||||||
displayMode?: "label" | "calculated"; // How to display the filter button text
|
displayMode?: "label" | "calculated"; // How to display the filter button text
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DataTablePaginationState = PaginationState & {
|
||||||
|
pageCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DataTablePaginationUpdateFn = (newPage: PaginationState) => void;
|
||||||
|
|
||||||
type DataTableProps<TData, TValue> = {
|
type DataTableProps<TData, TValue> = {
|
||||||
columns: ExtendedColumnDef<TData, TValue>[];
|
columns: ExtendedColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
@@ -178,6 +187,11 @@ type DataTableProps<TData, TValue> = {
|
|||||||
defaultPageSize?: number;
|
defaultPageSize?: number;
|
||||||
columnVisibility?: Record<string, boolean>;
|
columnVisibility?: Record<string, boolean>;
|
||||||
enableColumnVisibility?: boolean;
|
enableColumnVisibility?: boolean;
|
||||||
|
manualFiltering?: boolean;
|
||||||
|
onSearch?: (input: string) => void;
|
||||||
|
searchValue?: string;
|
||||||
|
pagination?: DataTablePaginationState;
|
||||||
|
onPaginationChange?: DataTablePaginationUpdateFn;
|
||||||
persistColumnVisibility?: boolean | string;
|
persistColumnVisibility?: boolean | string;
|
||||||
stickyLeftColumn?: string; // Column ID or accessorKey for left sticky column
|
stickyLeftColumn?: string; // Column ID or accessorKey for left sticky column
|
||||||
stickyRightColumn?: string; // Column ID or accessorKey for right sticky column (typically "actions")
|
stickyRightColumn?: string; // Column ID or accessorKey for right sticky column (typically "actions")
|
||||||
@@ -203,7 +217,12 @@ export function DataTable<TData, TValue>({
|
|||||||
columnVisibility: defaultColumnVisibility,
|
columnVisibility: defaultColumnVisibility,
|
||||||
enableColumnVisibility = false,
|
enableColumnVisibility = false,
|
||||||
persistColumnVisibility = false,
|
persistColumnVisibility = false,
|
||||||
|
manualFiltering = false,
|
||||||
|
pagination: paginationState,
|
||||||
stickyLeftColumn,
|
stickyLeftColumn,
|
||||||
|
onSearch,
|
||||||
|
searchValue,
|
||||||
|
onPaginationChange,
|
||||||
stickyRightColumn
|
stickyRightColumn
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -248,22 +267,25 @@ export function DataTable<TData, TValue>({
|
|||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||||
initialColumnVisibility
|
initialColumnVisibility
|
||||||
);
|
);
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [_pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: pageSize
|
pageSize: pageSize
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pagination = paginationState ?? _pagination;
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<string>(
|
const [activeTab, setActiveTab] = useState<string>(
|
||||||
defaultTab || tabs?.[0]?.id || ""
|
defaultTab || tabs?.[0]?.id || ""
|
||||||
);
|
);
|
||||||
const [activeFilters, setActiveFilters] = useState<Record<string, (string | number | boolean)[]>>(
|
const [activeFilters, setActiveFilters] = useState<
|
||||||
() => {
|
Record<string, (string | number | boolean)[]>
|
||||||
const initial: Record<string, (string | number | boolean)[]> = {};
|
>(() => {
|
||||||
filters?.forEach((filter) => {
|
const initial: Record<string, (string | number | boolean)[]> = {};
|
||||||
initial[filter.id] = filter.defaultValues || [];
|
filters?.forEach((filter) => {
|
||||||
});
|
initial[filter.id] = filter.defaultValues || [];
|
||||||
return initial;
|
});
|
||||||
}
|
return initial;
|
||||||
);
|
});
|
||||||
|
|
||||||
// Track initial values to avoid storing defaults on first render
|
// Track initial values to avoid storing defaults on first render
|
||||||
const initialPageSize = useRef(pageSize);
|
const initialPageSize = useRef(pageSize);
|
||||||
@@ -309,7 +331,16 @@ export function DataTable<TData, TValue>({
|
|||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
onGlobalFilterChange: setGlobalFilter,
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
onPaginationChange: setPagination,
|
onPaginationChange: onPaginationChange
|
||||||
|
? (state) => {
|
||||||
|
const newState =
|
||||||
|
typeof state === "function" ? state(pagination) : state;
|
||||||
|
onPaginationChange(newState);
|
||||||
|
}
|
||||||
|
: setPagination,
|
||||||
|
manualFiltering,
|
||||||
|
manualPagination: !!paginationState,
|
||||||
|
pageCount: paginationState?.pageCount,
|
||||||
initialState: {
|
initialState: {
|
||||||
pagination: {
|
pagination: {
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
@@ -368,11 +399,11 @@ export function DataTable<TData, TValue>({
|
|||||||
setActiveFilters((prev) => {
|
setActiveFilters((prev) => {
|
||||||
const currentValues = prev[filterId] || [];
|
const currentValues = prev[filterId] || [];
|
||||||
const filter = filters?.find((f) => f.id === filterId);
|
const filter = filters?.find((f) => f.id === filterId);
|
||||||
|
|
||||||
if (!filter) return prev;
|
if (!filter) return prev;
|
||||||
|
|
||||||
let newValues: (string | number | boolean)[];
|
let newValues: (string | number | boolean)[];
|
||||||
|
|
||||||
if (filter.multiSelect) {
|
if (filter.multiSelect) {
|
||||||
// Multi-select: add or remove the value
|
// Multi-select: add or remove the value
|
||||||
if (checked) {
|
if (checked) {
|
||||||
@@ -397,7 +428,7 @@ export function DataTable<TData, TValue>({
|
|||||||
// Calculate display text for a filter based on selected values
|
// Calculate display text for a filter based on selected values
|
||||||
const getFilterDisplayText = (filter: DataTableFilter): string => {
|
const getFilterDisplayText = (filter: DataTableFilter): string => {
|
||||||
const selectedValues = activeFilters[filter.id] || [];
|
const selectedValues = activeFilters[filter.id] || [];
|
||||||
|
|
||||||
if (selectedValues.length === 0) {
|
if (selectedValues.length === 0) {
|
||||||
return filter.label;
|
return filter.label;
|
||||||
}
|
}
|
||||||
@@ -477,12 +508,14 @@ export function DataTable<TData, TValue>({
|
|||||||
<div className="relative w-full sm:max-w-sm">
|
<div className="relative w-full sm:max-w-sm">
|
||||||
<Input
|
<Input
|
||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
value={globalFilter ?? ""}
|
value={searchValue ?? globalFilter ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
table.setGlobalFilter(
|
onSearch
|
||||||
String(e.target.value)
|
? onSearch(e.currentTarget.value)
|
||||||
)
|
: table.setGlobalFilter(
|
||||||
}
|
String(e.target.value)
|
||||||
|
);
|
||||||
|
}}
|
||||||
className="w-full pl-8"
|
className="w-full pl-8"
|
||||||
/>
|
/>
|
||||||
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
||||||
@@ -490,13 +523,17 @@ export function DataTable<TData, TValue>({
|
|||||||
{filters && filters.length > 0 && (
|
{filters && filters.length > 0 && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{filters.map((filter) => {
|
{filters.map((filter) => {
|
||||||
const selectedValues = activeFilters[filter.id] || [];
|
const selectedValues =
|
||||||
const hasActiveFilters = selectedValues.length > 0;
|
activeFilters[filter.id] || [];
|
||||||
const displayMode = filter.displayMode || filterDisplayMode;
|
const hasActiveFilters =
|
||||||
const displayText = displayMode === "calculated"
|
selectedValues.length > 0;
|
||||||
? getFilterDisplayText(filter)
|
const displayMode =
|
||||||
: filter.label;
|
filter.displayMode || filterDisplayMode;
|
||||||
|
const displayText =
|
||||||
|
displayMode === "calculated"
|
||||||
|
? getFilterDisplayText(filter)
|
||||||
|
: filter.label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu key={filter.id}>
|
<DropdownMenu key={filter.id}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -507,37 +544,54 @@ export function DataTable<TData, TValue>({
|
|||||||
>
|
>
|
||||||
<Filter className="h-4 w-4 mr-2" />
|
<Filter className="h-4 w-4 mr-2" />
|
||||||
{displayText}
|
{displayText}
|
||||||
{displayMode === "label" && hasActiveFilters && (
|
{displayMode === "label" &&
|
||||||
<span className="ml-2 bg-muted text-foreground rounded-full px-2 py-0.5 text-xs">
|
hasActiveFilters && (
|
||||||
{selectedValues.length}
|
<span className="ml-2 bg-muted text-foreground rounded-full px-2 py-0.5 text-xs">
|
||||||
</span>
|
{
|
||||||
)}
|
selectedValues.length
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="w-48">
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
className="w-48"
|
||||||
|
>
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
{filter.label}
|
{filter.label}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{filter.options.map((option) => {
|
{filter.options.map(
|
||||||
const isChecked = selectedValues.includes(option.value);
|
(option) => {
|
||||||
return (
|
const isChecked =
|
||||||
<DropdownMenuCheckboxItem
|
selectedValues.includes(
|
||||||
key={option.id}
|
option.value
|
||||||
checked={isChecked}
|
);
|
||||||
onCheckedChange={(checked) =>
|
return (
|
||||||
handleFilterChange(
|
<DropdownMenuCheckboxItem
|
||||||
filter.id,
|
key={option.id}
|
||||||
option.value,
|
checked={
|
||||||
|
isChecked
|
||||||
|
}
|
||||||
|
onCheckedChange={(
|
||||||
checked
|
checked
|
||||||
)
|
) =>
|
||||||
}
|
handleFilterChange(
|
||||||
onSelect={(e) => e.preventDefault()}
|
filter.id,
|
||||||
>
|
option.value,
|
||||||
{option.label}
|
checked
|
||||||
</DropdownMenuCheckboxItem>
|
)
|
||||||
);
|
}
|
||||||
})}
|
onSelect={(e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,19 +188,20 @@ 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().catch(undefined),
|
.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().catch(undefined),
|
.optional()
|
||||||
|
.catch(undefined),
|
||||||
resourceId: z.coerce.number().optional().catch(undefined)
|
resourceId: z.coerce.number().optional().catch(undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LogAnalyticsFilters = z.TypeOf<typeof logAnalyticsFiltersSchema>;
|
export type LogAnalyticsFilters = z.TypeOf<typeof logAnalyticsFiltersSchema>;
|
||||||
|
|
||||||
|
|
||||||
export const logQueries = {
|
export const logQueries = {
|
||||||
requestAnalytics: ({
|
requestAnalytics: ({
|
||||||
orgId,
|
orgId,
|
||||||
@@ -230,7 +231,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 }) => {
|
||||||
@@ -240,7 +241,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 }) => {
|
||||||
@@ -251,7 +252,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 }) => {
|
||||||
@@ -261,7 +262,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 }) => {
|
||||||
@@ -272,7 +273,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 }) => {
|
||||||
@@ -283,7 +284,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 }) => {
|
||||||
@@ -294,7 +295,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 }) => {
|
||||||
@@ -367,7 +368,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
|
||||||
});
|
});
|
||||||
@@ -379,7 +380,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