mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-23 01:05:27 +00:00
Paginate the tables with queries
This commit is contained in:
@@ -21,7 +21,7 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
import { and, eq, inArray, like, sql } from "drizzle-orm";
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
orgId: z.string().nonempty()
|
orgId: z.string().nonempty()
|
||||||
@@ -40,6 +40,7 @@ const querySchema = z.strictObject({
|
|||||||
.default("0")
|
.default("0")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.number().int().nonnegative()),
|
.pipe(z.number().int().nonnegative()),
|
||||||
|
query: z.string().optional(),
|
||||||
siteId: z
|
siteId: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -112,7 +113,7 @@ export async function listAlertRules(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { limit, offset, siteId, resourceId } = parsedQuery.data;
|
const { limit, offset, query, siteId, resourceId } = parsedQuery.data;
|
||||||
|
|
||||||
// Resolve siteId filter → matching alertRuleIds
|
// Resolve siteId filter → matching alertRuleIds
|
||||||
let siteFilterRuleIds: number[] | null = null;
|
let siteFilterRuleIds: number[] | null = null;
|
||||||
@@ -160,6 +161,9 @@ export async function listAlertRules(
|
|||||||
|
|
||||||
const whereClause = and(
|
const whereClause = and(
|
||||||
eq(alertRules.orgId, orgId),
|
eq(alertRules.orgId, orgId),
|
||||||
|
query
|
||||||
|
? like(sql`LOWER(${alertRules.name})`, `%${query.toLowerCase()}%`)
|
||||||
|
: undefined,
|
||||||
siteFilterRuleIds !== null
|
siteFilterRuleIds !== null
|
||||||
? inArray(alertRules.alertRuleId, siteFilterRuleIds)
|
? inArray(alertRules.alertRuleId, siteFilterRuleIds)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { and, eq, isNull, sql } from "drizzle-orm";
|
import { and, eq, like, sql } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
@@ -39,7 +39,8 @@ const querySchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.default("0")
|
.default("0")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.int().nonnegative())
|
.pipe(z.int().nonnegative()),
|
||||||
|
query: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
@@ -80,10 +81,16 @@ export async function listHealthChecks(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { limit, offset } = parsedQuery.data;
|
const { limit, offset, query } = parsedQuery.data;
|
||||||
|
|
||||||
const whereClause = and(
|
const whereClause = and(
|
||||||
eq(targetHealthCheck.orgId, orgId),
|
eq(targetHealthCheck.orgId, orgId),
|
||||||
|
query
|
||||||
|
? like(
|
||||||
|
sql`LOWER(${targetHealthCheck.name})`,
|
||||||
|
`%${query.toLowerCase()}%`
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const list = await db
|
const list = await db
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { Switch } from "@app/components/ui/switch";
|
import { Switch } from "@app/components/ui/switch";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { orgQueries } from "@app/lib/queries";
|
import { orgQueries } from "@app/lib/queries";
|
||||||
@@ -26,6 +27,7 @@ import { useState } from "react";
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { PaginationState } from "@tanstack/react-table";
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
import type { DataTablePaginationState } from "@app/components/ui/data-table";
|
import type { DataTablePaginationState } from "@app/components/ui/data-table";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
|
||||||
type AlertingRulesTableProps = {
|
type AlertingRulesTableProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -105,28 +107,27 @@ export default function AlertingRulesTable({ orgId }: AlertingRulesTableProps) {
|
|||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
||||||
|
|
||||||
|
const {
|
||||||
|
navigate: filter,
|
||||||
|
isNavigating: isFiltering,
|
||||||
|
searchParams
|
||||||
|
} = useNavigationContext();
|
||||||
|
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [selected, setSelected] = useState<AlertRuleRow | null>(null);
|
const [selected, setSelected] = useState<AlertRuleRow | null>(null);
|
||||||
const [togglingId, setTogglingId] = useState<number | null>(null);
|
const [togglingId, setTogglingId] = useState<number | null>(null);
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
|
||||||
const [pageSize, setPageSize] = useState(() => {
|
const page = Math.max(1, Number(searchParams.get("page") ?? 1));
|
||||||
if (typeof window === "undefined") return 20;
|
const pageSize = Math.max(1, Number(searchParams.get("pageSize") ?? 20));
|
||||||
try {
|
const pageIndex = page - 1;
|
||||||
const stored = localStorage.getItem("Org-alerting-rules-table-size");
|
const query = searchParams.get("query") ?? undefined;
|
||||||
if (stored) {
|
|
||||||
const parsed = parseInt(stored, 10);
|
|
||||||
if (parsed > 0 && parsed <= 1000) return parsed;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return 20;
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
refetch,
|
refetch,
|
||||||
isRefetching
|
isRefetching
|
||||||
} = useQuery(orgQueries.alertRules({ orgId, limit: pageSize, offset: pageIndex * pageSize }));
|
} = useQuery(orgQueries.alertRules({ orgId, limit: pageSize, offset: pageIndex * pageSize, query }));
|
||||||
|
|
||||||
const rows = data?.alertRules ?? [];
|
const rows = data?.alertRules ?? [];
|
||||||
const total = data?.pagination.total ?? 0;
|
const total = data?.pagination.total ?? 0;
|
||||||
@@ -135,10 +136,21 @@ export default function AlertingRulesTable({ orgId }: AlertingRulesTableProps) {
|
|||||||
const paginationState: DataTablePaginationState = { pageIndex, pageSize, pageCount };
|
const paginationState: DataTablePaginationState = { pageIndex, pageSize, pageCount };
|
||||||
|
|
||||||
const handlePaginationChange = (newState: PaginationState) => {
|
const handlePaginationChange = (newState: PaginationState) => {
|
||||||
setPageIndex(newState.pageIndex);
|
searchParams.set("page", (newState.pageIndex + 1).toString());
|
||||||
setPageSize(newState.pageSize);
|
searchParams.set("pageSize", newState.pageSize.toString());
|
||||||
|
filter({ searchParams });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = useDebouncedCallback((value: string) => {
|
||||||
|
if (value) {
|
||||||
|
searchParams.set("query", value);
|
||||||
|
} else {
|
||||||
|
searchParams.delete("query");
|
||||||
|
}
|
||||||
|
searchParams.delete("page");
|
||||||
|
filter({ searchParams });
|
||||||
|
}, 300);
|
||||||
|
|
||||||
const invalidate = () =>
|
const invalidate = () =>
|
||||||
queryClient.invalidateQueries({ queryKey: ["ORG", orgId, "ALERT_RULES"] });
|
queryClient.invalidateQueries({ queryKey: ["ORG", orgId, "ALERT_RULES"] });
|
||||||
|
|
||||||
@@ -308,15 +320,16 @@ export default function AlertingRulesTable({ orgId }: AlertingRulesTableProps) {
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={rows}
|
||||||
persistPageSize="Org-alerting-rules-table"
|
|
||||||
title={t("alertingRules")}
|
title={t("alertingRules")}
|
||||||
searchPlaceholder={t("alertingSearchRules")}
|
searchPlaceholder={t("alertingSearchRules")}
|
||||||
searchColumn="name"
|
onSearch={handleSearchChange}
|
||||||
|
searchQuery={query}
|
||||||
|
manualFiltering
|
||||||
onAdd={() => {
|
onAdd={() => {
|
||||||
router.push(`/${orgId}/settings/alerting/create`);
|
router.push(`/${orgId}/settings/alerting/create`);
|
||||||
}}
|
}}
|
||||||
onRefresh={() => refetch()}
|
onRefresh={() => refetch()}
|
||||||
isRefreshing={isRefetching || isLoading}
|
isRefreshing={isRefetching || isLoading || isFiltering}
|
||||||
addButtonText={t("alertingAddRule")}
|
addButtonText={t("alertingAddRule")}
|
||||||
enableColumnVisibility
|
enableColumnVisibility
|
||||||
stickyLeftColumn="name"
|
stickyLeftColumn="name"
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { useTranslations } from "next-intl";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { PaginationState } from "@tanstack/react-table";
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
import type { DataTablePaginationState } from "@app/components/ui/data-table";
|
import type { DataTablePaginationState } from "@app/components/ui/data-table";
|
||||||
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
@@ -74,23 +76,20 @@ export default function HealthChecksTable({
|
|||||||
const isPaid = isPaidUser(tierMatrix.standaloneHealthChecks);
|
const isPaid = isPaidUser(tierMatrix.standaloneHealthChecks);
|
||||||
|
|
||||||
const [credenzaOpen, setCredenzaOpen] = useState(false);
|
const [credenzaOpen, setCredenzaOpen] = useState(false);
|
||||||
|
const {
|
||||||
|
navigate: filter,
|
||||||
|
isNavigating: isFiltering,
|
||||||
|
searchParams
|
||||||
|
} = useNavigationContext();
|
||||||
|
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [selected, setSelected] = useState<HealthCheckRow | null>(null);
|
const [selected, setSelected] = useState<HealthCheckRow | null>(null);
|
||||||
const [togglingId, setTogglingId] = useState<number | null>(null);
|
const [togglingId, setTogglingId] = useState<number | null>(null);
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
|
||||||
const [pageSize, setPageSize] = useState(() => {
|
const page = Math.max(1, Number(searchParams.get("page") ?? 1));
|
||||||
if (typeof window === "undefined") return 20;
|
const pageSize = Math.max(1, Number(searchParams.get("pageSize") ?? 20));
|
||||||
try {
|
const pageIndex = page - 1;
|
||||||
const stored = localStorage.getItem(
|
const query = searchParams.get("query") ?? undefined;
|
||||||
"Org-standalone-health-checks-table-size"
|
|
||||||
);
|
|
||||||
if (stored) {
|
|
||||||
const parsed = parseInt(stored, 10);
|
|
||||||
if (parsed > 0 && parsed <= 1000) return parsed;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return 20;
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -101,7 +100,8 @@ export default function HealthChecksTable({
|
|||||||
...orgQueries.standaloneHealthChecks({
|
...orgQueries.standaloneHealthChecks({
|
||||||
orgId,
|
orgId,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: pageIndex * pageSize
|
offset: pageIndex * pageSize,
|
||||||
|
query
|
||||||
}),
|
}),
|
||||||
refetchInterval: 10_000
|
refetchInterval: 10_000
|
||||||
});
|
});
|
||||||
@@ -117,10 +117,21 @@ export default function HealthChecksTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePaginationChange = (newState: PaginationState) => {
|
const handlePaginationChange = (newState: PaginationState) => {
|
||||||
setPageIndex(newState.pageIndex);
|
searchParams.set("page", (newState.pageIndex + 1).toString());
|
||||||
setPageSize(newState.pageSize);
|
searchParams.set("pageSize", newState.pageSize.toString());
|
||||||
|
filter({ searchParams });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = useDebouncedCallback((value: string) => {
|
||||||
|
if (value) {
|
||||||
|
searchParams.set("query", value);
|
||||||
|
} else {
|
||||||
|
searchParams.delete("query");
|
||||||
|
}
|
||||||
|
searchParams.delete("page");
|
||||||
|
filter({ searchParams });
|
||||||
|
}, 300);
|
||||||
|
|
||||||
const invalidate = () =>
|
const invalidate = () =>
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["ORG", orgId, "STANDALONE_HEALTH_CHECKS"]
|
queryKey: ["ORG", orgId, "STANDALONE_HEALTH_CHECKS"]
|
||||||
@@ -376,17 +387,18 @@ export default function HealthChecksTable({
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={rows}
|
||||||
persistPageSize="Org-standalone-health-checks-table"
|
|
||||||
title={t("standaloneHcTableTitle")}
|
title={t("standaloneHcTableTitle")}
|
||||||
searchPlaceholder={t("standaloneHcSearchPlaceholder")}
|
searchPlaceholder={t("standaloneHcSearchPlaceholder")}
|
||||||
searchColumn="name"
|
onSearch={handleSearchChange}
|
||||||
|
searchQuery={query}
|
||||||
|
manualFiltering
|
||||||
onAdd={() => {
|
onAdd={() => {
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
setCredenzaOpen(true);
|
setCredenzaOpen(true);
|
||||||
}}
|
}}
|
||||||
addButtonDisabled={!isPaid}
|
addButtonDisabled={!isPaid}
|
||||||
onRefresh={() => refetch()}
|
onRefresh={() => refetch()}
|
||||||
isRefreshing={isRefetching || isLoading}
|
isRefreshing={isRefetching || isLoading || isFiltering}
|
||||||
addButtonText={t("standaloneHcAddButton")}
|
addButtonText={t("standaloneHcAddButton")}
|
||||||
enableColumnVisibility
|
enableColumnVisibility
|
||||||
stickyLeftColumn="name"
|
stickyLeftColumn="name"
|
||||||
|
|||||||
@@ -259,18 +259,24 @@ export const orgQueries = {
|
|||||||
alertRules: ({
|
alertRules: ({
|
||||||
orgId,
|
orgId,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
offset = 0
|
offset = 0,
|
||||||
|
query
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
query?: string;
|
||||||
}) =>
|
}) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["ORG", orgId, "ALERT_RULES", { limit, offset }] as const,
|
queryKey: ["ORG", orgId, "ALERT_RULES", { limit, offset, query }] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const sp = new URLSearchParams();
|
||||||
|
sp.set("limit", String(limit));
|
||||||
|
sp.set("offset", String(offset));
|
||||||
|
if (query) sp.set("query", query);
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListAlertRulesResponse>
|
AxiosResponse<ListAlertRulesResponse>
|
||||||
>(`/org/${orgId}/alert-rules?limit=${limit}&offset=${offset}`, { signal });
|
>(`/org/${orgId}/alert-rules?${sp.toString()}`, { signal });
|
||||||
return {
|
return {
|
||||||
alertRules: res.data.data.alertRules,
|
alertRules: res.data.data.alertRules,
|
||||||
pagination: res.data.data.pagination
|
pagination: res.data.data.pagination
|
||||||
@@ -303,15 +309,21 @@ export const orgQueries = {
|
|||||||
standaloneHealthChecks: ({
|
standaloneHealthChecks: ({
|
||||||
orgId,
|
orgId,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
offset = 0
|
offset = 0,
|
||||||
|
query
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
query?: string;
|
||||||
}) =>
|
}) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["ORG", orgId, "STANDALONE_HEALTH_CHECKS", { limit, offset }] as const,
|
queryKey: ["ORG", orgId, "STANDALONE_HEALTH_CHECKS", { limit, offset, query }] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const sp = new URLSearchParams();
|
||||||
|
sp.set("limit", String(limit));
|
||||||
|
sp.set("offset", String(offset));
|
||||||
|
if (query) sp.set("query", query);
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<{
|
AxiosResponse<{
|
||||||
healthChecks: {
|
healthChecks: {
|
||||||
@@ -344,7 +356,7 @@ export const orgQueries = {
|
|||||||
offset: number;
|
offset: number;
|
||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
>(`/org/${orgId}/health-checks?limit=${limit}&offset=${offset}`, { signal });
|
>(`/org/${orgId}/health-checks?${sp.toString()}`, { signal });
|
||||||
return {
|
return {
|
||||||
healthChecks: res.data.data.healthChecks,
|
healthChecks: res.data.data.healthChecks,
|
||||||
pagination: res.data.data.pagination
|
pagination: res.data.data.pagination
|
||||||
|
|||||||
Reference in New Issue
Block a user