Merge branch 'dev' into feat/show-idp-icon-in-smart-login-form

This commit is contained in:
Fred KISSIE
2026-07-30 17:37:47 +01:00
47 changed files with 1466 additions and 411 deletions
-1
View File
@@ -412,7 +412,6 @@ function AuthPageSettings({
fullDomain={
loginPage.fullDomain
}
autoFetch={true}
showLabel={true}
polling={true}
/>
+3 -6
View File
@@ -5,6 +5,7 @@ import { FileBadge, RotateCw } from "lucide-react";
import { useCertificate } from "@app/hooks/useCertificate";
import type { GetCertificateResponse } from "@server/routers/certificates/types";
import { useTranslations } from "next-intl";
import { durationToMs } from "@app/lib/durationToMs";
export type CertificateStatusContentProps = {
cert: GetCertificateResponse | null;
@@ -32,8 +33,7 @@ export function CertificateStatusContent({
const labelClass =
"inline-flex shrink-0 items-center self-center text-sm font-medium leading-normal";
const valueClass =
"inline-flex items-center gap-2 text-sm leading-normal";
const valueClass = "inline-flex items-center gap-2 text-sm leading-normal";
const handleRefresh = async () => {
await refreshCert();
@@ -187,7 +187,6 @@ type CertificateStatusProps = {
orgId: string;
domainId: string;
fullDomain: string;
autoFetch?: boolean;
showLabel?: boolean;
className?: string;
onRefresh?: () => void;
@@ -199,18 +198,16 @@ export default function CertificateStatus({
orgId,
domainId,
fullDomain,
autoFetch = true,
showLabel = true,
className = "",
onRefresh,
polling = false,
pollingInterval = 5000
pollingInterval = durationToMs(5, "seconds")
}: CertificateStatusProps) {
const hook = useCertificate({
orgId,
domainId,
fullDomain,
autoFetch,
polling,
pollingInterval
});
+26 -4
View File
@@ -1,6 +1,6 @@
"use client";
import UptimeMiniBar from "@app/components/UptimeMiniBar";
import { UptimeMiniBar } from "@app/components/UptimeMiniBar";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import HealthCheckCredenza, {
@@ -51,6 +51,8 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { cn } from "@app/lib/cn";
import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover";
import { orgQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query";
type StandaloneHealthChecksTableProps = {
orgId: string;
@@ -81,6 +83,8 @@ function formatTarget(row: HealthCheckRow): string {
return `${scheme}://${host}${port}${path}`;
}
const HEALTH_CHECK_STATUS_HISTORY_DAYS = 30;
export default function HealthChecksTable({
orgId,
healthChecks,
@@ -157,6 +161,20 @@ export default function HealthChecksTable({
const rows = healthChecks;
const healthCheckIds = useMemo(
() => rows.map((r) => r.targetHealthCheckId),
[rows]
);
const statusHistoryQuery = useQuery({
...orgQueries.batchedHealthCheckStatusHistory({
orgId,
healthCheckIds,
days: HEALTH_CHECK_STATUS_HISTORY_DAYS
}),
enabled: healthCheckIds.length > 0
});
function refreshList() {
startRefresh(() => {
router.refresh();
@@ -547,9 +565,13 @@ export default function HealthChecksTable({
cell: ({ row }) => {
return (
<UptimeMiniBar
orgId={orgId}
healthCheckId={row.original.targetHealthCheckId}
days={30}
isLoading={statusHistoryQuery.isLoading}
data={
statusHistoryQuery.data?.[
row.original.targetHealthCheckId
]
}
days={HEALTH_CHECK_STATUS_HISTORY_DAYS}
/>
);
}
+9 -2
View File
@@ -37,6 +37,7 @@ import { getPrivateResourceSettingsHref } from "@app/lib/launcherResourceAdminHr
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import type { GetBatchedCertificateResponse } from "@server/routers/certificates/types";
import { UpdateSiteResourceResponse } from "@server/routers/siteResource";
import type { PaginationState } from "@tanstack/react-table";
import { AxiosResponse } from "axios";
@@ -136,13 +137,16 @@ type ClientResourcesTableProps = {
orgId: string;
pagination: PaginationState;
rowCount: number;
/** Certificates prefetched on the server, keyed by full domain. */
initialCertificates?: GetBatchedCertificateResponse;
};
export default function PrivateResourcesTable({
internalResources,
orgId,
pagination,
rowCount
rowCount,
initialCertificates
}: ClientResourcesTableProps) {
const router = useRouter();
const {
@@ -432,6 +436,9 @@ export default function PrivateResourcesTable({
orgId={resourceRow.orgId}
domainId={domainId}
fullDomain={fullDomain}
initialCertValue={
initialCertificates?.[fullDomain]
}
/>
) : null}
<div className="">
@@ -585,7 +592,7 @@ export default function PrivateResourcesTable({
];
return cols;
}, [orgId, t, searchParams]);
}, [orgId, t, searchParams, initialCertificates]);
function handleFilterChange(
column: string,
+43 -24
View File
@@ -7,8 +7,7 @@ import {
ResourceSitesStatusCell,
type ResourceSiteRow
} from "@app/components/ResourceSitesStatusCell";
import { Selectedsite, SitesSelector } from "@app/components/site-selector";
import { Badge } from "@app/components/ui/badge";
import { Selectedsite } from "@app/components/site-selector";
import { Button } from "@app/components/ui/button";
import { ExtendedColumnDef } from "@app/components/ui/data-table";
import {
@@ -18,23 +17,19 @@ import {
DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu";
import { InfoPopup } from "@app/components/ui/info-popup";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import { Switch } from "@app/components/ui/switch";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useNavigationContext } from "@app/hooks/useNavigationContext";
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { cn } from "@app/lib/cn";
import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover";
import { orgQueries } from "@app/lib/queries";
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { UpdateResourceResponse } from "@server/routers/resource";
import type { GetBatchedCertificateResponse } from "@server/routers/certificates/types";
import { useQuery } from "@tanstack/react-query";
import type { PaginationState } from "@tanstack/react-table";
import { AxiosResponse } from "axios";
import {
@@ -45,9 +40,7 @@ import {
ChevronDown,
ChevronsUpDownIcon,
Clock,
Funnel,
MoreHorizontal,
PlusIcon,
ShieldCheck,
ShieldOff,
XCircle
@@ -57,7 +50,6 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import {
startTransition,
useEffect,
useMemo,
useOptimistic,
useRef,
@@ -68,15 +60,11 @@ import {
import { useDebouncedCallback } from "use-debounce";
import z from "zod";
import { ColumnFilterButton } from "./ColumnFilterButton";
import { ControlledDataTable } from "./ui/controlled-data-table";
import UptimeMiniBar from "./UptimeMiniBar";
import { type SelectedLabel } from "./labels-selector";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
import { useLocalLabels } from "@app/hooks/useLocalLabels";
import { LabelsTableCell } from "./LabelsTableCell";
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
import { refresh } from "next/cache";
import { SitesColumnFilterButton } from "./SitesColumnFilterButton";
import { ControlledDataTable } from "./ui/controlled-data-table";
import { UptimeMiniBar } from "./UptimeMiniBar";
export type TargetHealth = {
targetId: number;
@@ -120,6 +108,8 @@ type ProxyResourcesTableProps = {
pagination: PaginationState;
rowCount: number;
initialFilterSite?: Selectedsite | null;
/** Certificates prefetched on the server, keyed by full domain. */
initialCertificates?: GetBatchedCertificateResponse;
};
const booleanSearchFilterSchema = z
@@ -127,12 +117,14 @@ const booleanSearchFilterSchema = z
.optional()
.catch(undefined);
const RESOURCE_STATUS_HISTORY_DAYS = 30;
export default function PublicResourcesTable({
resources,
orgId,
pagination,
rowCount,
initialFilterSite = null
initialCertificates
}: ProxyResourcesTableProps) {
const router = useRouter();
const {
@@ -150,11 +142,24 @@ export default function PublicResourcesTable({
const [selectedResource, setSelectedResource] =
useState<ResourceRow | null>();
const { isPaidUser } = usePaidStatus();
const [isRefreshing, startTransition] = useTransition();
const [isNavigatingToAddPage, startNavigation] = useTransition();
// Only http resources show an uptime bar, so don't ask for the others
const statusHistoryResourceIds = useMemo(
() => resources.filter((r) => r.mode === "http").map((r) => r.id),
[resources]
);
const statusHistoryQuery = useQuery({
...orgQueries.batchedResourceStatusHistory({
orgId,
resourceIds: statusHistoryResourceIds,
days: RESOURCE_STATUS_HISTORY_DAYS
}),
enabled: statusHistoryResourceIds.length > 0
});
const refreshData = () => {
startTransition(() => {
try {
@@ -407,7 +412,11 @@ export default function PublicResourcesTable({
return <span>-</span>;
}
return (
<UptimeMiniBar resourceId={resourceRow.id} days={30} />
<UptimeMiniBar
isLoading={statusHistoryQuery.isLoading}
data={statusHistoryQuery.data?.[resourceRow.id]}
days={RESOURCE_STATUS_HISTORY_DAYS}
/>
);
}
},
@@ -461,6 +470,9 @@ export default function PublicResourcesTable({
orgId={resourceRow.orgId}
domainId={domainId}
fullDomain={certHostname}
initialCertValue={
initialCertificates?.[certHostname]
}
/>
) : null}
<div className="">
@@ -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,
+11 -4
View File
@@ -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(() => {
-1
View File
@@ -180,7 +180,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
orgId={resource.orgId}
domainId={resource.domainId!}
fullDomain={resource.fullDomain!}
autoFetch={true}
showLabel={false}
polling={true}
/>
-1
View File
@@ -182,7 +182,6 @@ export function SiteResourceInfoSections({
orgId={siteResource.orgId}
domainId={siteResource.domainId!}
fullDomain={siteResource.fullDomain!}
autoFetch={true}
showLabel={false}
polling={true}
/>
+37 -15
View File
@@ -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 <span>-</span>;
}
return <UptimeMiniBar siteId={originalRow.id} days={30} />;
const data = statusHistoryQuery.data?.[row.original.id];
return (
<UptimeMiniBar
isLoading={statusHistoryQuery.isLoading}
data={data}
days={SITE_STATUS_HISTORY_DAYS}
/>
);
}
},
{
@@ -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);
+26 -19
View File
@@ -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 <UptimeMiniBar data={data} isLoading={isLoading} days={days} />;
}
type UptimeMiniBarUIProps = {
data?: StatusHistoryResponse;
isLoading?: boolean;
days?: number;
};
export function UptimeMiniBar({
data,
isLoading,
days = 30
}: UptimeMiniBarUIProps) {
const t = useTranslations();
if (isLoading) {
return (
<div className="flex items-center gap-2">
@@ -138,7 +144,8 @@ export default function UptimeMiniBar({
{formatDate(day.date)}
</div>
<div className="text-xs text-primary-foreground/80">
{day.status === "no_data" || day.status === "unknown"
{day.status === "no_data" ||
day.status === "unknown"
? t("uptimeNoData")
: `${day.uptimePercent.toFixed(1)}% ${t("uptimeSuffix")}`}
</div>
@@ -159,4 +166,4 @@ export default function UptimeMiniBar({
</span>
</div>
);
}
}