mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-30 12:42:22 +00:00
More refreshing and status history displays
This commit is contained in:
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"contactSalesEnable": "Contact sales to enable this feature.",
|
||||||
|
"contactSalesBookDemo": "Book a demo",
|
||||||
|
"contactSalesOr": "or",
|
||||||
|
"contactSalesContactUs": "contact us",
|
||||||
"setupCreate": "Create the organization, site, and resources",
|
"setupCreate": "Create the organization, site, and resources",
|
||||||
"headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.",
|
"headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.",
|
||||||
"headerAuthCompatibility": "Extended compatibility",
|
"headerAuthCompatibility": "Extended compatibility",
|
||||||
@@ -3047,5 +3051,6 @@
|
|||||||
"healthCheckTabStrategy": "Strategy",
|
"healthCheckTabStrategy": "Strategy",
|
||||||
"healthCheckTabConnection": "Connection",
|
"healthCheckTabConnection": "Connection",
|
||||||
"healthCheckTabAdvanced": "Advanced",
|
"healthCheckTabAdvanced": "Advanced",
|
||||||
"healthCheckStrategyNotAvailable": "This strategy is not available. Please contact sales to enable this feature."
|
"healthCheckStrategyNotAvailable": "This strategy is not available. Please contact sales to enable this feature.",
|
||||||
|
"uptime30d": "Uptime (30d)"
|
||||||
}
|
}
|
||||||
|
|||||||
91
server/private/lib/alerts/events/resourceEvents.ts
Normal file
91
server/private/lib/alerts/events/resourceEvents.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { processAlerts } from "../processAlerts";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public API
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire a `health_check_healthy` alert for the given health check.
|
||||||
|
*
|
||||||
|
* Call this after a previously-failing health check has recovered so that any
|
||||||
|
* matching `alertRules` can dispatch their email and webhook actions.
|
||||||
|
*
|
||||||
|
* @param orgId - Organisation that owns the health check.
|
||||||
|
* @param healthCheckId - Numeric primary key of the health check.
|
||||||
|
* @param healthCheckName - Human-readable name shown in notifications (optional).
|
||||||
|
* @param extra - Any additional key/value pairs to include in the payload.
|
||||||
|
*/
|
||||||
|
export async function fireHealthCheckHealthyAlert(
|
||||||
|
orgId: string,
|
||||||
|
healthCheckId: number,
|
||||||
|
healthCheckName?: string | null,
|
||||||
|
extra?: Record<string, unknown>
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await processAlerts({
|
||||||
|
eventType: "health_check_healthy",
|
||||||
|
orgId,
|
||||||
|
healthCheckId,
|
||||||
|
data: {
|
||||||
|
healthCheckId,
|
||||||
|
...(healthCheckName != null ? { healthCheckName } : {}),
|
||||||
|
...extra
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`fireHealthCheckHealthyAlert: unexpected error for healthCheckId ${healthCheckId}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire a `health_check_not_healthy` alert for the given health check.
|
||||||
|
*
|
||||||
|
* Call this after a health check has been detected as failing so that any
|
||||||
|
* matching `alertRules` can dispatch their email and webhook actions.
|
||||||
|
*
|
||||||
|
* @param orgId - Organisation that owns the health check.
|
||||||
|
* @param healthCheckId - Numeric primary key of the health check.
|
||||||
|
* @param healthCheckName - Human-readable name shown in notifications (optional).
|
||||||
|
* @param extra - Any additional key/value pairs to include in the payload.
|
||||||
|
*/
|
||||||
|
export async function fireHealthCheckNotHealthyAlert(
|
||||||
|
orgId: string,
|
||||||
|
healthCheckId: number,
|
||||||
|
healthCheckName?: string | null,
|
||||||
|
extra?: Record<string, unknown>
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await processAlerts({
|
||||||
|
eventType: "health_check_not_healthy",
|
||||||
|
orgId,
|
||||||
|
healthCheckId,
|
||||||
|
data: {
|
||||||
|
healthCheckId,
|
||||||
|
...(healthCheckName != null ? { healthCheckName } : {}),
|
||||||
|
...extra
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`fireHealthCheckNotHealthyAlert: unexpected error for healthCheckId ${healthCheckId}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -427,6 +427,13 @@ authenticated.get(
|
|||||||
resource.listResources
|
resource.listResources
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/resource/:resourceId/status-history",
|
||||||
|
verifyResourceAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.getResource),
|
||||||
|
resource.getResourceStatusHistory
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/resources",
|
"/org/:orgId/resources",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
|
|||||||
93
server/routers/resource/getStatusHistory.ts
Normal file
93
server/routers/resource/getStatusHistory.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db, statusHistory } from "@server/db";
|
||||||
|
import { and, eq, gte, asc } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import {
|
||||||
|
computeBuckets,
|
||||||
|
statusHistoryQuerySchema,
|
||||||
|
StatusHistoryResponse
|
||||||
|
} from "@server/lib/statusHistory";
|
||||||
|
|
||||||
|
const resourceParamsSchema = z.object({
|
||||||
|
resourceId: z.string().transform((v) => parseInt(v, 10))
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getResourceStatusHistory(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = resourceParamsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const parsedQuery = statusHistoryQuerySchema.safeParse(req.query);
|
||||||
|
if (!parsedQuery.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedQuery.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityType = "resource";
|
||||||
|
const entityId = parsedParams.data.resourceId;
|
||||||
|
const { days } = parsedQuery.data;
|
||||||
|
|
||||||
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
|
const startSec = nowSec - days * 86400;
|
||||||
|
|
||||||
|
const events = await db
|
||||||
|
.select()
|
||||||
|
.from(statusHistory)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(statusHistory.entityType, entityType),
|
||||||
|
eq(statusHistory.entityId, entityId),
|
||||||
|
gte(statusHistory.timestamp, startSec)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(asc(statusHistory.timestamp));
|
||||||
|
|
||||||
|
const { buckets, totalDowntime } = computeBuckets(events, days);
|
||||||
|
const totalWindow = days * 86400;
|
||||||
|
const overallUptime =
|
||||||
|
totalWindow > 0
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
((totalWindow - totalDowntime) / totalWindow) * 100
|
||||||
|
)
|
||||||
|
: 100;
|
||||||
|
|
||||||
|
return response<StatusHistoryResponse>(res, {
|
||||||
|
data: {
|
||||||
|
entityType,
|
||||||
|
entityId,
|
||||||
|
days: buckets,
|
||||||
|
overallUptimePercent: Math.round(overallUptime * 100) / 100,
|
||||||
|
totalDowntimeSeconds: totalDowntime
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Status history retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,3 +32,4 @@ export * from "./addUserToResource";
|
|||||||
export * from "./removeUserFromResource";
|
export * from "./removeUserFromResource";
|
||||||
export * from "./listAllResourceNames";
|
export * from "./listAllResourceNames";
|
||||||
export * from "./removeEmailFromResourceWhitelist";
|
export * from "./removeEmailFromResourceWhitelist";
|
||||||
|
export * from "./getStatusHistory";
|
||||||
|
|||||||
@@ -2,32 +2,35 @@
|
|||||||
|
|
||||||
import { KeyRound, ExternalLink } from "lucide-react";
|
import { KeyRound, ExternalLink } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export function ContactSalesBanner() {
|
export function ContactSalesBanner() {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden">
|
<div className="rounded-md border border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden">
|
||||||
<div className="py-3 px-4">
|
<div className="py-3 px-4">
|
||||||
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
||||||
<KeyRound className="size-4 shrink-0 text-black-500" />
|
<KeyRound className="size-4 shrink-0 text-black-500" />
|
||||||
<span>
|
<span>
|
||||||
Contact sales to enable this feature.{" "}
|
{t("contactSalesEnable")}{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://click.fossorial.io/ep922"
|
href="https://click.fossorial.io/ep922"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 font-medium text-black-600 underline"
|
className="inline-flex items-center gap-1 font-medium text-black-600 underline"
|
||||||
>
|
>
|
||||||
Book a demo
|
{t("contactSalesBookDemo")}
|
||||||
<ExternalLink className="size-3.5 shrink-0" />
|
<ExternalLink className="size-3.5 shrink-0" />
|
||||||
</Link>
|
</Link>
|
||||||
{" or "}
|
{" " + t("contactSalesOr") + " "}
|
||||||
<Link
|
<Link
|
||||||
href="https://pangolin.net/contact"
|
href="https://pangolin.net/contact"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 font-medium text-black-600 underline"
|
className="inline-flex items-center gap-1 font-medium text-black-600 underline"
|
||||||
>
|
>
|
||||||
contact us
|
{t("contactSalesContactUs")}
|
||||||
<ExternalLink className="size-3.5 shrink-0" />
|
<ExternalLink className="size-3.5 shrink-0" />
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export default function HealthChecksTable({
|
|||||||
{
|
{
|
||||||
id: "uptime",
|
id: "uptime",
|
||||||
friendlyName: "Uptime",
|
friendlyName: "Uptime",
|
||||||
header: () => <span className="p-3">Uptime (30d)</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} />
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { toast } from "@app/hooks/useToast";
|
|||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { UpdateResourceResponse } from "@server/routers/resource";
|
import { UpdateResourceResponse } from "@server/routers/resource";
|
||||||
import type { PaginationState } from "@tanstack/react-table";
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import {
|
import {
|
||||||
ArrowDown01Icon,
|
ArrowDown01Icon,
|
||||||
@@ -37,6 +38,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
|
useEffect,
|
||||||
useOptimistic,
|
useOptimistic,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@@ -47,6 +49,13 @@ import { useDebouncedCallback } from "use-debounce";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||||
import { ControlledDataTable } from "./ui/controlled-data-table";
|
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger
|
||||||
|
} from "@app/components/ui/tooltip";
|
||||||
|
import type { StatusHistoryResponse } from "@server/lib/statusHistory";
|
||||||
|
|
||||||
export type TargetHealth = {
|
export type TargetHealth = {
|
||||||
targetId: number;
|
targetId: number;
|
||||||
@@ -161,6 +170,13 @@ export default function ProxyResourcesTable({
|
|||||||
const [isRefreshing, startTransition] = useTransition();
|
const [isRefreshing, startTransition] = useTransition();
|
||||||
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
router.refresh();
|
||||||
|
}, 10_000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const refreshData = () => {
|
const refreshData = () => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
try {
|
try {
|
||||||
@@ -322,6 +338,82 @@ export default function ProxyResourcesTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ResourceStatusHistory({
|
||||||
|
resourceId,
|
||||||
|
api
|
||||||
|
}: {
|
||||||
|
resourceId: number;
|
||||||
|
api: ReturnType<typeof createApiClient>;
|
||||||
|
}) {
|
||||||
|
const { data: history, isLoading: loading } = useQuery({
|
||||||
|
queryKey: ["RESOURCE_STATUS_HISTORY", resourceId, 30],
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
const res = await api.get(
|
||||||
|
`/resource/${resourceId}/status-history`,
|
||||||
|
{
|
||||||
|
params: { days: 30 },
|
||||||
|
signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return (res.data.data ?? res.data) as StatusHistoryResponse;
|
||||||
|
},
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
meta: { api }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
{Array.from({ length: 90 }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="w-1 h-6 rounded-sm bg-muted animate-pulse"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!history) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TooltipProvider>
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
{history.days.map((bucket, i) => {
|
||||||
|
const colorClass =
|
||||||
|
bucket.status === "good"
|
||||||
|
? "bg-green-500"
|
||||||
|
: bucket.status === "degraded"
|
||||||
|
? "bg-yellow-500"
|
||||||
|
: bucket.status === "bad"
|
||||||
|
? "bg-red-500"
|
||||||
|
: "bg-muted";
|
||||||
|
return (
|
||||||
|
<Tooltip key={i}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
className={`w-1 h-6 rounded-sm ${colorClass} cursor-default`}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<span>
|
||||||
|
{bucket.date}:{" "}
|
||||||
|
{bucket.uptimePercent}% uptime
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||||
|
{history.overallUptimePercent.toFixed(1)}% uptime
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const proxyColumns: ExtendedColumnDef<ResourceRow>[] = [
|
const proxyColumns: ExtendedColumnDef<ResourceRow>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
@@ -422,6 +514,20 @@ export default function ProxyResourcesTable({
|
|||||||
return statusOrder[statusA] - statusOrder[statusB];
|
return statusOrder[statusA] - statusOrder[statusB];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "statusHistory",
|
||||||
|
friendlyName: t("statusHistory"),
|
||||||
|
header: () => <span className="p-3">{t("statusHistory")}</span>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resourceRow = row.original;
|
||||||
|
return (
|
||||||
|
<ResourceStatusHistory
|
||||||
|
resourceId={resourceRow.id}
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "domain",
|
accessorKey: "domain",
|
||||||
friendlyName: t("access"),
|
friendlyName: t("access"),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition, useEffect } from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||||
@@ -85,6 +85,13 @@ export default function SitesTable({
|
|||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
router.refresh();
|
||||||
|
}, 10_000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const booleanSearchFilterSchema = z
|
const booleanSearchFilterSchema = z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.optional()
|
.optional()
|
||||||
@@ -226,7 +233,7 @@ export default function SitesTable({
|
|||||||
{
|
{
|
||||||
id: "uptime",
|
id: "uptime",
|
||||||
friendlyName: "Uptime",
|
friendlyName: "Uptime",
|
||||||
header: () => <span className="p-3">Uptime (30d)</span>,
|
header: () => <span className="p-3">{t("uptime30d")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -54,13 +54,15 @@ export default function UptimeMiniBar({
|
|||||||
const siteQuery = useQuery({
|
const siteQuery = useQuery({
|
||||||
...orgQueries.siteStatusHistory({ siteId: siteId ?? 0, days }),
|
...orgQueries.siteStatusHistory({ siteId: siteId ?? 0, days }),
|
||||||
enabled: siteId != null,
|
enabled: siteId != null,
|
||||||
meta: { api }
|
meta: { api },
|
||||||
|
staleTime: 5 * 60 * 1000
|
||||||
});
|
});
|
||||||
|
|
||||||
const hcQuery = useQuery({
|
const hcQuery = useQuery({
|
||||||
...orgQueries.healthCheckStatusHistory({ orgId: orgId ?? "", healthCheckId: healthCheckId ?? 0, days }),
|
...orgQueries.healthCheckStatusHistory({ orgId: orgId ?? "", healthCheckId: healthCheckId ?? 0, days }),
|
||||||
enabled: healthCheckId != null && siteId == null,
|
enabled: healthCheckId != null && siteId == null,
|
||||||
meta: { api }
|
meta: { api },
|
||||||
|
staleTime: 5 * 60 * 1000
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, isLoading } = siteId != null ? siteQuery : hcQuery;
|
const { data, isLoading } = siteId != null ? siteQuery : hcQuery;
|
||||||
|
|||||||
Reference in New Issue
Block a user