@@ -625,7 +637,14 @@ export default function PublicResourcesTable({
];
return cols;
- }, [orgId, t, searchParams]);
+ }, [
+ orgId,
+ t,
+ searchParams,
+ statusHistoryQuery.data,
+ statusHistoryQuery.isLoading,
+ initialCertificates
+ ]);
function handleFilterChange(
column: string,
diff --git a/src/components/ResourceAccessCertIndicator.tsx b/src/components/ResourceAccessCertIndicator.tsx
index 40ddfbfab..4fc9c4c2d 100644
--- a/src/components/ResourceAccessCertIndicator.tsx
+++ b/src/components/ResourceAccessCertIndicator.tsx
@@ -7,6 +7,7 @@ import {
PopoverContent
} from "@app/components/ui/popover";
import { useCertificate } from "@app/hooks/useCertificate";
+import type { GetCertificateResponse } from "@server/routers/certificates/types";
import { cn } from "@app/lib/cn";
import { FileBadge } from "lucide-react";
import { useTranslations } from "next-intl";
@@ -17,11 +18,13 @@ import {
useState,
type ReactNode
} from "react";
+import { durationToMs } from "@app/lib/durationToMs";
type ResourceAccessCertIndicatorProps = {
orgId: string;
domainId: string;
fullDomain: string;
+ initialCertValue?: GetCertificateResponse | null;
};
function getStatusColor(status: string) {
@@ -43,7 +46,8 @@ function getStatusColor(status: string) {
export function ResourceAccessCertIndicator({
orgId,
domainId,
- fullDomain
+ fullDomain,
+ initialCertValue
}: ResourceAccessCertIndicatorProps) {
const t = useTranslations();
const [open, setOpen] = useState(false);
@@ -53,16 +57,19 @@ export function ResourceAccessCertIndicator({
orgId,
domainId,
fullDomain,
- autoFetch: true,
+ initialCertValue,
polling: open,
- pollingInterval: 5000
+ pollingInterval: durationToMs(5, "seconds")
});
const { cert, certLoading, certError, refreshing, fetchCert } = certificate;
+ // `polling` only schedules on predefined intervals (1 second),
+ // so the first request would be a full interval away if open = true (which set polling = true).
+ // So we fetch immediately so the popover opens with fresh data.
useEffect(() => {
if (!open) return;
- void fetchCert(false);
+ void fetchCert();
}, [open, fetchCert]);
const clearCloseTimer = useCallback(() => {
diff --git a/src/components/ResourceInfoBox.tsx b/src/components/ResourceInfoBox.tsx
index f459d2c38..642ba8e12 100644
--- a/src/components/ResourceInfoBox.tsx
+++ b/src/components/ResourceInfoBox.tsx
@@ -180,7 +180,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
orgId={resource.orgId}
domainId={resource.domainId!}
fullDomain={resource.fullDomain!}
- autoFetch={true}
showLabel={false}
polling={true}
/>
diff --git a/src/components/SiteResourceInfoBox.tsx b/src/components/SiteResourceInfoBox.tsx
index f57acbe99..15457a189 100644
--- a/src/components/SiteResourceInfoBox.tsx
+++ b/src/components/SiteResourceInfoBox.tsx
@@ -182,7 +182,6 @@ export function SiteResourceInfoSections({
orgId={siteResource.orgId}
domainId={siteResource.domainId!}
fullDomain={siteResource.fullDomain!}
- autoFetch={true}
showLabel={false}
polling={true}
/>
diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx
index 95d63cf9a..5af07d112 100644
--- a/src/components/SitesTable.tsx
+++ b/src/components/SitesTable.tsx
@@ -1,7 +1,7 @@
"use client";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
-import UptimeMiniBar from "@app/components/UptimeMiniBar";
+import { UptimeMiniBar } from "@app/components/UptimeMiniBar";
import {
Credenza,
@@ -52,12 +52,12 @@ import {
} from "./ui/controlled-data-table";
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
-import { usePaidStatus } from "@app/hooks/usePaidStatus";
+import { durationToMs } from "@app/lib/durationToMs";
+import { orgQueries, productUpdatesQueries } from "@app/lib/queries";
+import { useQuery } from "@tanstack/react-query";
+import semver from "semver";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
import { LabelsTableCell } from "./LabelsTableCell";
-import { useQuery } from "@tanstack/react-query";
-import { productUpdatesQueries } from "@app/lib/queries";
-import semver from "semver";
export type SiteRow = {
id: number;
@@ -89,6 +89,8 @@ type SitesTableProps = {
rowCount: number;
};
+const SITE_STATUS_HISTORY_DAYS = 30;
+
export default function SitesTable({
sites,
orgId,
@@ -112,7 +114,16 @@ export default function SitesTable({
const [isRefreshing, startTransition] = useTransition();
const [isNavigatingToAddPage, startNavigation] = useTransition();
- const { isPaidUser } = usePaidStatus();
+ const siteIds = useMemo(() => sites.map((s) => s.id), [sites]);
+
+ const statusHistoryQuery = useQuery({
+ ...orgQueries.batchedSiteStatusHistory({
+ orgId,
+ siteIds,
+ days: SITE_STATUS_HISTORY_DAYS
+ }),
+ enabled: siteIds.length > 0
+ });
const api = createApiClient(useEnvContext());
const t = useTranslations();
@@ -296,7 +307,14 @@ export default function SitesTable({
if (originalRow.type == "local") {
return
-;
}
- return
;
+ const data = statusHistoryQuery.data?.[row.original.id];
+ return (
+
+ );
}
},
{
@@ -359,14 +377,11 @@ export default function SitesTable({
cell: ({ row }) => {
const originalRow = row.original;
- let updateAvailable = Boolean(
+ const updateAvailable = Boolean(
latestNewtVersion &&
- originalRow.newtVersion &&
- semver.valid(originalRow.newtVersion) &&
- semver.lt(
- originalRow.newtVersion,
- latestNewtVersion
- )
+ originalRow.newtVersion &&
+ semver.valid(originalRow.newtVersion) &&
+ semver.lt(originalRow.newtVersion, latestNewtVersion)
);
if (originalRow.type === "newt") {
@@ -623,7 +638,14 @@ export default function SitesTable({
];
return cols;
- }, [orgId, t, searchParams, latestNewtVersion]);
+ }, [
+ orgId,
+ t,
+ searchParams,
+ latestNewtVersion,
+ statusHistoryQuery.data,
+ statusHistoryQuery.isLoading
+ ]);
function toggleSort(column: string) {
const newSearch = getNextSortOrder(column, searchParams);
diff --git a/src/components/UptimeMiniBar.tsx b/src/components/UptimeMiniBar.tsx
index b7e684c8b..454f44d66 100644
--- a/src/components/UptimeMiniBar.tsx
+++ b/src/components/UptimeMiniBar.tsx
@@ -1,15 +1,14 @@
"use client";
-import { useQuery } from "@tanstack/react-query";
-import { orgQueries } from "@app/lib/queries";
import {
Tooltip,
TooltipContent,
TooltipTrigger
} from "@app/components/ui/tooltip";
-import { useEnvContext } from "@app/hooks/useEnvContext";
-import { createApiClient } from "@app/lib/api";
import { cn } from "@app/lib/cn";
+import { orgQueries } from "@app/lib/queries";
+import type { StatusHistoryResponse } from "@server/lib/statusHistory";
+import { useQuery } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
function formatDuration(seconds: number): string {
@@ -46,21 +45,16 @@ type UptimeMiniBarProps = {
days?: number;
};
-export default function UptimeMiniBar({
+export default function UptimeMiniBarWrapper({
orgId,
siteId,
resourceId,
healthCheckId,
days = 30
}: UptimeMiniBarProps) {
- const t = useTranslations();
- const api = createApiClient(useEnvContext());
-
const siteQuery = useQuery({
...orgQueries.siteStatusHistory({ siteId: siteId ?? 0, days }),
- enabled: siteId != null,
- meta: { api },
- staleTime: 5 * 60 * 1000
+ enabled: siteId != null
});
const hcQuery = useQuery({
@@ -69,16 +63,12 @@ export default function UptimeMiniBar({
healthCheckId: healthCheckId ?? 0,
days
}),
- enabled: healthCheckId != null && siteId == null && resourceId == null,
- meta: { api },
- staleTime: 5 * 60 * 1000
+ enabled: healthCheckId != null && siteId == null && resourceId == null
});
const resourceQuery = useQuery({
...orgQueries.resourceStatusHistory({ resourceId, days }),
- enabled: resourceId != null && siteId == null && healthCheckId == null,
- meta: { api },
- staleTime: 5 * 60 * 1000
+ enabled: resourceId != null && siteId == null && healthCheckId == null
});
const { data, isLoading } =
@@ -88,6 +78,22 @@ export default function UptimeMiniBar({
? resourceQuery
: hcQuery;
+ return
;
+}
+
+type UptimeMiniBarUIProps = {
+ data?: StatusHistoryResponse;
+ isLoading?: boolean;
+ days?: number;
+};
+
+export function UptimeMiniBar({
+ data,
+ isLoading,
+ days = 30
+}: UptimeMiniBarUIProps) {
+ const t = useTranslations();
+
if (isLoading) {
return (
@@ -138,7 +144,8 @@ export default function UptimeMiniBar({
{formatDate(day.date)}
- {day.status === "no_data" || day.status === "unknown"
+ {day.status === "no_data" ||
+ day.status === "unknown"
? t("uptimeNoData")
: `${day.uptimePercent.toFixed(1)}% ${t("uptimeSuffix")}`}
@@ -159,4 +166,4 @@ export default function UptimeMiniBar({
);
-}
\ No newline at end of file
+}
diff --git a/src/hooks/useCertificate.ts b/src/hooks/useCertificate.ts
index cd0802ef0..a71c3ef70 100644
--- a/src/hooks/useCertificate.ts
+++ b/src/hooks/useCertificate.ts
@@ -1,18 +1,20 @@
"use client";
-import { useState, useCallback, useEffect } from "react";
-import { AxiosResponse } from "axios";
+import { useCallback } from "react";
import { GetCertificateResponse } from "@server/routers/certificates/types";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { domainQueries } from "@app/lib/queries";
+import { durationToMs } from "@app/lib/durationToMs";
type UseCertificateProps = {
orgId: string;
domainId: string;
fullDomain: string;
- autoFetch?: boolean;
polling?: boolean;
pollingInterval?: number;
+ initialCertValue?: GetCertificateResponse | null;
};
type UseCertificateReturn = {
@@ -20,124 +22,106 @@ type UseCertificateReturn = {
certLoading: boolean;
certError: string | null;
refreshing: boolean;
- fetchCert: (showLoading?: boolean) => Promise