mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-28 11:43:03 +00:00
add server filters to health check table
This commit is contained in:
@@ -1499,6 +1499,22 @@
|
|||||||
"standaloneHcColumnHealth": "Health",
|
"standaloneHcColumnHealth": "Health",
|
||||||
"standaloneHcColumnMode": "Mode",
|
"standaloneHcColumnMode": "Mode",
|
||||||
"standaloneHcColumnTarget": "Target",
|
"standaloneHcColumnTarget": "Target",
|
||||||
|
"standaloneHcHealthStateHealthy": "Healthy",
|
||||||
|
"standaloneHcHealthStateUnhealthy": "Unhealthy",
|
||||||
|
"standaloneHcHealthStateUnknown": "Unknown",
|
||||||
|
"standaloneHcFilterAnySite": "All sites",
|
||||||
|
"standaloneHcFilterAnyResource": "All resources",
|
||||||
|
"standaloneHcFilterMode": "Mode",
|
||||||
|
"standaloneHcFilterModeHttp": "HTTP",
|
||||||
|
"standaloneHcFilterModeTcp": "TCP",
|
||||||
|
"standaloneHcFilterModeSnmp": "SNMP",
|
||||||
|
"standaloneHcFilterModePing": "Ping",
|
||||||
|
"standaloneHcFilterHealth": "Health",
|
||||||
|
"standaloneHcFilterEnabled": "Enabled",
|
||||||
|
"standaloneHcFilterEnabledOn": "Enabled",
|
||||||
|
"standaloneHcFilterEnabledOff": "Disabled",
|
||||||
|
"standaloneHcFilterSiteIdFallback": "Site {id}",
|
||||||
|
"standaloneHcFilterResourceIdFallback": "Resource {id}",
|
||||||
"blueprints": "Blueprints",
|
"blueprints": "Blueprints",
|
||||||
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
||||||
"blueprintAdd": "Add Blueprint",
|
"blueprintAdd": "Add Blueprint",
|
||||||
|
|||||||
@@ -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, isNotNull, like, sql } from "drizzle-orm";
|
import { and, eq, exists, isNotNull, 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";
|
||||||
@@ -40,7 +40,23 @@ const querySchema = z.object({
|
|||||||
.default("0")
|
.default("0")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.int().nonnegative()),
|
.pipe(z.int().nonnegative()),
|
||||||
query: z.string().optional()
|
query: z.string().optional(),
|
||||||
|
hcMode: z.enum(["http", "tcp", "snmp", "ping"]).optional(),
|
||||||
|
siteId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((s) => (s == null || s === "" ? undefined : Number(s)))
|
||||||
|
.pipe(z.union([z.undefined(), z.number().int().positive()])),
|
||||||
|
resourceId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((s) => (s == null || s === "" ? undefined : Number(s)))
|
||||||
|
.pipe(z.union([z.undefined(), z.number().int().positive()])),
|
||||||
|
hcHealth: z.enum(["healthy", "unhealthy", "unknown"]).optional(),
|
||||||
|
hcEnabled: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.optional()
|
||||||
|
.transform((v) => (v === undefined ? undefined : v === "true"))
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
@@ -81,7 +97,30 @@ export async function listHealthChecks(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { limit, offset, query } = parsedQuery.data;
|
const {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
query,
|
||||||
|
hcMode,
|
||||||
|
siteId,
|
||||||
|
resourceId,
|
||||||
|
hcHealth,
|
||||||
|
hcEnabled
|
||||||
|
} = parsedQuery.data;
|
||||||
|
|
||||||
|
const resourceIdFilter = resourceId
|
||||||
|
? exists(
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(targets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(targets.targetId, targetHealthCheck.targetId),
|
||||||
|
eq(targets.resourceId, resourceId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const whereClause = and(
|
const whereClause = and(
|
||||||
eq(targetHealthCheck.orgId, orgId),
|
eq(targetHealthCheck.orgId, orgId),
|
||||||
@@ -91,6 +130,13 @@ export async function listHealthChecks(
|
|||||||
sql`LOWER(${targetHealthCheck.name})`,
|
sql`LOWER(${targetHealthCheck.name})`,
|
||||||
`%${query.toLowerCase()}%`
|
`%${query.toLowerCase()}%`
|
||||||
)
|
)
|
||||||
|
: undefined,
|
||||||
|
hcMode ? eq(targetHealthCheck.hcMode, hcMode) : undefined,
|
||||||
|
siteId ? eq(targetHealthCheck.siteId, siteId) : undefined,
|
||||||
|
resourceIdFilter,
|
||||||
|
hcHealth ? eq(targetHealthCheck.hcHealth, hcHealth) : undefined,
|
||||||
|
hcEnabled !== undefined
|
||||||
|
? eq(targetHealthCheck.hcEnabled, hcEnabled)
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import DismissableBanner from "@app/components/DismissableBanner";
|
|||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
|
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
|
||||||
import { AxiosResponse } from "axios";
|
import { GetResourceResponse } from "@server/routers/resource/getResource";
|
||||||
|
import { GetSiteResponse } from "@server/routers/site/getSite";
|
||||||
|
import type ResponseT from "@server/types/Response";
|
||||||
import { HeartPulse } from "lucide-react";
|
import { HeartPulse } from "lucide-react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
@@ -26,24 +28,70 @@ function parsePositiveInt(s: string | undefined): number | undefined {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendListFilters(
|
||||||
|
apiSp: URLSearchParams,
|
||||||
|
searchParams: URLSearchParams
|
||||||
|
) {
|
||||||
|
const query = searchParams.get("query");
|
||||||
|
if (query) apiSp.set("query", query);
|
||||||
|
|
||||||
|
const hcMode = searchParams.get("hcMode");
|
||||||
|
if (
|
||||||
|
hcMode === "http" ||
|
||||||
|
hcMode === "tcp" ||
|
||||||
|
hcMode === "snmp" ||
|
||||||
|
hcMode === "ping"
|
||||||
|
) {
|
||||||
|
apiSp.set("hcMode", hcMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hcHealth = searchParams.get("hcHealth");
|
||||||
|
if (
|
||||||
|
hcHealth === "healthy" ||
|
||||||
|
hcHealth === "unhealthy" ||
|
||||||
|
hcHealth === "unknown"
|
||||||
|
) {
|
||||||
|
apiSp.set("hcHealth", hcHealth);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hcEnabled = searchParams.get("hcEnabled");
|
||||||
|
if (hcEnabled === "true" || hcEnabled === "false") {
|
||||||
|
apiSp.set("hcEnabled", hcEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteId = parsePositiveInt(searchParams.get("siteId") ?? undefined);
|
||||||
|
if (siteId) {
|
||||||
|
apiSp.set("siteId", String(siteId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceId = parsePositiveInt(
|
||||||
|
searchParams.get("resourceId") ?? undefined
|
||||||
|
);
|
||||||
|
if (resourceId) {
|
||||||
|
apiSp.set("resourceId", String(resourceId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default async function AlertingHealthChecksPage(
|
export default async function AlertingHealthChecksPage(
|
||||||
props: AlertingHealthChecksPageProps
|
props: AlertingHealthChecksPageProps
|
||||||
) {
|
) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const searchParams = new URLSearchParams(await props.searchParams);
|
const searchParams = new URLSearchParams(await props.searchParams);
|
||||||
|
|
||||||
const page = Math.max(1, parsePositiveInt(searchParams.get("page") ?? undefined) ?? 1);
|
const page = Math.max(
|
||||||
|
1,
|
||||||
|
parsePositiveInt(searchParams.get("page") ?? undefined) ?? 1
|
||||||
|
);
|
||||||
const pageSize = Math.max(
|
const pageSize = Math.max(
|
||||||
1,
|
1,
|
||||||
parsePositiveInt(searchParams.get("pageSize") ?? undefined) ?? 20
|
parsePositiveInt(searchParams.get("pageSize") ?? undefined) ?? 20
|
||||||
);
|
);
|
||||||
const pageIndex = page - 1;
|
const pageIndex = page - 1;
|
||||||
const query = searchParams.get("query") ?? undefined;
|
|
||||||
|
|
||||||
const apiSp = new URLSearchParams();
|
const apiSp = new URLSearchParams();
|
||||||
apiSp.set("limit", String(pageSize));
|
apiSp.set("limit", String(pageSize));
|
||||||
apiSp.set("offset", String(pageIndex * pageSize));
|
apiSp.set("offset", String(pageIndex * pageSize));
|
||||||
if (query) apiSp.set("query", query);
|
appendListFilters(apiSp, searchParams);
|
||||||
|
|
||||||
let healthChecks: ListHealthChecksResponse["healthChecks"] = [];
|
let healthChecks: ListHealthChecksResponse["healthChecks"] = [];
|
||||||
let pagination: ListHealthChecksResponse["pagination"] = {
|
let pagination: ListHealthChecksResponse["pagination"] = {
|
||||||
@@ -51,18 +99,80 @@ export default async function AlertingHealthChecksPage(
|
|||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: pageIndex * pageSize
|
offset: pageIndex * pageSize
|
||||||
};
|
};
|
||||||
try {
|
|
||||||
const res = await internal.get<AxiosResponse<ListHealthChecksResponse>>(
|
const siteIdParam = parsePositiveInt(
|
||||||
`/org/${params.orgId}/health-checks?${apiSp.toString()}`,
|
searchParams.get("siteId") ?? undefined
|
||||||
await authCookieHeader()
|
|
||||||
);
|
);
|
||||||
const responseData = res.data.data;
|
const resourceIdParam = parsePositiveInt(
|
||||||
|
searchParams.get("resourceId") ?? undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const header = await authCookieHeader();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await internal.get(
|
||||||
|
`/org/${params.orgId}/health-checks?${apiSp.toString()}`,
|
||||||
|
header
|
||||||
|
);
|
||||||
|
const responseData = (res.data as ResponseT<ListHealthChecksResponse>)
|
||||||
|
.data;
|
||||||
|
if (responseData) {
|
||||||
healthChecks = responseData.healthChecks;
|
healthChecks = responseData.healthChecks;
|
||||||
pagination = responseData.pagination;
|
pagination = responseData.pagination;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// leave defaults
|
// leave defaults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initialFilterSite: {
|
||||||
|
siteId: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
} | null = null;
|
||||||
|
if (siteIdParam) {
|
||||||
|
try {
|
||||||
|
const siteRes = await internal.get(`/site/${siteIdParam}`, header);
|
||||||
|
const s = (siteRes.data as ResponseT<GetSiteResponse>).data;
|
||||||
|
if (s && s.orgId === params.orgId) {
|
||||||
|
initialFilterSite = {
|
||||||
|
siteId: s.siteId,
|
||||||
|
name: s.name,
|
||||||
|
type: s.type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// leave null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialFilterResource: {
|
||||||
|
name: string;
|
||||||
|
resourceId: number;
|
||||||
|
fullDomain: string | null;
|
||||||
|
niceId: string;
|
||||||
|
ssl: boolean;
|
||||||
|
} | null = null;
|
||||||
|
if (resourceIdParam) {
|
||||||
|
try {
|
||||||
|
const resourceRes = await internal.get(
|
||||||
|
`/resource/${resourceIdParam}`,
|
||||||
|
header
|
||||||
|
);
|
||||||
|
const r = (resourceRes.data as ResponseT<GetResourceResponse>).data;
|
||||||
|
if (r && r.orgId === params.orgId) {
|
||||||
|
initialFilterResource = {
|
||||||
|
name: r.name,
|
||||||
|
resourceId: r.resourceId,
|
||||||
|
fullDomain: r.fullDomain,
|
||||||
|
niceId: r.niceId,
|
||||||
|
ssl: r.ssl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// leave null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,6 +190,12 @@ export default async function AlertingHealthChecksPage(
|
|||||||
orgId={params.orgId}
|
orgId={params.orgId}
|
||||||
healthChecks={healthChecks}
|
healthChecks={healthChecks}
|
||||||
rowCount={pagination.total}
|
rowCount={pagination.total}
|
||||||
|
pagination={{
|
||||||
|
pageIndex,
|
||||||
|
pageSize
|
||||||
|
}}
|
||||||
|
initialFilterSite={initialFilterSite}
|
||||||
|
initialFilterResource={initialFilterResource}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,23 +6,42 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
|||||||
import HealthCheckCredenza, {
|
import HealthCheckCredenza, {
|
||||||
HealthCheckRow
|
HealthCheckRow
|
||||||
} from "@app/components/HealthCheckCredenza";
|
} from "@app/components/HealthCheckCredenza";
|
||||||
|
import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
|
||||||
|
import { Badge } from "@app/components/ui/badge";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { DataTable, ExtendedColumnDef } from "@app/components/ui/data-table";
|
import {
|
||||||
|
ControlledDataTable,
|
||||||
|
type ExtendedColumnDef
|
||||||
|
} from "@app/components/ui/controlled-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 {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from "@app/components/ui/popover";
|
||||||
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 { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { ArrowUpDown, ArrowUpRight, MoreHorizontal } from "lucide-react";
|
import { Selectedsite, SitesSelector } from "@app/components/site-selector";
|
||||||
|
import {
|
||||||
|
ResourceSelector,
|
||||||
|
SelectedResource
|
||||||
|
} from "@app/components/resource-selector";
|
||||||
|
import {
|
||||||
|
ArrowUpDown,
|
||||||
|
ArrowUpRight,
|
||||||
|
Funnel,
|
||||||
|
MoreHorizontal
|
||||||
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState, useTransition, useEffect } from "react";
|
import { useState, useTransition, useEffect, useMemo } 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 { useNavigationContext } from "@app/hooks/useNavigationContext";
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -30,11 +49,15 @@ import { useRouter } from "next/navigation";
|
|||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
type StandaloneHealthChecksTableProps = {
|
type StandaloneHealthChecksTableProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
healthChecks: HealthCheckRow[];
|
healthChecks: HealthCheckRow[];
|
||||||
rowCount: number;
|
rowCount: number;
|
||||||
|
pagination: PaginationState;
|
||||||
|
initialFilterSite?: Selectedsite | null;
|
||||||
|
initialFilterResource?: SelectedResource | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatTarget(row: HealthCheckRow): string {
|
function formatTarget(row: HealthCheckRow): string {
|
||||||
@@ -43,6 +66,12 @@ function formatTarget(row: HealthCheckRow): string {
|
|||||||
if (!row.hcPort) return row.hcHostname;
|
if (!row.hcPort) return row.hcHostname;
|
||||||
return `${row.hcHostname}:${row.hcPort}`;
|
return `${row.hcHostname}:${row.hcPort}`;
|
||||||
}
|
}
|
||||||
|
if (row.hcMode === "snmp" || row.hcMode === "ping") {
|
||||||
|
if (row.hcPort) {
|
||||||
|
return `${row.hcHostname}:${row.hcPort}`;
|
||||||
|
}
|
||||||
|
return row.hcHostname;
|
||||||
|
}
|
||||||
// HTTP / default
|
// HTTP / default
|
||||||
const scheme = row.hcScheme ?? "http";
|
const scheme = row.hcScheme ?? "http";
|
||||||
const host = row.hcHostname;
|
const host = row.hcHostname;
|
||||||
@@ -51,16 +80,13 @@ function formatTarget(row: HealthCheckRow): string {
|
|||||||
return `${scheme}://${host}${port}${path}`;
|
return `${scheme}://${host}${port}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const healthLabel: Record<HealthCheckRow["hcHealth"], string> = {
|
|
||||||
healthy: "Healthy",
|
|
||||||
unhealthy: "Unhealthy",
|
|
||||||
unknown: "Unknown"
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function HealthChecksTable({
|
export default function HealthChecksTable({
|
||||||
orgId,
|
orgId,
|
||||||
healthChecks,
|
healthChecks,
|
||||||
rowCount
|
rowCount,
|
||||||
|
pagination,
|
||||||
|
initialFilterSite = null,
|
||||||
|
initialFilterResource = null
|
||||||
}: StandaloneHealthChecksTableProps) {
|
}: StandaloneHealthChecksTableProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -79,15 +105,56 @@ export default function HealthChecksTable({
|
|||||||
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 [siteFilterOpen, setSiteFilterOpen] = useState(false);
|
||||||
|
const [resourceFilterOpen, setResourceFilterOpen] = useState(false);
|
||||||
|
|
||||||
const page = Math.max(1, Number(searchParams.get("page") ?? 1));
|
const pageSize = pagination.pageSize;
|
||||||
const pageSize = Math.max(1, Number(searchParams.get("pageSize") ?? 20));
|
|
||||||
const pageIndex = page - 1;
|
|
||||||
const query = searchParams.get("query") ?? undefined;
|
const query = searchParams.get("query") ?? undefined;
|
||||||
|
|
||||||
|
const siteIdQ = searchParams.get("siteId");
|
||||||
|
const siteIdNum = siteIdQ ? parseInt(siteIdQ, 10) : NaN;
|
||||||
|
const selectedSite: Selectedsite | null = useMemo(() => {
|
||||||
|
if (!siteIdQ || !Number.isInteger(siteIdNum) || siteIdNum <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (initialFilterSite && initialFilterSite.siteId === siteIdNum) {
|
||||||
|
return initialFilterSite;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
siteId: siteIdNum,
|
||||||
|
name: t("standaloneHcFilterSiteIdFallback", { id: siteIdNum }),
|
||||||
|
type: "newt"
|
||||||
|
};
|
||||||
|
}, [initialFilterSite, siteIdQ, siteIdNum, t]);
|
||||||
|
|
||||||
|
const resourceIdQ = searchParams.get("resourceId");
|
||||||
|
const resourceIdNum = resourceIdQ ? parseInt(resourceIdQ, 10) : NaN;
|
||||||
|
const selectedResource: SelectedResource | null = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!resourceIdQ ||
|
||||||
|
!Number.isInteger(resourceIdNum) ||
|
||||||
|
resourceIdNum <= 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
initialFilterResource &&
|
||||||
|
initialFilterResource.resourceId === resourceIdNum
|
||||||
|
) {
|
||||||
|
return initialFilterResource;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: t("standaloneHcFilterResourceIdFallback", {
|
||||||
|
id: resourceIdNum
|
||||||
|
}),
|
||||||
|
resourceId: resourceIdNum,
|
||||||
|
fullDomain: null,
|
||||||
|
niceId: "",
|
||||||
|
ssl: false
|
||||||
|
};
|
||||||
|
}, [initialFilterResource, resourceIdQ, resourceIdNum, t]);
|
||||||
|
|
||||||
const rows = healthChecks;
|
const rows = healthChecks;
|
||||||
const total = rowCount;
|
|
||||||
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
||||||
|
|
||||||
function refreshList() {
|
function refreshList() {
|
||||||
startRefresh(() => {
|
startRefresh(() => {
|
||||||
@@ -102,12 +169,6 @@ export default function HealthChecksTable({
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const paginationState: DataTablePaginationState = {
|
|
||||||
pageIndex,
|
|
||||||
pageSize,
|
|
||||||
pageCount
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePaginationChange = (newState: PaginationState) => {
|
const handlePaginationChange = (newState: PaginationState) => {
|
||||||
searchParams.set("page", (newState.pageIndex + 1).toString());
|
searchParams.set("page", (newState.pageIndex + 1).toString());
|
||||||
searchParams.set("pageSize", newState.pageSize.toString());
|
searchParams.set("pageSize", newState.pageSize.toString());
|
||||||
@@ -124,6 +185,39 @@ export default function HealthChecksTable({
|
|||||||
filter({ searchParams });
|
filter({ searchParams });
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
function handleFilterChange(
|
||||||
|
column: string,
|
||||||
|
value: string | undefined | null
|
||||||
|
) {
|
||||||
|
const sp = new URLSearchParams(searchParams);
|
||||||
|
sp.delete(column);
|
||||||
|
sp.delete("page");
|
||||||
|
if (value) {
|
||||||
|
sp.set(column, value);
|
||||||
|
}
|
||||||
|
filter({ searchParams: sp });
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSiteFilter = () => {
|
||||||
|
handleFilterChange("siteId", undefined);
|
||||||
|
setSiteFilterOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearResourceFilter = () => {
|
||||||
|
handleFilterChange("resourceId", undefined);
|
||||||
|
setResourceFilterOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPickSite = (site: Selectedsite) => {
|
||||||
|
handleFilterChange("siteId", String(site.siteId));
|
||||||
|
setSiteFilterOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPickResource = (resource: SelectedResource) => {
|
||||||
|
handleFilterChange("resourceId", String(resource.resourceId));
|
||||||
|
setResourceFilterOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggleEnabled = async (
|
const handleToggleEnabled = async (
|
||||||
row: HealthCheckRow,
|
row: HealthCheckRow,
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@@ -166,6 +260,27 @@ export default function HealthChecksTable({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const modeParam = searchParams.get("hcMode");
|
||||||
|
const selectedHcMode =
|
||||||
|
modeParam === "http" ||
|
||||||
|
modeParam === "tcp" ||
|
||||||
|
modeParam === "snmp" ||
|
||||||
|
modeParam === "ping"
|
||||||
|
? modeParam
|
||||||
|
: undefined;
|
||||||
|
const healthParam = searchParams.get("hcHealth");
|
||||||
|
const selectedHcHealth =
|
||||||
|
healthParam === "healthy" ||
|
||||||
|
healthParam === "unhealthy" ||
|
||||||
|
healthParam === "unknown"
|
||||||
|
? healthParam
|
||||||
|
: undefined;
|
||||||
|
const enabledParam = searchParams.get("hcEnabled");
|
||||||
|
const selectedHcEnabled =
|
||||||
|
enabledParam === "true" || enabledParam === "false"
|
||||||
|
? enabledParam
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const columns: ExtendedColumnDef<HealthCheckRow>[] = [
|
const columns: ExtendedColumnDef<HealthCheckRow>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
@@ -190,12 +305,34 @@ export default function HealthChecksTable({
|
|||||||
id: "mode",
|
id: "mode",
|
||||||
friendlyName: t("standaloneHcColumnMode"),
|
friendlyName: t("standaloneHcColumnMode"),
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="p-3">{t("standaloneHcColumnMode")}</span>
|
<ColumnFilterButton
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: "http",
|
||||||
|
label: t("standaloneHcFilterModeHttp")
|
||||||
|
},
|
||||||
|
{ value: "tcp", label: t("standaloneHcFilterModeTcp") },
|
||||||
|
{
|
||||||
|
value: "snmp",
|
||||||
|
label: t("standaloneHcFilterModeSnmp")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "ping",
|
||||||
|
label: t("standaloneHcFilterModePing")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
selectedValue={selectedHcMode}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleFilterChange("hcMode", value)
|
||||||
|
}
|
||||||
|
searchPlaceholder={t("searchPlaceholder")}
|
||||||
|
emptyMessage={t("emptySearchOptions")}
|
||||||
|
label={t("standaloneHcColumnMode")}
|
||||||
|
className="p-3"
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>
|
<span>{row.original.hcMode?.toUpperCase() ?? "-"}</span>
|
||||||
{row.original.hcMode?.toUpperCase() ?? "-"}
|
|
||||||
</span>
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -208,9 +345,58 @@ export default function HealthChecksTable({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "resource",
|
id: "resource",
|
||||||
friendlyName: "Resource",
|
friendlyName: t("resource"),
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="p-3">Resource</span>
|
<Popover
|
||||||
|
open={resourceFilterOpen}
|
||||||
|
onOpenChange={setResourceFilterOpen}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"justify-between text-sm h-8 px-2 w-full p-3",
|
||||||
|
!selectedResource && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
{t("resource")}
|
||||||
|
<Funnel className="size-4 flex-none" />
|
||||||
|
{selectedResource && (
|
||||||
|
<Badge
|
||||||
|
className="truncate max-w-[10rem]"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
{selectedResource.name}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-[min(20rem,var(--radix-popover-trigger-width))] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<div className="border-b p-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-full justify-start font-normal"
|
||||||
|
onClick={clearResourceFilter}
|
||||||
|
>
|
||||||
|
{t("standaloneHcFilterAnyResource")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ResourceSelector
|
||||||
|
orgId={orgId}
|
||||||
|
selectedResource={selectedResource}
|
||||||
|
onSelectResource={onPickResource}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
@@ -218,7 +404,9 @@ export default function HealthChecksTable({
|
|||||||
return <span className="text-neutral-400">-</span>;
|
return <span className="text-neutral-400">-</span>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Link href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}>
|
<Link
|
||||||
|
href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}
|
||||||
|
>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
{r.resourceName}
|
{r.resourceName}
|
||||||
<ArrowUpRight className="ml-2 h-3 w-3" />
|
<ArrowUpRight className="ml-2 h-3 w-3" />
|
||||||
@@ -229,9 +417,55 @@ export default function HealthChecksTable({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "site",
|
id: "site",
|
||||||
friendlyName: "Site",
|
friendlyName: t("site"),
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="p-3">Site</span>
|
<Popover open={siteFilterOpen} onOpenChange={setSiteFilterOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"justify-between text-sm h-8 px-2 w-full p-3",
|
||||||
|
!selectedSite && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
{t("site")}
|
||||||
|
<Funnel className="size-4 flex-none" />
|
||||||
|
{selectedSite && (
|
||||||
|
<Badge
|
||||||
|
className="truncate max-w-[10rem]"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
{selectedSite.name}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-[min(20rem,var(--radix-popover-trigger-width))] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<div className="border-b p-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-full justify-start font-normal"
|
||||||
|
onClick={clearSiteFilter}
|
||||||
|
>
|
||||||
|
{t("standaloneHcFilterAnySite")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<SitesSelector
|
||||||
|
orgId={orgId}
|
||||||
|
selectedSite={selectedSite}
|
||||||
|
onSelectSite={onPickSite}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
@@ -239,7 +473,9 @@ export default function HealthChecksTable({
|
|||||||
return <span className="text-neutral-400">-</span>;
|
return <span className="text-neutral-400">-</span>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Link href={`/${orgId}/settings/sites/${r.siteNiceId}/general`}>
|
<Link
|
||||||
|
href={`/${orgId}/settings/sites/${r.siteNiceId}/general`}
|
||||||
|
>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
{r.siteName}
|
{r.siteName}
|
||||||
<ArrowUpRight className="ml-2 h-3 w-3" />
|
<ArrowUpRight className="ml-2 h-3 w-3" />
|
||||||
@@ -252,29 +488,52 @@ export default function HealthChecksTable({
|
|||||||
id: "health",
|
id: "health",
|
||||||
friendlyName: t("standaloneHcColumnHealth"),
|
friendlyName: t("standaloneHcColumnHealth"),
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="p-3">{t("standaloneHcColumnHealth")}</span>
|
<ColumnFilterButton
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: "healthy",
|
||||||
|
label: t("standaloneHcHealthStateHealthy")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "unhealthy",
|
||||||
|
label: t("standaloneHcHealthStateUnhealthy")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "unknown",
|
||||||
|
label: t("standaloneHcHealthStateUnknown")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
selectedValue={selectedHcHealth}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleFilterChange("hcHealth", value)
|
||||||
|
}
|
||||||
|
searchPlaceholder={t("searchPlaceholder")}
|
||||||
|
emptyMessage={t("emptySearchOptions")}
|
||||||
|
label={t("standaloneHcColumnHealth")}
|
||||||
|
className="p-3"
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const health = row.original.hcHealth;
|
const health = row.original.hcHealth;
|
||||||
if (health === "healthy") {
|
if (health === "healthy") {
|
||||||
return (
|
return (
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
<span className="text-green-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full" />
|
||||||
<span>{healthLabel.healthy}</span>
|
<span>{t("standaloneHcHealthStateHealthy")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else if (health === "unhealthy") {
|
} else if (health === "unhealthy") {
|
||||||
return (
|
return (
|
||||||
<span className="text-red-500 flex items-center space-x-2">
|
<span className="text-red-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-red-500 rounded-full" />
|
||||||
<span>{healthLabel.unhealthy}</span>
|
<span>{t("standaloneHcHealthStateUnhealthy")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="text-neutral-500 flex items-center space-x-2">
|
<span className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-neutral-500 rounded-full" />
|
||||||
<span>{healthLabel.unknown}</span>
|
<span>{t("standaloneHcHealthStateUnknown")}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -282,11 +541,15 @@ export default function HealthChecksTable({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "uptime",
|
id: "uptime",
|
||||||
friendlyName: "Uptime",
|
friendlyName: t("uptime30d"),
|
||||||
header: () => <span className="p-3">{t("uptime30d")}</span>,
|
header: () => <span className="p-3">{t("uptime30d")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<UptimeMiniBar orgId={orgId} healthCheckId={row.original.targetHealthCheckId} days={30} />
|
<UptimeMiniBar
|
||||||
|
orgId={orgId}
|
||||||
|
healthCheckId={row.original.targetHealthCheckId}
|
||||||
|
days={30}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -294,7 +557,26 @@ export default function HealthChecksTable({
|
|||||||
accessorKey: "hcEnabled",
|
accessorKey: "hcEnabled",
|
||||||
friendlyName: t("alertingColumnEnabled"),
|
friendlyName: t("alertingColumnEnabled"),
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="p-3">{t("alertingColumnEnabled")}</span>
|
<ColumnFilterButton
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: "true",
|
||||||
|
label: t("standaloneHcFilterEnabledOn")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "false",
|
||||||
|
label: t("standaloneHcFilterEnabledOff")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
selectedValue={selectedHcEnabled}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleFilterChange("hcEnabled", value)
|
||||||
|
}
|
||||||
|
searchPlaceholder={t("searchPlaceholder")}
|
||||||
|
emptyMessage={t("emptySearchOptions")}
|
||||||
|
label={t("alertingColumnEnabled")}
|
||||||
|
className="p-3"
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
@@ -302,8 +584,7 @@ export default function HealthChecksTable({
|
|||||||
<Switch
|
<Switch
|
||||||
checked={r.hcEnabled}
|
checked={r.hcEnabled}
|
||||||
disabled={
|
disabled={
|
||||||
!isPaid ||
|
!isPaid || togglingId === r.targetHealthCheckId
|
||||||
togglingId === r.targetHealthCheckId
|
|
||||||
}
|
}
|
||||||
onCheckedChange={(v) => handleToggleEnabled(r, v)}
|
onCheckedChange={(v) => handleToggleEnabled(r, v)}
|
||||||
/>
|
/>
|
||||||
@@ -339,15 +620,13 @@ export default function HealthChecksTable({
|
|||||||
{t("delete")}
|
{t("delete")}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{r.resourceId && r.resourceName && r.resourceNiceId ? (
|
{r.resourceId && r.resourceName && r.resourceNiceId ? (
|
||||||
<Link href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}>
|
<Link
|
||||||
<Button
|
href={`/${orgId}/settings/resources/proxy/${r.resourceNiceId}`}
|
||||||
variant="outline"
|
|
||||||
disabled={!isPaid}
|
|
||||||
>
|
>
|
||||||
|
<Button variant="outline" disabled={!isPaid}>
|
||||||
{t("edit")}
|
{t("edit")}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -363,7 +642,6 @@ export default function HealthChecksTable({
|
|||||||
{t("edit")}
|
{t("edit")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -405,14 +683,13 @@ export default function HealthChecksTable({
|
|||||||
|
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.standaloneHealthChecks} />
|
<PaidFeaturesAlert tiers={tierMatrix.standaloneHealthChecks} />
|
||||||
|
|
||||||
<DataTable
|
<ControlledDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
rows={rows}
|
||||||
title={t("standaloneHcTableTitle")}
|
tableId="health-checks-table"
|
||||||
searchPlaceholder={t("standaloneHcSearchPlaceholder")}
|
searchPlaceholder={t("standaloneHcSearchPlaceholder")}
|
||||||
onSearch={handleSearchChange}
|
onSearch={handleSearchChange}
|
||||||
searchQuery={query}
|
searchQuery={query}
|
||||||
manualFiltering
|
|
||||||
onAdd={() => {
|
onAdd={() => {
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
setCredenzaOpen(true);
|
setCredenzaOpen(true);
|
||||||
@@ -424,8 +701,9 @@ export default function HealthChecksTable({
|
|||||||
enableColumnVisibility
|
enableColumnVisibility
|
||||||
stickyLeftColumn="name"
|
stickyLeftColumn="name"
|
||||||
stickyRightColumn="rowActions"
|
stickyRightColumn="rowActions"
|
||||||
pagination={paginationState}
|
pagination={pagination}
|
||||||
onPaginationChange={handlePaginationChange}
|
onPaginationChange={handlePaginationChange}
|
||||||
|
rowCount={rowCount}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
Search
|
Search
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState, type ReactNode } from "react";
|
||||||
|
|
||||||
// Extended ColumnDef type that includes optional friendlyName for column visibility dropdown
|
// Extended ColumnDef type that includes optional friendlyName for column visibility dropdown
|
||||||
export type ExtendedColumnDef<TData, TValue = unknown> = ColumnDef<
|
export type ExtendedColumnDef<TData, TValue = unknown> = ColumnDef<
|
||||||
@@ -84,6 +84,8 @@ type ControlledDataTableProps<TData, TValue> = {
|
|||||||
isNavigatingToAddPage?: boolean;
|
isNavigatingToAddPage?: boolean;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
filters?: DataTableFilter[];
|
filters?: DataTableFilter[];
|
||||||
|
/** Extra filter controls (e.g. searchable entity pickers) shown after the filter dropdowns. */
|
||||||
|
filterExtras?: ReactNode;
|
||||||
filterDisplayMode?: "label" | "calculated"; // Global filter display mode (can be overridden per filter)
|
filterDisplayMode?: "label" | "calculated"; // Global filter display mode (can be overridden per filter)
|
||||||
columnVisibility?: Record<string, boolean>;
|
columnVisibility?: Record<string, boolean>;
|
||||||
enableColumnVisibility?: boolean;
|
enableColumnVisibility?: boolean;
|
||||||
@@ -108,6 +110,7 @@ export function ControlledDataTable<TData, TValue>({
|
|||||||
refreshButtonDisabled = false,
|
refreshButtonDisabled = false,
|
||||||
searchPlaceholder = "Search...",
|
searchPlaceholder = "Search...",
|
||||||
filters,
|
filters,
|
||||||
|
filterExtras,
|
||||||
filterDisplayMode = "label",
|
filterDisplayMode = "label",
|
||||||
columnVisibility: defaultColumnVisibility,
|
columnVisibility: defaultColumnVisibility,
|
||||||
enableColumnVisibility = false,
|
enableColumnVisibility = false,
|
||||||
@@ -343,6 +346,7 @@ export function ControlledDataTable<TData, TValue>({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{filterExtras}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 sm:justify-end">
|
<div className="flex items-center gap-2 sm:justify-end">
|
||||||
{onRefresh && (
|
{onRefresh && (
|
||||||
@@ -350,7 +354,9 @@ export function ControlledDataTable<TData, TValue>({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
disabled={isRefreshing || refreshButtonDisabled}
|
disabled={
|
||||||
|
isRefreshing || refreshButtonDisabled
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||||
@@ -361,7 +367,9 @@ export function ControlledDataTable<TData, TValue>({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{addActions && addActions.length > 0 && addButtonText ? (
|
{addActions &&
|
||||||
|
addActions.length > 0 &&
|
||||||
|
addButtonText ? (
|
||||||
<div>
|
<div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
@@ -361,25 +361,50 @@ export const orgQueries = {
|
|||||||
orgId,
|
orgId,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
query
|
query,
|
||||||
|
hcMode,
|
||||||
|
siteId,
|
||||||
|
resourceId,
|
||||||
|
hcHealth,
|
||||||
|
hcEnabled
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
hcMode?: "http" | "tcp" | "snmp" | "ping";
|
||||||
|
siteId?: number;
|
||||||
|
resourceId?: number;
|
||||||
|
hcHealth?: "healthy" | "unhealthy" | "unknown";
|
||||||
|
hcEnabled?: "true" | "false";
|
||||||
}) =>
|
}) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
"ORG",
|
"ORG",
|
||||||
orgId,
|
orgId,
|
||||||
"STANDALONE_HEALTH_CHECKS",
|
"STANDALONE_HEALTH_CHECKS",
|
||||||
{ limit, offset, query }
|
{
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
query,
|
||||||
|
hcMode,
|
||||||
|
siteId,
|
||||||
|
resourceId,
|
||||||
|
hcHealth,
|
||||||
|
hcEnabled
|
||||||
|
}
|
||||||
] as const,
|
] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
const sp = new URLSearchParams();
|
const sp = new URLSearchParams();
|
||||||
sp.set("limit", String(limit));
|
sp.set("limit", String(limit));
|
||||||
sp.set("offset", String(offset));
|
sp.set("offset", String(offset));
|
||||||
if (query) sp.set("query", query);
|
if (query) sp.set("query", query);
|
||||||
|
if (hcMode) sp.set("hcMode", hcMode);
|
||||||
|
if (siteId != null) sp.set("siteId", String(siteId));
|
||||||
|
if (resourceId != null)
|
||||||
|
sp.set("resourceId", String(resourceId));
|
||||||
|
if (hcHealth) sp.set("hcHealth", hcHealth);
|
||||||
|
if (hcEnabled) sp.set("hcEnabled", hcEnabled);
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<{
|
AxiosResponse<{
|
||||||
healthChecks: {
|
healthChecks: {
|
||||||
|
|||||||
Reference in New Issue
Block a user