mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-04 19:44:47 +00:00
More refreshing and status history displays
This commit is contained in:
@@ -2,32 +2,35 @@
|
||||
|
||||
import { KeyRound, ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export function ContactSalesBanner() {
|
||||
const t = useTranslations();
|
||||
|
||||
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="py-3 px-4">
|
||||
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
||||
<KeyRound className="size-4 shrink-0 text-black-500" />
|
||||
<span>
|
||||
Contact sales to enable this feature.{" "}
|
||||
{t("contactSalesEnable")}{" "}
|
||||
<Link
|
||||
href="https://click.fossorial.io/ep922"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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" />
|
||||
</Link>
|
||||
{" or "}
|
||||
{" " + t("contactSalesOr") + " "}
|
||||
<Link
|
||||
href="https://pangolin.net/contact"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 font-medium text-black-600 underline"
|
||||
>
|
||||
contact us
|
||||
{t("contactSalesContactUs")}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</Link>
|
||||
.
|
||||
@@ -36,4 +39,4 @@ export function ContactSalesBanner() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,7 @@ export default function HealthChecksTable({
|
||||
{
|
||||
id: "uptime",
|
||||
friendlyName: "Uptime",
|
||||
header: () => <span className="p-3">Uptime (30d)</span>,
|
||||
header: () => <span className="p-3">{t("uptime30d")}</span>,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<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 { UpdateResourceResponse } from "@server/routers/resource";
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AxiosResponse } from "axios";
|
||||
import {
|
||||
ArrowDown01Icon,
|
||||
@@ -37,6 +38,7 @@ import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
useEffect,
|
||||
useOptimistic,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -47,6 +49,13 @@ import { useDebouncedCallback } from "use-debounce";
|
||||
import z from "zod";
|
||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||
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 = {
|
||||
targetId: number;
|
||||
@@ -161,6 +170,13 @@ export default function ProxyResourcesTable({
|
||||
const [isRefreshing, startTransition] = useTransition();
|
||||
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
router.refresh();
|
||||
}, 10_000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const refreshData = () => {
|
||||
startTransition(() => {
|
||||
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>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -422,6 +514,20 @@ export default function ProxyResourcesTable({
|
||||
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",
|
||||
friendlyName: t("access"),
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useState, useTransition, useEffect } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import z from "zod";
|
||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||
@@ -85,6 +85,13 @@ export default function SitesTable({
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
router.refresh();
|
||||
}, 10_000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const booleanSearchFilterSchema = z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
@@ -226,7 +233,7 @@ export default function SitesTable({
|
||||
{
|
||||
id: "uptime",
|
||||
friendlyName: "Uptime",
|
||||
header: () => <span className="p-3">Uptime (30d)</span>,
|
||||
header: () => <span className="p-3">{t("uptime30d")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
return (
|
||||
|
||||
@@ -54,13 +54,15 @@ export default function UptimeMiniBar({
|
||||
const siteQuery = useQuery({
|
||||
...orgQueries.siteStatusHistory({ siteId: siteId ?? 0, days }),
|
||||
enabled: siteId != null,
|
||||
meta: { api }
|
||||
meta: { api },
|
||||
staleTime: 5 * 60 * 1000
|
||||
});
|
||||
|
||||
const hcQuery = useQuery({
|
||||
...orgQueries.healthCheckStatusHistory({ orgId: orgId ?? "", healthCheckId: healthCheckId ?? 0, days }),
|
||||
enabled: healthCheckId != null && siteId == null,
|
||||
meta: { api }
|
||||
meta: { api },
|
||||
staleTime: 5 * 60 * 1000
|
||||
});
|
||||
|
||||
const { data, isLoading } = siteId != null ? siteQuery : hcQuery;
|
||||
|
||||
Reference in New Issue
Block a user