mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-07 21:19:03 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4eb2a0aa24 | |||
| d04740fede | |||
| b7c0669c38 |
@@ -1804,10 +1804,6 @@
|
||||
"alertingRulesBannerDescription": "Each rule ties together what to watch (a site, health check, or resource), when to fire (for example offline or unhealthy), and how to notify your team via email, webhooks, or integrations. Use this list to create, enable, and manage those rules.",
|
||||
"alertingHealthChecksBannerTitle": "Monitor Health & Resources",
|
||||
"alertingHealthChecksBannerDescription": "Health checks are HTTP or TCP monitors you define once. You can then use them as sources in alert rules so you get notified when a target becomes healthy or unhealthy. Health checks on resources also appear here.",
|
||||
"alertingTestRule": "Test Alert Rule",
|
||||
"alertingNoActionsTitle": "No actions configured",
|
||||
"alertingNoActionsSaveDescription": "Add at least one action so this rule can notify someone when it fires.",
|
||||
"alertingNoActionsTestDescription": "Add at least one action before you can test this rule.",
|
||||
"standaloneHcTableTitle": "Health Checks",
|
||||
"standaloneHcSearchPlaceholder": "Search health checks…",
|
||||
"standaloneHcAddButton": "Create Health Check",
|
||||
|
||||
Generated
+2381
-3110
File diff suppressed because it is too large
Load Diff
+69
-69
@@ -32,49 +32,49 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "8.5.0",
|
||||
"@asteasolutions/zod-to-openapi": "9.1.0",
|
||||
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz",
|
||||
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz",
|
||||
"@aws-sdk/client-s3": "3.1056.0",
|
||||
"@aws-sdk/client-s3": "3.1102.0",
|
||||
"@headlessui/react": "2.2.10",
|
||||
"@hookform/resolvers": "5.4.0",
|
||||
"@hookform/resolvers": "5.7.1",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@node-rs/argon2": "2.0.2",
|
||||
"@novnc/novnc": "^1.7.0",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@radix-ui/react-avatar": "1.1.11",
|
||||
"@radix-ui/react-checkbox": "1.3.3",
|
||||
"@radix-ui/react-collapsible": "1.1.12",
|
||||
"@radix-ui/react-dialog": "1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.16",
|
||||
"@radix-ui/react-avatar": "1.2.6",
|
||||
"@radix-ui/react-checkbox": "1.3.11",
|
||||
"@radix-ui/react-collapsible": "1.1.20",
|
||||
"@radix-ui/react-dialog": "1.1.23",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.24",
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-label": "2.1.8",
|
||||
"@radix-ui/react-popover": "1.1.15",
|
||||
"@radix-ui/react-progress": "1.1.8",
|
||||
"@radix-ui/react-radio-group": "1.3.8",
|
||||
"@radix-ui/react-scroll-area": "1.2.10",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-separator": "1.1.8",
|
||||
"@radix-ui/react-slot": "1.2.4",
|
||||
"@radix-ui/react-switch": "1.2.6",
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
"@radix-ui/react-label": "2.1.15",
|
||||
"@radix-ui/react-popover": "1.1.23",
|
||||
"@radix-ui/react-progress": "1.1.16",
|
||||
"@radix-ui/react-radio-group": "1.4.7",
|
||||
"@radix-ui/react-scroll-area": "1.2.18",
|
||||
"@radix-ui/react-select": "2.3.7",
|
||||
"@radix-ui/react-separator": "1.1.15",
|
||||
"@radix-ui/react-slot": "1.3.3",
|
||||
"@radix-ui/react-switch": "1.3.7",
|
||||
"@radix-ui/react-tabs": "1.1.21",
|
||||
"@radix-ui/react-toast": "1.2.23",
|
||||
"@radix-ui/react-tooltip": "1.2.16",
|
||||
"@react-email/body": "0.3.0",
|
||||
"@react-email/components": "1.0.12",
|
||||
"@react-email/render": "2.0.8",
|
||||
"@react-email/render": "2.1.0",
|
||||
"@react-email/tailwind": "2.0.7",
|
||||
"@simplewebauthn/browser": "13.3.0",
|
||||
"@simplewebauthn/server": "13.3.1",
|
||||
"@simplewebauthn/server": "13.3.2",
|
||||
"@tailwindcss/forms": "0.5.11",
|
||||
"@tanstack/react-query": "5.100.14",
|
||||
"@tanstack/react-query": "5.101.4",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"arctic": "3.7.0",
|
||||
"axios": "1.18.0",
|
||||
"axios": "1.19.0",
|
||||
"better-sqlite3": "11.9.1",
|
||||
"canvas-confetti": "1.9.4",
|
||||
"class-variance-authority": "0.7.1",
|
||||
@@ -86,61 +86,61 @@
|
||||
"d3": "7.9.0",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "8.5.2",
|
||||
"express-rate-limit": "8.6.1",
|
||||
"glob": "13.0.6",
|
||||
"helmet": "8.2.0",
|
||||
"helmet": "8.3.0",
|
||||
"http-errors": "2.0.1",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.11.0",
|
||||
"ioredis": "6.0.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.3.0",
|
||||
"js-yaml": "5.2.3",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "1.17.0",
|
||||
"maxmind": "5.0.6",
|
||||
"lucide-react": "1.28.0",
|
||||
"maxmind": "5.0.7",
|
||||
"moment": "2.30.1",
|
||||
"next": "16.2.11",
|
||||
"next-intl": "4.13.0",
|
||||
"next": "16.3.0",
|
||||
"next-intl": "4.13.4",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"nodemailer": "9.0.1",
|
||||
"nodemailer": "9.0.3",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.21.0",
|
||||
"posthog-node": "5.35.6",
|
||||
"pg": "8.22.0",
|
||||
"posthog-node": "5.47.7",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.6",
|
||||
"react-day-picker": "9.14.0",
|
||||
"react-dom": "19.2.6",
|
||||
"react": "19.2.8",
|
||||
"react-day-picker": "10.0.1",
|
||||
"react-dom": "19.2.8",
|
||||
"react-easy-sort": "1.8.0",
|
||||
"react-hook-form": "7.76.1",
|
||||
"react-icons": "5.6.0",
|
||||
"recharts": "3.8.1",
|
||||
"react-hook-form": "7.84.0",
|
||||
"react-icons": "5.7.0",
|
||||
"recharts": "3.10.1",
|
||||
"reodotdev": "1.1.0",
|
||||
"semver": "7.8.1",
|
||||
"semver": "7.8.5",
|
||||
"sshpk": "1.18.0",
|
||||
"stripe": "22.2.0",
|
||||
"stripe": "22.4.0",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tailwind-merge": "3.6.0",
|
||||
"topojson-client": "3.1.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"use-debounce": "10.1.1",
|
||||
"uuid": "14.0.0",
|
||||
"uuid": "14.0.1",
|
||||
"vaul": "1.1.2",
|
||||
"visionscarto-world-atlas": "1.0.0",
|
||||
"winston": "3.19.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.21.0",
|
||||
"ws": "8.21.2",
|
||||
"yaml": "2.9.0",
|
||||
"yargs": "18.0.0",
|
||||
"yargs": "18.1.0",
|
||||
"zod": "4.4.3",
|
||||
"zod-validation-error": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.69.1",
|
||||
"@dotenvx/dotenvx": "2.19.2",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@react-email/ui": "^6.5.0",
|
||||
"@tailwindcss/postcss": "4.3.0",
|
||||
"@tanstack/react-query-devtools": "5.100.14",
|
||||
"@react-email/ui": "^6.9.1",
|
||||
"@tailwindcss/postcss": "4.3.3",
|
||||
"@tanstack/react-query-devtools": "5.101.4",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
"@types/cors": "2.8.19",
|
||||
@@ -151,14 +151,14 @@
|
||||
"@types/jmespath": "0.15.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/node": "25.9.1",
|
||||
"@types/nodemailer": "8.0.0",
|
||||
"@types/node": "26.1.2",
|
||||
"@types/nodemailer": "8.0.1",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/pg": "8.20.0",
|
||||
"@types/react": "19.2.15",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/sshpk": "1.17.4",
|
||||
"@types/pg": "8.20.3",
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-dom": "19.2.4",
|
||||
"@types/semver": "7.8.0",
|
||||
"@types/sshpk": "1.17.5",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/topojson-client": "3.1.5",
|
||||
"@types/ws": "8.18.1",
|
||||
@@ -166,21 +166,21 @@
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"drizzle-kit": "0.31.10",
|
||||
"esbuild": "0.28.1",
|
||||
"esbuild-node-externals": "1.22.0",
|
||||
"eslint": "10.4.0",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"postcss": "8.5.15",
|
||||
"prettier": "3.8.3",
|
||||
"react-email": "6.5.0",
|
||||
"tailwindcss": "4.3.0",
|
||||
"tsc-alias": "1.8.17",
|
||||
"tsx": "4.22.3",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.60.0"
|
||||
"esbuild-node-externals": "1.23.1",
|
||||
"eslint": "10.8.0",
|
||||
"eslint-config-next": "16.3.0",
|
||||
"postcss": "8.5.25",
|
||||
"prettier": "3.9.6",
|
||||
"react-email": "6.9.1",
|
||||
"tailwindcss": "4.3.3",
|
||||
"tsc-alias": "1.9.1",
|
||||
"tsx": "4.23.5",
|
||||
"typescript": "7.0.2",
|
||||
"typescript-eslint": "8.66.0"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "0.28.1",
|
||||
"dompurify": "3.4.0",
|
||||
"postcss": "8.5.15"
|
||||
"postcss": "8.5.25"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,6 @@ export enum ActionsEnum {
|
||||
createAlertRule = "createAlertRule",
|
||||
updateAlertRule = "updateAlertRule",
|
||||
deleteAlertRule = "deleteAlertRule",
|
||||
testAlertRule = "testAlertRule",
|
||||
listAlertRules = "listAlertRules",
|
||||
listOrgLabels = "listOrgLabels",
|
||||
createOrgLabel = "createOrgLabel",
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import logger from "@server/logger";
|
||||
import type {
|
||||
EmailAlertAction,
|
||||
TestAlertContext
|
||||
} from "@server/routers/alertRule/types";
|
||||
import { sendAlertEmail } from "./sendAlertEmail";
|
||||
import type { db, alertEmailRecipients, users, userOrgRoles } from "@server/db";
|
||||
import type { eq } from "drizzle-orm";
|
||||
|
||||
export async function processTestAlerts(context: TestAlertContext) {
|
||||
const emailActions = context.actions.filter(
|
||||
(action) => action.type === "email"
|
||||
);
|
||||
// Process email actions
|
||||
for (const action of emailActions) {
|
||||
try {
|
||||
const recipients = await resolveEmailRecipients(action);
|
||||
if (recipients.length > 0) {
|
||||
await sendAlertEmail(recipients, context);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`processAlerts: failed to send alert email`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all email addresses for a given `emailActionId`.
|
||||
*
|
||||
* Recipients may be:
|
||||
* - Direct users (by `userId`)
|
||||
* - All users in a role (by `roleId`, resolved via `userOrgRoles`)
|
||||
* - Direct external email addresses
|
||||
*/
|
||||
async function resolveEmailRecipients(
|
||||
action: EmailAlertAction
|
||||
): Promise<string[]> {
|
||||
const emailSet = new Set<string>();
|
||||
|
||||
// for (const row of rows) {
|
||||
// if (row.email) {
|
||||
// emailSet.add(row.email);
|
||||
// }
|
||||
|
||||
// if (row.userId) {
|
||||
// const [user] = await db
|
||||
// .select({ email: users.email })
|
||||
// .from(users)
|
||||
// .where(eq(users.userId, row.userId))
|
||||
// .limit(1);
|
||||
// if (user?.email) {
|
||||
// emailSet.add(user.email);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (row.roleId) {
|
||||
// // Find all users with this role via userOrgRoles
|
||||
// const roleUsers = await db
|
||||
// .select({ email: users.email })
|
||||
// .from(userOrgRoles)
|
||||
// .innerJoin(users, eq(userOrgRoles.userId, users.userId))
|
||||
// .where(eq(userOrgRoles.roleId, Number(row.roleId)));
|
||||
|
||||
// for (const u of roleUsers) {
|
||||
// if (u.email) {
|
||||
// emailSet.add(u.email);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return Array.from(emailSet);
|
||||
}
|
||||
@@ -15,5 +15,4 @@ export * from "./createAlertRule";
|
||||
export * from "./updateAlertRule";
|
||||
export * from "./deleteAlertRule";
|
||||
export * from "./listAlertRules";
|
||||
export * from "./getAlertRule";
|
||||
export * from "./testAlertRule";
|
||||
export * from "./getAlertRule";
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* 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 { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
alertRules,
|
||||
alertSites,
|
||||
alertHealthChecks,
|
||||
alertResources
|
||||
} from "@server/db";
|
||||
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 { OpenAPITags, registry } from "@server/openApi";
|
||||
import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm";
|
||||
import { ListAlertRulesResponse } from "@server/routers/alertRule/types";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
});
|
||||
|
||||
export const SITE_EVENT_TYPES = [
|
||||
"site_online",
|
||||
"site_offline",
|
||||
"site_toggle"
|
||||
] as const;
|
||||
export const HC_EVENT_TYPES = [
|
||||
"health_check_healthy",
|
||||
"health_check_unhealthy",
|
||||
"health_check_toggle"
|
||||
] as const;
|
||||
export const RESOURCE_EVENT_TYPES = [
|
||||
"resource_healthy",
|
||||
"resource_unhealthy",
|
||||
"resource_degraded",
|
||||
"resource_toggle"
|
||||
] as const;
|
||||
|
||||
const webhookActionSchema = z.strictObject({
|
||||
webhookUrl: z.string().url(),
|
||||
config: z.string().optional(),
|
||||
enabled: z.boolean().optional().default(true)
|
||||
});
|
||||
|
||||
const bodySchema = z.strictObject({
|
||||
eventType: z.enum([
|
||||
...HC_EVENT_TYPES,
|
||||
...SITE_EVENT_TYPES,
|
||||
...RESOURCE_EVENT_TYPES
|
||||
]),
|
||||
// Email recipients (flat)
|
||||
userIds: z.array(z.string().nonempty()).optional().default([]),
|
||||
roleIds: z.array(z.number()).optional().default([]),
|
||||
emails: z.array(z.email()).optional().default([]),
|
||||
// Webhook actions
|
||||
webhookActions: z.array(webhookActionSchema).optional().default([])
|
||||
});
|
||||
|
||||
export async function testAlertRule(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: process alert rule
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -808,14 +808,6 @@ authenticated.get(
|
||||
alertRule.listAlertRules
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/alert-rule/test",
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.testAlertRule),
|
||||
alertRule.testAlertRule
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/alert-rule/:alertRuleId",
|
||||
verifyValidLicense,
|
||||
|
||||
@@ -124,26 +124,3 @@ export interface AlertContext {
|
||||
/** Human-readable context data included in emails and webhook payloads */
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type EmailAlertAction = {
|
||||
type: "email";
|
||||
userIds?: string[];
|
||||
roleIds?: string[];
|
||||
emails?: string[];
|
||||
};
|
||||
|
||||
export type WebhookAlertAction = {
|
||||
type: "webhook";
|
||||
webhookUrl: string;
|
||||
enabled: boolean;
|
||||
config?: string | undefined;
|
||||
};
|
||||
|
||||
type AlertAction = EmailAlertAction | WebhookAlertAction;
|
||||
export interface TestAlertContext {
|
||||
eventType: AlertEventType;
|
||||
actions: AlertAction[];
|
||||
orgId: string;
|
||||
/** Human-readable context data included in emails and webhook payloads */
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
+1
-1
@@ -181,7 +181,7 @@ export default function NetworkingPage() {
|
||||
<SettingsSectionDescription>
|
||||
{t("remoteExitNodeNetworkingDescription")}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/remote-node/backhaul"
|
||||
href="https://docs.pangolin.net/placeholder"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
|
||||
@@ -6,9 +6,7 @@ import {
|
||||
AlertRuleSourceFields,
|
||||
AlertRuleTriggerFields
|
||||
} from "@app/components/alert-rule-editor/AlertRuleFields";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { SettingsContainer } from "@app/components/Settings";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import {
|
||||
@@ -21,7 +19,6 @@ import {
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import {
|
||||
buildFormSchema,
|
||||
@@ -30,15 +27,19 @@ import {
|
||||
type AlertRuleFormValues
|
||||
} from "@app/lib/alertRuleForm";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import type { CreateAlertRuleResponse } from "@server/routers/alertRule/types";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { Cog, Flag, Zap, ZapIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ChevronLeft, Cog, Flag, Zap } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useActionState, useMemo, useTransition, type ReactNode } from "react";
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
import { useFieldArray, useForm, type Resolver } from "react-hook-form";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
const FORM_ID = "alert-rule-form";
|
||||
@@ -114,6 +115,7 @@ export default function AlertRuleGraphEditor({
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const schema = useMemo(() => buildFormSchema(t), [t]);
|
||||
const form = useForm<AlertRuleFormValues>({
|
||||
resolver: zodResolver(schema) as Resolver<AlertRuleFormValues>,
|
||||
@@ -125,22 +127,8 @@ export default function AlertRuleGraphEditor({
|
||||
name: "actions"
|
||||
});
|
||||
|
||||
const saveAlert = async () => {
|
||||
const isValid = await form.trigger();
|
||||
if (!isValid) {
|
||||
const values = form.getValues();
|
||||
if (values.actions.length === 0) {
|
||||
toast({
|
||||
variant: "warning",
|
||||
title: t("alertingNoActionsTitle"),
|
||||
description: t("alertingNoActionsSaveDescription")
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const values = form.getValues();
|
||||
|
||||
const onSubmit = form.handleSubmit(async (values) => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const payload = formValuesToApiPayload(values);
|
||||
if (isNew) {
|
||||
@@ -170,37 +158,14 @@ export default function AlertRuleGraphEditor({
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
// const submit = form.handleSubmit(async (values) => {
|
||||
|
||||
// });
|
||||
|
||||
// await submit();
|
||||
};
|
||||
|
||||
const testAlert = async () => {
|
||||
const isValid = await form.trigger();
|
||||
if (!isValid) {
|
||||
const values = form.getValues();
|
||||
if (values.actions.length === 0) {
|
||||
toast({
|
||||
variant: "warning",
|
||||
title: t("alertingNoActionsTitle"),
|
||||
description: t("alertingNoActionsTestDescription")
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const values = form.getValues();
|
||||
};
|
||||
|
||||
const [, formAction, isSaving] = useActionState(saveAlert, null);
|
||||
const [isTestingAlert, startTransition] = useTransition();
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form id={FORM_ID} action={formAction}>
|
||||
<form id={FORM_ID} onSubmit={onSubmit}>
|
||||
<SettingsContainer>
|
||||
<PaidFeaturesAlert tiers={tierMatrix.alertingRules} />
|
||||
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8 items-start">
|
||||
@@ -298,29 +263,14 @@ export default function AlertRuleGraphEditor({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col items-center w-full gap-3">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isSaving}
|
||||
loading={isSaving}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full gap-1.5"
|
||||
onClick={() =>
|
||||
startTransition(testAlert)
|
||||
}
|
||||
loading={isTestingAlert}
|
||||
>
|
||||
{t("alertingTestRule")}
|
||||
<ZapIcon className="size-3.5 flex-none" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isSaving}
|
||||
loading={isSaving}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</fieldset>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user