diff --git a/server/private/routers/alertRule/listAlertRules.ts b/server/private/routers/alertRule/listAlertRules.ts index 9d40817fb..601ab0fa3 100644 --- a/server/private/routers/alertRule/listAlertRules.ts +++ b/server/private/routers/alertRule/listAlertRules.ts @@ -21,7 +21,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; 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({ orgId: z.string().nonempty() @@ -40,6 +40,7 @@ const querySchema = z.strictObject({ .default("0") .transform(Number) .pipe(z.number().int().nonnegative()), + query: z.string().optional(), siteId: z .string() .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 let siteFilterRuleIds: number[] | null = null; @@ -160,6 +161,9 @@ export async function listAlertRules( const whereClause = and( eq(alertRules.orgId, orgId), + query + ? like(sql`LOWER(${alertRules.name})`, `%${query.toLowerCase()}%`) + : undefined, siteFilterRuleIds !== null ? inArray(alertRules.alertRuleId, siteFilterRuleIds) : undefined, diff --git a/server/private/routers/healthChecks/listHealthChecks.ts b/server/private/routers/healthChecks/listHealthChecks.ts index b2e6949a1..e266441b2 100644 --- a/server/private/routers/healthChecks/listHealthChecks.ts +++ b/server/private/routers/healthChecks/listHealthChecks.ts @@ -17,7 +17,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; 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 { z } from "zod"; import { fromError } from "zod-validation-error"; @@ -39,7 +39,8 @@ const querySchema = z.object({ .optional() .default("0") .transform(Number) - .pipe(z.int().nonnegative()) + .pipe(z.int().nonnegative()), + query: z.string().optional() }); registry.registerPath({ @@ -80,10 +81,16 @@ export async function listHealthChecks( ) ); } - const { limit, offset } = parsedQuery.data; + const { limit, offset, query } = parsedQuery.data; const whereClause = and( eq(targetHealthCheck.orgId, orgId), + query + ? like( + sql`LOWER(${targetHealthCheck.name})`, + `%${query.toLowerCase()}%` + ) + : undefined ); const list = await db diff --git a/src/components/AlertingRulesTable.tsx b/src/components/AlertingRulesTable.tsx index 1b34ae2bd..6c9bc7f0a 100644 --- a/src/components/AlertingRulesTable.tsx +++ b/src/components/AlertingRulesTable.tsx @@ -13,6 +13,7 @@ import { import { Switch } from "@app/components/ui/switch"; import { toast } from "@app/hooks/useToast"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { orgQueries } from "@app/lib/queries"; @@ -26,6 +27,7 @@ import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import type { PaginationState } from "@tanstack/react-table"; import type { DataTablePaginationState } from "@app/components/ui/data-table"; +import { useDebouncedCallback } from "use-debounce"; type AlertingRulesTableProps = { orgId: string; @@ -105,28 +107,27 @@ export default function AlertingRulesTable({ orgId }: AlertingRulesTableProps) { const { isPaidUser } = usePaidStatus(); const isPaid = isPaidUser(tierMatrix.alertingRules); + const { + navigate: filter, + isNavigating: isFiltering, + searchParams + } = useNavigationContext(); + const [deleteOpen, setDeleteOpen] = useState(false); const [selected, setSelected] = useState(null); const [togglingId, setTogglingId] = useState(null); - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(() => { - if (typeof window === "undefined") return 20; - try { - const stored = localStorage.getItem("Org-alerting-rules-table-size"); - if (stored) { - const parsed = parseInt(stored, 10); - if (parsed > 0 && parsed <= 1000) return parsed; - } - } catch {} - return 20; - }); + + const page = Math.max(1, Number(searchParams.get("page") ?? 1)); + const pageSize = Math.max(1, Number(searchParams.get("pageSize") ?? 20)); + const pageIndex = page - 1; + const query = searchParams.get("query") ?? undefined; const { data, isLoading, refetch, 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 total = data?.pagination.total ?? 0; @@ -135,10 +136,21 @@ export default function AlertingRulesTable({ orgId }: AlertingRulesTableProps) { const paginationState: DataTablePaginationState = { pageIndex, pageSize, pageCount }; const handlePaginationChange = (newState: PaginationState) => { - setPageIndex(newState.pageIndex); - setPageSize(newState.pageSize); + searchParams.set("page", (newState.pageIndex + 1).toString()); + 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 = () => queryClient.invalidateQueries({ queryKey: ["ORG", orgId, "ALERT_RULES"] }); @@ -308,15 +320,16 @@ export default function AlertingRulesTable({ orgId }: AlertingRulesTableProps) { { router.push(`/${orgId}/settings/alerting/create`); }} onRefresh={() => refetch()} - isRefreshing={isRefetching || isLoading} + isRefreshing={isRefetching || isLoading || isFiltering} addButtonText={t("alertingAddRule")} enableColumnVisibility stickyLeftColumn="name" diff --git a/src/components/HealthChecksTable.tsx b/src/components/HealthChecksTable.tsx index a63585532..e0e57ff09 100644 --- a/src/components/HealthChecksTable.tsx +++ b/src/components/HealthChecksTable.tsx @@ -26,6 +26,8 @@ import { useTranslations } from "next-intl"; import { useState } from "react"; import type { PaginationState } from "@tanstack/react-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 { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; @@ -74,23 +76,20 @@ export default function HealthChecksTable({ const isPaid = isPaidUser(tierMatrix.standaloneHealthChecks); const [credenzaOpen, setCredenzaOpen] = useState(false); + const { + navigate: filter, + isNavigating: isFiltering, + searchParams + } = useNavigationContext(); + const [deleteOpen, setDeleteOpen] = useState(false); const [selected, setSelected] = useState(null); const [togglingId, setTogglingId] = useState(null); - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(() => { - if (typeof window === "undefined") return 20; - try { - const stored = localStorage.getItem( - "Org-standalone-health-checks-table-size" - ); - if (stored) { - const parsed = parseInt(stored, 10); - if (parsed > 0 && parsed <= 1000) return parsed; - } - } catch {} - return 20; - }); + + const page = Math.max(1, Number(searchParams.get("page") ?? 1)); + const pageSize = Math.max(1, Number(searchParams.get("pageSize") ?? 20)); + const pageIndex = page - 1; + const query = searchParams.get("query") ?? undefined; const { data, @@ -101,7 +100,8 @@ export default function HealthChecksTable({ ...orgQueries.standaloneHealthChecks({ orgId, limit: pageSize, - offset: pageIndex * pageSize + offset: pageIndex * pageSize, + query }), refetchInterval: 10_000 }); @@ -117,10 +117,21 @@ export default function HealthChecksTable({ }; const handlePaginationChange = (newState: PaginationState) => { - setPageIndex(newState.pageIndex); - setPageSize(newState.pageSize); + searchParams.set("page", (newState.pageIndex + 1).toString()); + 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 = () => queryClient.invalidateQueries({ queryKey: ["ORG", orgId, "STANDALONE_HEALTH_CHECKS"] @@ -376,17 +387,18 @@ export default function HealthChecksTable({ { setSelected(null); setCredenzaOpen(true); }} addButtonDisabled={!isPaid} onRefresh={() => refetch()} - isRefreshing={isRefetching || isLoading} + isRefreshing={isRefetching || isLoading || isFiltering} addButtonText={t("standaloneHcAddButton")} enableColumnVisibility stickyLeftColumn="name" diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 45e62b515..228c37540 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -259,18 +259,24 @@ export const orgQueries = { alertRules: ({ orgId, limit = 20, - offset = 0 + offset = 0, + query }: { orgId: string; limit?: number; offset?: number; + query?: string; }) => queryOptions({ - queryKey: ["ORG", orgId, "ALERT_RULES", { limit, offset }] as const, + queryKey: ["ORG", orgId, "ALERT_RULES", { limit, offset, query }] as const, 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< AxiosResponse - >(`/org/${orgId}/alert-rules?limit=${limit}&offset=${offset}`, { signal }); + >(`/org/${orgId}/alert-rules?${sp.toString()}`, { signal }); return { alertRules: res.data.data.alertRules, pagination: res.data.data.pagination @@ -303,15 +309,21 @@ export const orgQueries = { standaloneHealthChecks: ({ orgId, limit = 20, - offset = 0 + offset = 0, + query }: { orgId: string; limit?: number; offset?: number; + query?: string; }) => 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 }) => { + 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< AxiosResponse<{ healthChecks: { @@ -344,7 +356,7 @@ export const orgQueries = { offset: number; }; }> - >(`/org/${orgId}/health-checks?limit=${limit}&offset=${offset}`, { signal }); + >(`/org/${orgId}/health-checks?${sp.toString()}`, { signal }); return { healthChecks: res.data.data.healthChecks, pagination: res.data.data.pagination