mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-08 21:37:11 +00:00
Merge branch 'alerting-rules' into dev
This commit is contained in:
95
src/app/[orgId]/settings/alerting/[ruleId]/page.tsx
Normal file
95
src/app/[orgId]/settings/alerting/[ruleId]/page.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGraphEditor";
|
||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||
import { apiResponseToFormValues } from "@app/lib/alertRuleForm";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { GetAlertRuleResponse } from "@server/private/routers/alertRule";
|
||||
import type { AlertRuleFormValues } from "@app/lib/alertRuleForm";
|
||||
|
||||
export default function EditAlertRulePage() {
|
||||
const t = useTranslations();
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const orgId = params.orgId as string;
|
||||
const ruleIdParam = params.ruleId as string;
|
||||
const alertRuleId = parseInt(ruleIdParam, 10);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
||||
|
||||
const [formValues, setFormValues] = useState<AlertRuleFormValues | null | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNaN(alertRuleId)) {
|
||||
router.replace(`/${orgId}/settings/alerting`);
|
||||
return;
|
||||
}
|
||||
|
||||
api.get<AxiosResponse<GetAlertRuleResponse>>(
|
||||
`/org/${orgId}/alert-rule/${alertRuleId}`
|
||||
)
|
||||
.then((res) => {
|
||||
const rule = res.data.data;
|
||||
setFormValues(apiResponseToFormValues(rule));
|
||||
})
|
||||
.catch((e) => {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
setFormValues(null);
|
||||
});
|
||||
}, [orgId, alertRuleId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formValues === null) {
|
||||
router.replace(`/${orgId}/settings/alerting`);
|
||||
}
|
||||
}, [formValues, orgId, router]);
|
||||
|
||||
if (formValues === undefined) {
|
||||
return (
|
||||
<>
|
||||
<HeaderTitle
|
||||
title={t("alertingEditRule")}
|
||||
description={t("alertingRuleCredenzaDescription")}
|
||||
/>
|
||||
<div className="min-h-[12rem] flex items-center justify-center text-muted-foreground text-sm">
|
||||
{t("loading")}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (formValues === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderTitle
|
||||
title={t("alertingEditRule")}
|
||||
description={t("alertingRuleCredenzaDescription")}
|
||||
/>
|
||||
<AlertRuleGraphEditor
|
||||
key={alertRuleId}
|
||||
orgId={orgId}
|
||||
alertRuleId={alertRuleId}
|
||||
initialValues={formValues}
|
||||
isNew={false}
|
||||
disabled={!isPaid}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
src/app/[orgId]/settings/alerting/create/page.tsx
Normal file
32
src/app/[orgId]/settings/alerting/create/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGraphEditor";
|
||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||
import { defaultFormValues } from "@app/lib/alertRuleForm";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function NewAlertRulePage() {
|
||||
const params = useParams();
|
||||
const orgId = params.orgId as string;
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const isPaid = isPaidUser(tierMatrix.alertingRules);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderTitle
|
||||
title={t("alertingCreateRule")}
|
||||
description={t("alertingRuleCredenzaDescription")}
|
||||
/>
|
||||
<AlertRuleGraphEditor
|
||||
orgId={orgId}
|
||||
initialValues={defaultFormValues()}
|
||||
isNew
|
||||
disabled={!isPaid}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
34
src/app/[orgId]/settings/alerting/page.tsx
Normal file
34
src/app/[orgId]/settings/alerting/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import AlertingRulesTable from "@app/components/AlertingRulesTable";
|
||||
import HealthChecksTable from "@app/components/HealthChecksTable";
|
||||
import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type AlertingPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AlertingPage(props: AlertingPageProps) {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{ title: t("alertingTabRules"), href: "" },
|
||||
{ title: t("alertingTabHealthChecks"), href: "" }
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t("alertingTitle")}
|
||||
description={t("alertingDescription")}
|
||||
/>
|
||||
<HorizontalTabs items={tabs} clientSide>
|
||||
<AlertingRulesTable orgId={params.orgId} />
|
||||
<HealthChecksTable orgId={params.orgId} />
|
||||
</HorizontalTabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -471,11 +471,7 @@ export default function GeneralPage() {
|
||||
: `/${row.original.orgId}/settings/resources/proxy/${row.original.resourceNiceId}`
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-6"
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
{row.original.resourceName}
|
||||
<ArrowUpRight className="ml-2 h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
|
||||
function formatBytes(bytes: number | null): string {
|
||||
if (bytes === null || bytes === undefined) return "—";
|
||||
if (bytes === null || bytes === undefined) return "-";
|
||||
if (bytes === 0) return "0 B";
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
@@ -33,7 +33,7 @@ function formatBytes(bytes: number | null): string {
|
||||
function formatDuration(startedAt: number, endedAt: number | null): string {
|
||||
if (endedAt === null || endedAt === undefined) return "Active";
|
||||
const durationSec = endedAt - startedAt;
|
||||
if (durationSec < 0) return "—";
|
||||
if (durationSec < 0) return "-";
|
||||
if (durationSec < 60) return `${durationSec}s`;
|
||||
if (durationSec < 3600) {
|
||||
const m = Math.floor(durationSec / 60);
|
||||
@@ -451,11 +451,7 @@ export default function ConnectionLogsPage() {
|
||||
<Link
|
||||
href={`/${row.original.orgId}/settings/resources/client/?query=${row.original.resourceNiceId}`}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-6"
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
{row.original.resourceName}
|
||||
<ArrowUpRight className="ml-2 h-3 w-3" />
|
||||
</Button>
|
||||
@@ -464,7 +460,7 @@ export default function ConnectionLogsPage() {
|
||||
}
|
||||
return (
|
||||
<span className="whitespace-nowrap">
|
||||
{row.original.resourceName ?? "—"}
|
||||
{row.original.resourceName ?? "-"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -497,11 +493,7 @@ export default function ConnectionLogsPage() {
|
||||
<Link
|
||||
href={`/${row.original.orgId}/settings/clients/${clientType}/${row.original.clientNiceId}`}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-6"
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
<Laptop className="mr-1 h-3 w-3" />
|
||||
{row.original.clientName}
|
||||
<ArrowUpRight className="ml-2 h-3 w-3" />
|
||||
@@ -511,7 +503,7 @@ export default function ConnectionLogsPage() {
|
||||
}
|
||||
return (
|
||||
<span className="whitespace-nowrap">
|
||||
{row.original.clientName ?? "—"}
|
||||
{row.original.clientName ?? "-"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -546,7 +538,7 @@ export default function ConnectionLogsPage() {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span>—</span>;
|
||||
return <span>-</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -620,23 +612,23 @@ export default function ConnectionLogsPage() {
|
||||
<div>
|
||||
<strong>Session ID:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{row.sessionId ?? "—"}
|
||||
{row.sessionId ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Protocol:</strong>{" "}
|
||||
{row.protocol?.toUpperCase() ?? "—"}
|
||||
{row.protocol?.toUpperCase() ?? "-"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Source:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{row.sourceAddr ?? "—"}
|
||||
{row.sourceAddr ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Destination:</strong>{" "}
|
||||
<span className="font-mono">
|
||||
{row.destAddr ?? "—"}
|
||||
{row.destAddr ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -646,7 +638,7 @@ export default function ConnectionLogsPage() {
|
||||
</div>*/}
|
||||
{/*<div>
|
||||
<strong>Resource:</strong>{" "}
|
||||
{row.resourceName ?? "—"}
|
||||
{row.resourceName ?? "-"}
|
||||
{row.resourceNiceId && (
|
||||
<span className="text-muted-foreground ml-1">
|
||||
({row.resourceNiceId})
|
||||
@@ -654,7 +646,7 @@ export default function ConnectionLogsPage() {
|
||||
)}
|
||||
</div>*/}
|
||||
<div>
|
||||
<strong>Site:</strong> {row.siteName ?? "—"}
|
||||
<strong>Site:</strong> {row.siteName ?? "-"}
|
||||
{row.siteNiceId && (
|
||||
<span className="text-muted-foreground ml-1">
|
||||
({row.siteNiceId})
|
||||
@@ -662,7 +654,7 @@ export default function ConnectionLogsPage() {
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Site ID:</strong> {row.siteId ?? "—"}
|
||||
<strong>Site ID:</strong> {row.siteId ?? "-"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Started At:</strong>{" "}
|
||||
@@ -670,14 +662,12 @@ export default function ConnectionLogsPage() {
|
||||
? new Date(
|
||||
row.startedAt * 1000
|
||||
).toLocaleString()
|
||||
: "—"}
|
||||
: "-"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Ended At:</strong>{" "}
|
||||
{row.endedAt
|
||||
? new Date(
|
||||
row.endedAt * 1000
|
||||
).toLocaleString()
|
||||
? new Date(row.endedAt * 1000).toLocaleString()
|
||||
: "Active"}
|
||||
</div>
|
||||
<div>
|
||||
@@ -686,7 +676,7 @@ export default function ConnectionLogsPage() {
|
||||
</div>
|
||||
{/*<div>
|
||||
<strong>Resource ID:</strong>{" "}
|
||||
{row.siteResourceId ?? "—"}
|
||||
{row.siteResourceId ?? "-"}
|
||||
</div>*/}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -360,6 +360,7 @@ export default function GeneralPage() {
|
||||
// 105 - Valid Password
|
||||
// 106 - Valid email
|
||||
// 107 - Valid SSO
|
||||
// 108 - Connected Client
|
||||
|
||||
// 201 - Resource Not Found
|
||||
// 202 - Resource Blocked
|
||||
@@ -377,6 +378,7 @@ export default function GeneralPage() {
|
||||
105: t("validPassword"),
|
||||
106: t("validEmail"),
|
||||
107: t("validSSO"),
|
||||
108: t("connectedClient"),
|
||||
201: t("resourceNotFound"),
|
||||
202: t("resourceBlocked"),
|
||||
203: t("droppedByRule"),
|
||||
@@ -510,14 +512,14 @@ export default function GeneralPage() {
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/${row.original.orgId}/settings/resources/proxy/${row.original.resourceNiceId}`}
|
||||
href={
|
||||
row.original.reason == 108 // for now the client will only have reason 108 so we know where to go
|
||||
? `/${row.original.orgId}/settings/resources/client?query=${row.original.resourceNiceId}`
|
||||
: `/${row.original.orgId}/settings/resources/proxy/${row.original.resourceNiceId}`
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-6"
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
{row.original.resourceName}
|
||||
<ArrowUpRight className="ml-2 h-3 w-3" />
|
||||
</Button>
|
||||
@@ -634,6 +636,7 @@ export default function GeneralPage() {
|
||||
{ value: "105", label: t("validPassword") },
|
||||
{ value: "106", label: t("validEmail") },
|
||||
{ value: "107", label: t("validSSO") },
|
||||
{ value: "108", label: t("connectedClient") },
|
||||
{ value: "201", label: t("resourceNotFound") },
|
||||
{ value: "202", label: t("resourceBlocked") },
|
||||
{ value: "203", label: t("droppedByRule") },
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
HttpDestinationCredenza,
|
||||
parseHttpConfig
|
||||
} from "@app/components/HttpDestinationCredenza";
|
||||
import { S3DestinationCredenza } from "@app/components/S3DestinationCredenza";
|
||||
import { DatadogDestinationCredenza } from "@app/components/DatadogDestinationCredenza";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
// ── Re-export Destination so the rest of the file can use it ──────────────────
|
||||
@@ -203,7 +205,6 @@ function DestinationTypePicker({
|
||||
id: "s3",
|
||||
title: t("streamingS3Title"),
|
||||
description: t("streamingS3Description"),
|
||||
disabled: true,
|
||||
icon: (
|
||||
<Image
|
||||
src="/third-party/s3.png"
|
||||
@@ -218,7 +219,6 @@ function DestinationTypePicker({
|
||||
id: "datadog",
|
||||
title: t("streamingDatadogTitle"),
|
||||
description: t("streamingDatadogDescription"),
|
||||
disabled: true,
|
||||
icon: (
|
||||
<Image
|
||||
src="/third-party/dd.png"
|
||||
@@ -255,7 +255,7 @@ function DestinationTypePicker({
|
||||
<StrategySelect
|
||||
options={destinationTypeOptions}
|
||||
value={selected}
|
||||
onChange={setSelected}
|
||||
onChange={(type) => setSelected(type)}
|
||||
cols={1}
|
||||
/>
|
||||
</div>
|
||||
@@ -291,6 +291,7 @@ export default function StreamingDestinationsPage() {
|
||||
const [typePickerOpen, setTypePickerOpen] = useState(false);
|
||||
const [editingDestination, setEditingDestination] =
|
||||
useState<Destination | null>(null);
|
||||
const [pickedType, setPickedType] = useState<DestinationType>("http");
|
||||
const [togglingIds, setTogglingIds] = useState<Set<number>>(new Set());
|
||||
|
||||
// Delete state
|
||||
@@ -392,7 +393,8 @@ export default function StreamingDestinationsPage() {
|
||||
setTypePickerOpen(true);
|
||||
};
|
||||
|
||||
const handleTypePicked = (_type: DestinationType) => {
|
||||
const handleTypePicked = (type: DestinationType) => {
|
||||
setPickedType(type);
|
||||
setTypePickerOpen(false);
|
||||
setEditingDestination(null);
|
||||
setModalOpen(true);
|
||||
@@ -400,6 +402,7 @@ export default function StreamingDestinationsPage() {
|
||||
|
||||
const openEdit = (destination: Destination) => {
|
||||
setEditingDestination(destination);
|
||||
setPickedType((destination.type as DestinationType) ?? "http");
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -434,7 +437,7 @@ export default function StreamingDestinationsPage() {
|
||||
disabled={!isEnterprise}
|
||||
/>
|
||||
))}
|
||||
{/* Add card is always clickable — paywall is enforced inside the picker */}
|
||||
{/* Add card is always clickable - paywall is enforced inside the picker */}
|
||||
<AddDestinationCard onClick={openCreate} />
|
||||
</div>
|
||||
)}
|
||||
@@ -446,13 +449,33 @@ export default function StreamingDestinationsPage() {
|
||||
isPaywalled={!isEnterprise}
|
||||
/>
|
||||
|
||||
<HttpDestinationCredenza
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
editing={editingDestination}
|
||||
orgId={orgId}
|
||||
onSaved={loadDestinations}
|
||||
/>
|
||||
{pickedType === "http" && (
|
||||
<HttpDestinationCredenza
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
editing={editingDestination}
|
||||
orgId={orgId}
|
||||
onSaved={loadDestinations}
|
||||
/>
|
||||
)}
|
||||
{pickedType === "s3" && (
|
||||
<S3DestinationCredenza
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
editing={editingDestination}
|
||||
orgId={orgId}
|
||||
onSaved={loadDestinations}
|
||||
/>
|
||||
)}
|
||||
{pickedType === "datadog" && (
|
||||
<DatadogDestinationCredenza
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
editing={editingDestination}
|
||||
orgId={orgId}
|
||||
onSaved={loadDestinations}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteTarget && (
|
||||
<ConfirmDeleteDialog
|
||||
|
||||
@@ -65,23 +65,34 @@ export default async function ClientResourcesPage(
|
||||
id: siteResource.siteResourceId,
|
||||
name: siteResource.name,
|
||||
orgId: params.orgId,
|
||||
siteName: siteResource.siteName,
|
||||
siteAddress: siteResource.siteAddress || null,
|
||||
mode: siteResource.mode || ("port" as any),
|
||||
sites: siteResource.siteIds.map((siteId, idx) => ({
|
||||
siteId,
|
||||
siteName: siteResource.siteNames[idx],
|
||||
siteNiceId: siteResource.siteNiceIds[idx],
|
||||
online: siteResource.siteOnlines[idx]
|
||||
})),
|
||||
mode: siteResource.mode,
|
||||
scheme: siteResource.scheme,
|
||||
ssl: siteResource.ssl,
|
||||
siteNames: siteResource.siteNames,
|
||||
siteAddresses: siteResource.siteAddresses || null,
|
||||
// protocol: siteResource.protocol,
|
||||
// proxyPort: siteResource.proxyPort,
|
||||
siteId: siteResource.siteId,
|
||||
siteIds: siteResource.siteIds,
|
||||
destination: siteResource.destination,
|
||||
// destinationPort: siteResource.destinationPort,
|
||||
httpHttpsPort: siteResource.destinationPort ?? null,
|
||||
alias: siteResource.alias || null,
|
||||
aliasAddress: siteResource.aliasAddress || null,
|
||||
siteNiceId: siteResource.siteNiceId,
|
||||
siteNiceIds: siteResource.siteNiceIds,
|
||||
niceId: siteResource.niceId,
|
||||
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
||||
udpPortRangeString: siteResource.udpPortRangeString || null,
|
||||
disableIcmp: siteResource.disableIcmp || false,
|
||||
authDaemonMode: siteResource.authDaemonMode ?? null,
|
||||
authDaemonPort: siteResource.authDaemonPort ?? null
|
||||
authDaemonPort: siteResource.authDaemonPort ?? null,
|
||||
subdomain: siteResource.subdomain ?? null,
|
||||
domainId: siteResource.domainId ?? null,
|
||||
fullDomain: siteResource.fullDomain ?? null
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -62,6 +62,7 @@ import { GetResourceResponse } from "@server/routers/resource/getResource";
|
||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import UptimeAlertSection from "@app/components/UptimeAlertSection";
|
||||
|
||||
type MaintenanceSectionFormProps = {
|
||||
resource: GetResourceResponse;
|
||||
@@ -578,6 +579,13 @@ export default function GeneralForm() {
|
||||
return (
|
||||
<>
|
||||
<SettingsContainer>
|
||||
{resource?.resourceId && resource?.orgId && (
|
||||
<UptimeAlertSection
|
||||
orgId={resource.orgId}
|
||||
resourceId={resource.resourceId}
|
||||
startingName={resource.name}
|
||||
/>
|
||||
)}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import HealthCheckDialog from "@/components/HealthCheckDialog";
|
||||
import HealthCheckCredenza from "@/components/HealthCheckCredenza";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
@@ -168,6 +168,30 @@ function ProxyResourceTargetsForm({
|
||||
|
||||
const [targets, setTargets] = useState<LocalTarget[]>(initialTargets);
|
||||
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
|
||||
|
||||
const { data: polledTargets } = useQuery({
|
||||
...resourceQueries.resourceTargets({
|
||||
resourceId: resource.resourceId
|
||||
}),
|
||||
refetchInterval: 10_000
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!polledTargets) return;
|
||||
setTargets((prev) =>
|
||||
prev.map((t) => {
|
||||
const fresh = polledTargets.find(
|
||||
(p) => p.targetId === t.targetId
|
||||
);
|
||||
if (!fresh) return t;
|
||||
return {
|
||||
...t,
|
||||
hcHealth: fresh.hcHealth,
|
||||
hcEnabled: t.updated ? t.hcEnabled : fresh.hcEnabled
|
||||
};
|
||||
})
|
||||
);
|
||||
}, [polledTargets]);
|
||||
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
|
||||
new Map()
|
||||
);
|
||||
@@ -317,19 +341,6 @@ function ProxyResourceTargetsForm({
|
||||
header: () => <span className="p-3">{t("healthCheck")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.hcHealth || "unknown";
|
||||
const isEnabled = row.original.hcEnabled;
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return "green";
|
||||
case "unhealthy":
|
||||
return "red";
|
||||
case "unknown":
|
||||
default:
|
||||
return "secondary";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
@@ -343,19 +354,7 @@ function ProxyResourceTargetsForm({
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return <CircleCheck className="w-3 h-3" />;
|
||||
case "unhealthy":
|
||||
return <CircleX className="w-3 h-3" />;
|
||||
case "unknown":
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
{row.original.siteType === "newt" ? (
|
||||
<Button
|
||||
@@ -366,12 +365,15 @@ function ProxyResourceTargetsForm({
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-2 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : ""}`}
|
||||
className={`flex items-center gap-2 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : "text-neutral-500"}`}
|
||||
>
|
||||
<Settings className="h-4 w-4 text-foreground" />
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${status === "healthy" ? "bg-green-500" : status === "unhealthy" ? "bg-destructive" : "bg-neutral-500"}`}
|
||||
></div>
|
||||
{getStatusText(status)}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
) : (
|
||||
<span>-</span>
|
||||
)}
|
||||
@@ -640,10 +642,10 @@ function ProxyResourceTargetsForm({
|
||||
hcInterval: null,
|
||||
hcTimeout: null,
|
||||
hcHeaders: null,
|
||||
hcFollowRedirects: null,
|
||||
hcScheme: null,
|
||||
hcHostname: null,
|
||||
hcPort: null,
|
||||
hcFollowRedirects: null,
|
||||
hcHealth: "unknown",
|
||||
hcStatus: null,
|
||||
hcMode: null,
|
||||
@@ -965,10 +967,10 @@ function ProxyResourceTargetsForm({
|
||||
</SettingsSection>
|
||||
|
||||
{selectedTargetForHealthCheck && (
|
||||
<HealthCheckDialog
|
||||
<HealthCheckCredenza
|
||||
mode="autoSave"
|
||||
open={healthCheckDialogOpen}
|
||||
setOpen={setHealthCheckDialogOpen}
|
||||
targetId={selectedTargetForHealthCheck.targetId}
|
||||
targetAddress={`${selectedTargetForHealthCheck.ip}:${selectedTargetForHealthCheck.port}`}
|
||||
targetMethod={
|
||||
selectedTargetForHealthCheck.method || undefined
|
||||
@@ -993,7 +995,7 @@ function ProxyResourceTargetsForm({
|
||||
selectedTargetForHealthCheck.hcPort ||
|
||||
selectedTargetForHealthCheck.port,
|
||||
hcFollowRedirects:
|
||||
selectedTargetForHealthCheck.hcFollowRedirects ||
|
||||
selectedTargetForHealthCheck.hcFollowRedirects ??
|
||||
true,
|
||||
hcStatus:
|
||||
selectedTargetForHealthCheck.hcStatus || undefined,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import DomainPicker from "@app/components/DomainPicker";
|
||||
import HealthCheckDialog from "@app/components/HealthCheckDialog";
|
||||
import HealthCheckCredenza from "@app/components/HealthCheckCredenza";
|
||||
import {
|
||||
PathMatchDisplay,
|
||||
PathMatchModal,
|
||||
@@ -1477,12 +1477,10 @@ export default function Page() {
|
||||
</Button>
|
||||
</div>
|
||||
{selectedTargetForHealthCheck && (
|
||||
<HealthCheckDialog
|
||||
<HealthCheckCredenza
|
||||
mode="autoSave"
|
||||
open={healthCheckDialogOpen}
|
||||
setOpen={setHealthCheckDialogOpen}
|
||||
targetId={
|
||||
selectedTargetForHealthCheck.targetId
|
||||
}
|
||||
targetAddress={`${selectedTargetForHealthCheck.ip}:${selectedTargetForHealthCheck.port}`}
|
||||
targetMethod={
|
||||
selectedTargetForHealthCheck.method ||
|
||||
@@ -1517,7 +1515,7 @@ export default function Page() {
|
||||
selectedTargetForHealthCheck.hcPort ||
|
||||
selectedTargetForHealthCheck.port,
|
||||
hcFollowRedirects:
|
||||
selectedTargetForHealthCheck.hcFollowRedirects ||
|
||||
selectedTargetForHealthCheck.hcFollowRedirects ??
|
||||
true,
|
||||
hcStatus:
|
||||
selectedTargetForHealthCheck.hcStatus ||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import UptimeAlertSection from "@app/components/UptimeAlertSection";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -111,6 +113,13 @@ export default function GeneralPage() {
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{site?.siteId && site?.orgId && (
|
||||
<UptimeAlertSection
|
||||
orgId={site.orgId}
|
||||
siteId={site.siteId}
|
||||
startingName={site.name}
|
||||
/>
|
||||
)}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
|
||||
@@ -425,7 +425,7 @@ export default function Page() {
|
||||
setRemoteExitNodeOptions(exitNodeOptions);
|
||||
|
||||
if (exitNodeOptions.length === 0) {
|
||||
// No remote exit nodes available — remove local option and default to newt
|
||||
// No remote exit nodes available - remove local option and default to newt
|
||||
setTunnelTypes((prev: any) =>
|
||||
prev.filter((item: any) => item.id !== "local")
|
||||
);
|
||||
@@ -434,7 +434,7 @@ export default function Page() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch remote exit nodes:", error);
|
||||
// If fetch fails, no remote exit nodes available — remove local option and default to newt
|
||||
// If fetch fails, no remote exit nodes available - remove local option and default to newt
|
||||
setTunnelTypes((prev: any) =>
|
||||
prev.filter((item: any) => item.id !== "local")
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { SidebarNavItem } from "@app/components/SidebarNav";
|
||||
import { Env } from "@app/lib/types/env";
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
BellRing,
|
||||
Boxes,
|
||||
Building2,
|
||||
Cable,
|
||||
@@ -212,9 +213,9 @@ export const orgNavSections = (
|
||||
icon: <Building2 className="size-4 flex-none" />,
|
||||
items: [
|
||||
{
|
||||
title: "sidebarApiKeys",
|
||||
href: "/{orgId}/settings/api-keys",
|
||||
icon: <KeyRound className="size-4 flex-none" />
|
||||
title: "sidebarAlerting",
|
||||
href: "/{orgId}/settings/alerting",
|
||||
icon: <BellRing className="size-4 flex-none" />
|
||||
},
|
||||
{
|
||||
title: "sidebarProvisioning",
|
||||
@@ -225,6 +226,11 @@ export const orgNavSections = (
|
||||
title: "sidebarBluePrints",
|
||||
href: "/{orgId}/settings/blueprints",
|
||||
icon: <ReceiptText className="size-4 flex-none" />
|
||||
},
|
||||
{
|
||||
title: "sidebarApiKeys",
|
||||
href: "/{orgId}/settings/api-keys",
|
||||
icon: <KeyRound className="size-4 flex-none" />
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
32
src/app/private-maintenance-screen/page.tsx
Normal file
32
src/app/private-maintenance-screen/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@app/components/ui/card";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Private Placeholder"
|
||||
};
|
||||
|
||||
export default async function MaintenanceScreen() {
|
||||
const t = await getTranslations();
|
||||
|
||||
let title = t("privateMaintenanceScreenTitle");
|
||||
let message = t("privateMaintenanceScreenMessage");
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">{message}</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user