mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-01 21:46:38 +00:00
Create hcs freely
This commit is contained in:
@@ -1426,6 +1426,23 @@
|
|||||||
"alertingNodeRoleSource": "Source",
|
"alertingNodeRoleSource": "Source",
|
||||||
"alertingNodeRoleTrigger": "Trigger",
|
"alertingNodeRoleTrigger": "Trigger",
|
||||||
"alertingNodeRoleAction": "Action",
|
"alertingNodeRoleAction": "Action",
|
||||||
|
"alertingTabRules": "Alert Rules",
|
||||||
|
"alertingTabHealthChecks": "Health Checks",
|
||||||
|
"standaloneHcTableTitle": "Health Checks",
|
||||||
|
"standaloneHcSearchPlaceholder": "Search health checks…",
|
||||||
|
"standaloneHcAddButton": "Create Health Check",
|
||||||
|
"standaloneHcCreateTitle": "Create Health Check",
|
||||||
|
"standaloneHcEditTitle": "Edit Health Check",
|
||||||
|
"standaloneHcDescription": "Configure a HTTP or TCP health check for use in alert rules.",
|
||||||
|
"standaloneHcNameLabel": "Name",
|
||||||
|
"standaloneHcNamePlaceholder": "My HTTP Monitor",
|
||||||
|
"standaloneHcDeleteTitle": "Delete health check",
|
||||||
|
"standaloneHcDeleteQuestion": "Delete this health check? This cannot be undone.",
|
||||||
|
"standaloneHcDeleted": "Health check deleted",
|
||||||
|
"standaloneHcSaved": "Health check saved",
|
||||||
|
"standaloneHcColumnHealth": "Health",
|
||||||
|
"standaloneHcColumnMode": "Mode",
|
||||||
|
"standaloneHcColumnTarget": "Target",
|
||||||
"blueprints": "Blueprints",
|
"blueprints": "Blueprints",
|
||||||
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
||||||
"blueprintAdd": "Add Blueprint",
|
"blueprintAdd": "Add Blueprint",
|
||||||
|
|||||||
@@ -145,12 +145,15 @@ export enum ActionsEnum {
|
|||||||
updateEventStreamingDestination = "updateEventStreamingDestination",
|
updateEventStreamingDestination = "updateEventStreamingDestination",
|
||||||
deleteEventStreamingDestination = "deleteEventStreamingDestination",
|
deleteEventStreamingDestination = "deleteEventStreamingDestination",
|
||||||
listEventStreamingDestinations = "listEventStreamingDestinations",
|
listEventStreamingDestinations = "listEventStreamingDestinations",
|
||||||
listHealthChecks = "listHealthChecks",
|
|
||||||
createAlertRule = "createAlertRule",
|
createAlertRule = "createAlertRule",
|
||||||
updateAlertRule = "updateAlertRule",
|
updateAlertRule = "updateAlertRule",
|
||||||
deleteAlertRule = "deleteAlertRule",
|
deleteAlertRule = "deleteAlertRule",
|
||||||
listAlertRules = "listAlertRules",
|
listAlertRules = "listAlertRules",
|
||||||
getAlertRule = "getAlertRule"
|
getAlertRule = "getAlertRule",
|
||||||
|
createHealthCheck = "createHealthCheck",
|
||||||
|
updateHealthCheck = "updateHealthCheck",
|
||||||
|
deleteHealthCheck = "deleteHealthCheck",
|
||||||
|
listHealthChecks = "listHealthChecks"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export async function updateProxyResources(
|
|||||||
const [newHealthcheck] = await trx
|
const [newHealthcheck] = await trx
|
||||||
.insert(targetHealthCheck)
|
.insert(targetHealthCheck)
|
||||||
.values({
|
.values({
|
||||||
|
name: `${targetData.hostname}:${targetData.port}`,
|
||||||
targetId: newTarget.targetId,
|
targetId: newTarget.targetId,
|
||||||
hcEnabled: healthcheckData?.enabled || false,
|
hcEnabled: healthcheckData?.enabled || false,
|
||||||
hcPath: healthcheckData?.path,
|
hcPath: healthcheckData?.path,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import * as user from "#private/routers/user";
|
|||||||
import * as siteProvisioning from "#private/routers/siteProvisioning";
|
import * as siteProvisioning from "#private/routers/siteProvisioning";
|
||||||
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
||||||
import * as alertRule from "#private/routers/alertRule";
|
import * as alertRule from "#private/routers/alertRule";
|
||||||
|
import * as healthChecks from "#private/routers/healthChecks";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
@@ -695,3 +696,38 @@ authenticated.get(
|
|||||||
verifyUserHasAction(ActionsEnum.getAlertRule),
|
verifyUserHasAction(ActionsEnum.getAlertRule),
|
||||||
alertRule.getAlertRule
|
alertRule.getAlertRule
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/health-checks",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.listHealthChecks),
|
||||||
|
healthChecks.listHealthChecks
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/health-check",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.createHealthCheck),
|
||||||
|
logActionAudit(ActionsEnum.createHealthCheck),
|
||||||
|
healthChecks.createHealthCheck
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/health-check/:healthCheckId",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.updateHealthCheck),
|
||||||
|
logActionAudit(ActionsEnum.updateHealthCheck),
|
||||||
|
healthChecks.updateHealthCheck
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/health-check/:healthCheckId",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.deleteHealthCheck),
|
||||||
|
logActionAudit(ActionsEnum.deleteHealthCheck),
|
||||||
|
healthChecks.deleteHealthCheck
|
||||||
|
);
|
||||||
|
|||||||
158
server/private/routers/healthChecks/createHealthCheck.ts
Normal file
158
server/private/routers/healthChecks/createHealthCheck.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* 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, targetHealthCheck } 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";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodySchema = z.strictObject({
|
||||||
|
name: z.string().nonempty(),
|
||||||
|
hcEnabled: z.boolean().default(false),
|
||||||
|
hcMode: z.string().default("http"),
|
||||||
|
hcHostname: z.string().optional(),
|
||||||
|
hcPort: z.number().int().min(1).max(65535).optional(),
|
||||||
|
hcPath: z.string().optional(),
|
||||||
|
hcScheme: z.string().optional(),
|
||||||
|
hcMethod: z.string().default("GET"),
|
||||||
|
hcInterval: z.number().int().positive().default(30),
|
||||||
|
hcUnhealthyInterval: z.number().int().positive().default(30),
|
||||||
|
hcTimeout: z.number().int().positive().default(5),
|
||||||
|
hcHeaders: z.string().optional().nullable(),
|
||||||
|
hcFollowRedirects: z.boolean().default(true),
|
||||||
|
hcStatus: z.number().int().optional().nullable(),
|
||||||
|
hcTlsServerName: z.string().optional(),
|
||||||
|
hcHealthyThreshold: z.number().int().positive().default(1),
|
||||||
|
hcUnhealthyThreshold: z.number().int().positive().default(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateHealthCheckResponse = {
|
||||||
|
targetHealthCheckId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/org/{orgId}/health-check",
|
||||||
|
description: "Create a health check for a specific organization.",
|
||||||
|
tags: [OpenAPITags.Org],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createHealthCheck(
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
hcEnabled,
|
||||||
|
hcMode,
|
||||||
|
hcHostname,
|
||||||
|
hcPort,
|
||||||
|
hcPath,
|
||||||
|
hcScheme,
|
||||||
|
hcMethod,
|
||||||
|
hcInterval,
|
||||||
|
hcUnhealthyInterval,
|
||||||
|
hcTimeout,
|
||||||
|
hcHeaders,
|
||||||
|
hcFollowRedirects,
|
||||||
|
hcStatus,
|
||||||
|
hcTlsServerName,
|
||||||
|
hcHealthyThreshold,
|
||||||
|
hcUnhealthyThreshold
|
||||||
|
} = parsedBody.data;
|
||||||
|
|
||||||
|
const [record] = await db
|
||||||
|
.insert(targetHealthCheck)
|
||||||
|
.values({
|
||||||
|
targetId: null,
|
||||||
|
orgId,
|
||||||
|
name,
|
||||||
|
hcEnabled,
|
||||||
|
hcMode,
|
||||||
|
hcHostname: hcHostname ?? null,
|
||||||
|
hcPort: hcPort ?? null,
|
||||||
|
hcPath: hcPath ?? null,
|
||||||
|
hcScheme: hcScheme ?? null,
|
||||||
|
hcMethod,
|
||||||
|
hcInterval,
|
||||||
|
hcUnhealthyInterval,
|
||||||
|
hcTimeout,
|
||||||
|
hcHeaders: hcHeaders ?? null,
|
||||||
|
hcFollowRedirects,
|
||||||
|
hcStatus: hcStatus ?? null,
|
||||||
|
hcTlsServerName: hcTlsServerName ?? null,
|
||||||
|
hcHealthyThreshold,
|
||||||
|
hcUnhealthyThreshold
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return response<CreateHealthCheckResponse>(res, {
|
||||||
|
data: {
|
||||||
|
targetHealthCheckId: record.targetHealthCheckId
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Standalone health check created successfully",
|
||||||
|
status: HttpCode.CREATED
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
107
server/private/routers/healthChecks/deleteHealthCheck.ts
Normal file
107
server/private/routers/healthChecks/deleteHealthCheck.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* 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, targetHealthCheck } 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, eq, isNull } from "drizzle-orm";
|
||||||
|
|
||||||
|
const paramsSchema = z
|
||||||
|
.object({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
healthCheckId: z
|
||||||
|
.string()
|
||||||
|
.transform(Number)
|
||||||
|
.pipe(z.number().int().positive())
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "delete",
|
||||||
|
path: "/org/{orgId}/health-check/{healthCheckId}",
|
||||||
|
description: "Delete a health check for a specific organization.",
|
||||||
|
tags: [OpenAPITags.Org],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function deleteHealthCheck(
|
||||||
|
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, healthCheckId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(targetHealthCheck)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(targetHealthCheck.targetHealthCheckId, healthCheckId),
|
||||||
|
eq(targetHealthCheck.orgId, orgId),
|
||||||
|
isNull(targetHealthCheck.targetId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"Standalone health check not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(targetHealthCheck)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(targetHealthCheck.targetHealthCheckId, healthCheckId),
|
||||||
|
eq(targetHealthCheck.orgId, orgId),
|
||||||
|
isNull(targetHealthCheck.targetId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response<null>(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Standalone health check deleted successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
server/private/routers/healthChecks/index.ts
Normal file
17
server/private/routers/healthChecks/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./listHealthChecks";
|
||||||
|
export * from "./createHealthCheck";
|
||||||
|
export * from "./updateHealthCheck";
|
||||||
|
export * from "./deleteHealthCheck";
|
||||||
@@ -1,19 +1,33 @@
|
|||||||
import { db, targetHealthCheck, targets, resources } from "@server/db";
|
/*
|
||||||
|
* 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 { db, targetHealthCheck } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { eq, sql, inArray } from "drizzle-orm";
|
import { and, eq, isNull, sql } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
|
||||||
|
|
||||||
const listHealthChecksParamsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
orgId: z.string().nonempty()
|
orgId: z.string().nonempty()
|
||||||
});
|
});
|
||||||
|
|
||||||
const listHealthChecksSchema = z.object({
|
const querySchema = z.object({
|
||||||
limit: z
|
limit: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -28,29 +42,14 @@ const listHealthChecksSchema = z.object({
|
|||||||
.pipe(z.int().nonnegative())
|
.pipe(z.int().nonnegative())
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ListHealthChecksResponse = {
|
|
||||||
healthChecks: {
|
|
||||||
targetHealthCheckId: number;
|
|
||||||
resourceId: number;
|
|
||||||
resourceName: string;
|
|
||||||
hcEnabled: boolean;
|
|
||||||
hcHealth: "unknown" | "healthy" | "unhealthy";
|
|
||||||
}[];
|
|
||||||
pagination: {
|
|
||||||
total: number;
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/health-checks",
|
path: "/org/{orgId}/health-checks",
|
||||||
description: "List health checks for all resources in an organization.",
|
description: "List health checks for an organization.",
|
||||||
tags: [OpenAPITags.Org, OpenAPITags.PublicResource],
|
tags: [OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
params: listHealthChecksParamsSchema,
|
params: paramsSchema,
|
||||||
query: listHealthChecksSchema
|
query: querySchema
|
||||||
},
|
},
|
||||||
responses: {}
|
responses: {}
|
||||||
});
|
});
|
||||||
@@ -61,62 +60,71 @@ export async function listHealthChecks(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedQuery = listHealthChecksSchema.safeParse(req.query);
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
if (!parsedQuery.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedQuery.error)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { limit, offset } = parsedQuery.data;
|
|
||||||
|
|
||||||
const parsedParams = listHealthChecksParamsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
if (!parsedParams.success) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
fromError(parsedParams.error)
|
fromError(parsedParams.error).toString()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
|
const parsedQuery = querySchema.safeParse(req.query);
|
||||||
|
if (!parsedQuery.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedQuery.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { limit, offset } = parsedQuery.data;
|
||||||
|
|
||||||
|
const whereClause = and(
|
||||||
|
eq(targetHealthCheck.orgId, orgId),
|
||||||
|
isNull(targetHealthCheck.targetId)
|
||||||
|
);
|
||||||
|
|
||||||
const list = await db
|
const list = await db
|
||||||
.select({
|
.select()
|
||||||
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
|
||||||
resourceId: resources.resourceId,
|
|
||||||
resourceName: resources.name,
|
|
||||||
hcEnabled: targetHealthCheck.hcEnabled,
|
|
||||||
hcHealth: targetHealthCheck.hcHealth
|
|
||||||
})
|
|
||||||
.from(targetHealthCheck)
|
.from(targetHealthCheck)
|
||||||
.innerJoin(targets, eq(targets.targetId, targetHealthCheck.targetId))
|
.where(whereClause)
|
||||||
.innerJoin(resources, eq(resources.resourceId, targets.resourceId))
|
.orderBy(sql`${targetHealthCheck.targetHealthCheckId} DESC`)
|
||||||
.where(eq(resources.orgId, orgId))
|
|
||||||
.orderBy(sql`${resources.name} ASC`)
|
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
|
|
||||||
const [{ count }] = await db
|
const [{ count }] = await db
|
||||||
.select({ count: sql<number>`count(*)` })
|
.select({ count: sql<number>`count(*)` })
|
||||||
.from(targetHealthCheck)
|
.from(targetHealthCheck)
|
||||||
.innerJoin(targets, eq(targets.targetId, targetHealthCheck.targetId))
|
.where(whereClause);
|
||||||
.innerJoin(resources, eq(resources.resourceId, targets.resourceId))
|
|
||||||
.where(eq(resources.orgId, orgId));
|
|
||||||
|
|
||||||
return response<ListHealthChecksResponse>(res, {
|
return response<ListHealthChecksResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
healthChecks: list.map((row) => ({
|
healthChecks: list.map((row) => ({
|
||||||
targetHealthCheckId: row.targetHealthCheckId,
|
targetHealthCheckId: row.targetHealthCheckId,
|
||||||
resourceId: row.resourceId,
|
name: row.name ?? "",
|
||||||
resourceName: row.resourceName,
|
|
||||||
hcEnabled: row.hcEnabled,
|
hcEnabled: row.hcEnabled,
|
||||||
hcHealth: (row.hcHealth ?? "unknown") as
|
hcHealth: (row.hcHealth ?? "unknown") as
|
||||||
| "unknown"
|
| "unknown"
|
||||||
| "healthy"
|
| "healthy"
|
||||||
| "unhealthy"
|
| "unhealthy",
|
||||||
|
hcMode: row.hcMode ?? null,
|
||||||
|
hcHostname: row.hcHostname ?? null,
|
||||||
|
hcPort: row.hcPort ?? null,
|
||||||
|
hcPath: row.hcPath ?? null,
|
||||||
|
hcScheme: row.hcScheme ?? null,
|
||||||
|
hcMethod: row.hcMethod ?? null,
|
||||||
|
hcInterval: row.hcInterval ?? null,
|
||||||
|
hcUnhealthyInterval: row.hcUnhealthyInterval ?? null,
|
||||||
|
hcTimeout: row.hcTimeout ?? null,
|
||||||
|
hcHeaders: row.hcHeaders ?? null,
|
||||||
|
hcFollowRedirects: row.hcFollowRedirects ?? null,
|
||||||
|
hcStatus: row.hcStatus ?? null,
|
||||||
|
hcTlsServerName: row.hcTlsServerName ?? null,
|
||||||
|
hcHealthyThreshold: row.hcHealthyThreshold ?? null,
|
||||||
|
hcUnhealthyThreshold: row.hcUnhealthyThreshold ?? null
|
||||||
})),
|
})),
|
||||||
pagination: {
|
pagination: {
|
||||||
total: count,
|
total: count,
|
||||||
@@ -126,7 +134,7 @@ export async function listHealthChecks(
|
|||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Health checks retrieved successfully",
|
message: "Standalone health checks retrieved successfully",
|
||||||
status: HttpCode.OK
|
status: HttpCode.OK
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -135,4 +143,4 @@ export async function listHealthChecks(
|
|||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
239
server/private/routers/healthChecks/updateHealthCheck.ts
Normal file
239
server/private/routers/healthChecks/updateHealthCheck.ts
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
* 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, targetHealthCheck } 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, eq, isNull } from "drizzle-orm";
|
||||||
|
|
||||||
|
const paramsSchema = z
|
||||||
|
.object({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
healthCheckId: z
|
||||||
|
.string()
|
||||||
|
.transform(Number)
|
||||||
|
.pipe(z.number().int().positive())
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const bodySchema = z.strictObject({
|
||||||
|
name: z.string().nonempty().optional(),
|
||||||
|
hcEnabled: z.boolean().optional(),
|
||||||
|
hcMode: z.string().optional(),
|
||||||
|
hcHostname: z.string().optional(),
|
||||||
|
hcPort: z.number().int().min(1).max(65535).optional(),
|
||||||
|
hcPath: z.string().optional(),
|
||||||
|
hcScheme: z.string().optional(),
|
||||||
|
hcMethod: z.string().optional(),
|
||||||
|
hcInterval: z.number().int().positive().optional(),
|
||||||
|
hcUnhealthyInterval: z.number().int().positive().optional(),
|
||||||
|
hcTimeout: z.number().int().positive().optional(),
|
||||||
|
hcHeaders: z.string().optional().nullable(),
|
||||||
|
hcFollowRedirects: z.boolean().optional(),
|
||||||
|
hcStatus: z.number().int().optional().nullable(),
|
||||||
|
hcTlsServerName: z.string().optional(),
|
||||||
|
hcHealthyThreshold: z.number().int().positive().optional(),
|
||||||
|
hcUnhealthyThreshold: z.number().int().positive().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateHealthCheckResponse = {
|
||||||
|
targetHealthCheckId: number;
|
||||||
|
name: string | null;
|
||||||
|
hcEnabled: boolean;
|
||||||
|
hcHealth: string | null;
|
||||||
|
hcMode: string | null;
|
||||||
|
hcHostname: string | null;
|
||||||
|
hcPort: number | null;
|
||||||
|
hcPath: string | null;
|
||||||
|
hcScheme: string | null;
|
||||||
|
hcMethod: string | null;
|
||||||
|
hcInterval: number | null;
|
||||||
|
hcUnhealthyInterval: number | null;
|
||||||
|
hcTimeout: number | null;
|
||||||
|
hcHeaders: string | null;
|
||||||
|
hcFollowRedirects: boolean | null;
|
||||||
|
hcStatus: number | null;
|
||||||
|
hcTlsServerName: string | null;
|
||||||
|
hcHealthyThreshold: number | null;
|
||||||
|
hcUnhealthyThreshold: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/org/{orgId}/health-check/{healthCheckId}",
|
||||||
|
description: "Update a health check for a specific organization.",
|
||||||
|
tags: [OpenAPITags.Org],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function updateHealthCheck(
|
||||||
|
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, healthCheckId } = parsedParams.data;
|
||||||
|
|
||||||
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(targetHealthCheck)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
targetHealthCheck.targetHealthCheckId,
|
||||||
|
healthCheckId
|
||||||
|
),
|
||||||
|
eq(targetHealthCheck.orgId, orgId),
|
||||||
|
isNull(targetHealthCheck.targetId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"Standalone health check not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
hcEnabled,
|
||||||
|
hcMode,
|
||||||
|
hcHostname,
|
||||||
|
hcPort,
|
||||||
|
hcPath,
|
||||||
|
hcScheme,
|
||||||
|
hcMethod,
|
||||||
|
hcInterval,
|
||||||
|
hcUnhealthyInterval,
|
||||||
|
hcTimeout,
|
||||||
|
hcHeaders,
|
||||||
|
hcFollowRedirects,
|
||||||
|
hcStatus,
|
||||||
|
hcTlsServerName,
|
||||||
|
hcHealthyThreshold,
|
||||||
|
hcUnhealthyThreshold
|
||||||
|
} = parsedBody.data;
|
||||||
|
|
||||||
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (name !== undefined) updateData.name = name;
|
||||||
|
if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled;
|
||||||
|
if (hcMode !== undefined) updateData.hcMode = hcMode;
|
||||||
|
if (hcHostname !== undefined) updateData.hcHostname = hcHostname;
|
||||||
|
if (hcPort !== undefined) updateData.hcPort = hcPort;
|
||||||
|
if (hcPath !== undefined) updateData.hcPath = hcPath;
|
||||||
|
if (hcScheme !== undefined) updateData.hcScheme = hcScheme;
|
||||||
|
if (hcMethod !== undefined) updateData.hcMethod = hcMethod;
|
||||||
|
if (hcInterval !== undefined) updateData.hcInterval = hcInterval;
|
||||||
|
if (hcUnhealthyInterval !== undefined)
|
||||||
|
updateData.hcUnhealthyInterval = hcUnhealthyInterval;
|
||||||
|
if (hcTimeout !== undefined) updateData.hcTimeout = hcTimeout;
|
||||||
|
if (hcHeaders !== undefined) updateData.hcHeaders = hcHeaders;
|
||||||
|
if (hcFollowRedirects !== undefined)
|
||||||
|
updateData.hcFollowRedirects = hcFollowRedirects;
|
||||||
|
if (hcStatus !== undefined) updateData.hcStatus = hcStatus;
|
||||||
|
if (hcTlsServerName !== undefined)
|
||||||
|
updateData.hcTlsServerName = hcTlsServerName;
|
||||||
|
if (hcHealthyThreshold !== undefined)
|
||||||
|
updateData.hcHealthyThreshold = hcHealthyThreshold;
|
||||||
|
if (hcUnhealthyThreshold !== undefined)
|
||||||
|
updateData.hcUnhealthyThreshold = hcUnhealthyThreshold;
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(targetHealthCheck)
|
||||||
|
.set(updateData)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
targetHealthCheck.targetHealthCheckId,
|
||||||
|
healthCheckId
|
||||||
|
),
|
||||||
|
eq(targetHealthCheck.orgId, orgId),
|
||||||
|
isNull(targetHealthCheck.targetId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return response<UpdateHealthCheckResponse>(res, {
|
||||||
|
data: {
|
||||||
|
targetHealthCheckId: updated.targetHealthCheckId,
|
||||||
|
name: updated.name ?? null,
|
||||||
|
hcEnabled: updated.hcEnabled,
|
||||||
|
hcHealth: updated.hcHealth ?? null,
|
||||||
|
hcMode: updated.hcMode ?? null,
|
||||||
|
hcHostname: updated.hcHostname ?? null,
|
||||||
|
hcPort: updated.hcPort ?? null,
|
||||||
|
hcPath: updated.hcPath ?? null,
|
||||||
|
hcScheme: updated.hcScheme ?? null,
|
||||||
|
hcMethod: updated.hcMethod ?? null,
|
||||||
|
hcInterval: updated.hcInterval ?? null,
|
||||||
|
hcUnhealthyInterval: updated.hcUnhealthyInterval ?? null,
|
||||||
|
hcTimeout: updated.hcTimeout ?? null,
|
||||||
|
hcHeaders: updated.hcHeaders ?? null,
|
||||||
|
hcFollowRedirects: updated.hcFollowRedirects ?? null,
|
||||||
|
hcStatus: updated.hcStatus ?? null,
|
||||||
|
hcTlsServerName: updated.hcTlsServerName ?? null,
|
||||||
|
hcHealthyThreshold: updated.hcHealthyThreshold ?? null,
|
||||||
|
hcUnhealthyThreshold: updated.hcUnhealthyThreshold ?? null
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Standalone health check updated successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -427,13 +427,6 @@ authenticated.get(
|
|||||||
resource.listResources
|
resource.listResources
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/health-checks",
|
|
||||||
verifyOrgAccess,
|
|
||||||
verifyUserHasAction(ActionsEnum.listHealthChecks),
|
|
||||||
resource.listHealthChecks
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/resource-names",
|
"/org/:orgId/resource-names",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
|
|||||||
28
server/routers/healthChecks/types.ts
Normal file
28
server/routers/healthChecks/types.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export type ListHealthChecksResponse = {
|
||||||
|
healthChecks: {
|
||||||
|
targetHealthCheckId: number;
|
||||||
|
name: string;
|
||||||
|
hcEnabled: boolean;
|
||||||
|
hcHealth: "unknown" | "healthy" | "unhealthy";
|
||||||
|
hcMode: string | null;
|
||||||
|
hcHostname: string | null;
|
||||||
|
hcPort: number | null;
|
||||||
|
hcPath: string | null;
|
||||||
|
hcScheme: string | null;
|
||||||
|
hcMethod: string | null;
|
||||||
|
hcInterval: number | null;
|
||||||
|
hcUnhealthyInterval: number | null;
|
||||||
|
hcTimeout: number | null;
|
||||||
|
hcHeaders: string | null;
|
||||||
|
hcFollowRedirects: boolean | null;
|
||||||
|
hcStatus: number | null;
|
||||||
|
hcTlsServerName: string | null;
|
||||||
|
hcHealthyThreshold: number | null;
|
||||||
|
hcUnhealthyThreshold: number | null;
|
||||||
|
}[];
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -32,4 +32,3 @@ export * from "./addUserToResource";
|
|||||||
export * from "./removeUserFromResource";
|
export * from "./removeUserFromResource";
|
||||||
export * from "./listAllResourceNames";
|
export * from "./listAllResourceNames";
|
||||||
export * from "./removeEmailFromResourceWhitelist";
|
export * from "./removeEmailFromResourceWhitelist";
|
||||||
export * from "./listHealthChecks";
|
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ export async function createTarget(
|
|||||||
healthCheck = await db
|
healthCheck = await db
|
||||||
.insert(targetHealthCheck)
|
.insert(targetHealthCheck)
|
||||||
.values({
|
.values({
|
||||||
|
name: `${targetData.ip}:${targetData.port}`,
|
||||||
targetId: newTarget[0].targetId,
|
targetId: newTarget[0].targetId,
|
||||||
hcEnabled: targetData.hcEnabled ?? false,
|
hcEnabled: targetData.hcEnabled ?? false,
|
||||||
hcPath: targetData.hcPath ?? null,
|
hcPath: targetData.hcPath ?? null,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import AlertingRulesTable from "@app/components/AlertingRulesTable";
|
import AlertingRulesTable from "@app/components/AlertingRulesTable";
|
||||||
|
import StandaloneHealthChecksTable from "@app/components/StandaloneHealthChecksTable";
|
||||||
|
import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
type AlertingPageProps = {
|
type AlertingPageProps = {
|
||||||
@@ -12,13 +14,21 @@ export default async function AlertingPage(props: AlertingPageProps) {
|
|||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
|
const tabs: TabItem[] = [
|
||||||
|
{ title: t("alertingTabRules"), href: "" },
|
||||||
|
{ title: t("alertingTabHealthChecks"), href: "" }
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title={t("alertingTitle")}
|
title={t("alertingTitle")}
|
||||||
description={t("alertingDescription")}
|
description={t("alertingDescription")}
|
||||||
/>
|
/>
|
||||||
<AlertingRulesTable orgId={params.orgId} />
|
<HorizontalTabs items={tabs} clientSide>
|
||||||
|
<AlertingRulesTable orgId={params.orgId} />
|
||||||
|
<StandaloneHealthChecksTable orgId={params.orgId} />
|
||||||
|
</HorizontalTabs>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
856
src/components/StandaloneHealthCheckCredenza.tsx
Normal file
856
src/components/StandaloneHealthCheckCredenza.tsx
Normal file
@@ -0,0 +1,856 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { HeadersInput } from "@app/components/HeadersInput";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Credenza,
|
||||||
|
CredenzaBody,
|
||||||
|
CredenzaClose,
|
||||||
|
CredenzaContent,
|
||||||
|
CredenzaDescription,
|
||||||
|
CredenzaFooter,
|
||||||
|
CredenzaHeader,
|
||||||
|
CredenzaTitle
|
||||||
|
} from "@/components/Credenza";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export type HealthCheckRow = {
|
||||||
|
targetHealthCheckId: number;
|
||||||
|
name: string;
|
||||||
|
hcEnabled: boolean;
|
||||||
|
hcHealth: "unknown" | "healthy" | "unhealthy";
|
||||||
|
hcMode: string | null;
|
||||||
|
hcHostname: string | null;
|
||||||
|
hcPort: number | null;
|
||||||
|
hcPath: string | null;
|
||||||
|
hcScheme: string | null;
|
||||||
|
hcMethod: string | null;
|
||||||
|
hcInterval: number | null;
|
||||||
|
hcUnhealthyInterval: number | null;
|
||||||
|
hcTimeout: number | null;
|
||||||
|
hcHeaders: string | null;
|
||||||
|
hcFollowRedirects: boolean | null;
|
||||||
|
hcStatus: number | null;
|
||||||
|
hcTlsServerName: string | null;
|
||||||
|
hcHealthyThreshold: number | null;
|
||||||
|
hcUnhealthyThreshold: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StandaloneHealthCheckCredenzaProps = {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (v: boolean) => void;
|
||||||
|
orgId: string;
|
||||||
|
initialValues?: HealthCheckRow | null;
|
||||||
|
onSaved: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_VALUES = {
|
||||||
|
name: "",
|
||||||
|
hcEnabled: true,
|
||||||
|
hcMode: "http",
|
||||||
|
hcScheme: "https",
|
||||||
|
hcMethod: "GET",
|
||||||
|
hcHostname: "",
|
||||||
|
hcPort: "",
|
||||||
|
hcPath: "/",
|
||||||
|
hcInterval: 30,
|
||||||
|
hcUnhealthyInterval: 30,
|
||||||
|
hcTimeout: 5,
|
||||||
|
hcHealthyThreshold: 1,
|
||||||
|
hcUnhealthyThreshold: 1,
|
||||||
|
hcFollowRedirects: true,
|
||||||
|
hcTlsServerName: "",
|
||||||
|
hcStatus: null as number | null,
|
||||||
|
hcHeaders: [] as { name: string; value: string }[]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function StandaloneHealthCheckCredenza({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
orgId,
|
||||||
|
initialValues,
|
||||||
|
onSaved
|
||||||
|
}: StandaloneHealthCheckCredenzaProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const api = createApiClient(useEnvContext());
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const healthCheckSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1, { message: t("standaloneHcNameLabel") }),
|
||||||
|
hcEnabled: z.boolean(),
|
||||||
|
hcPath: z.string().optional(),
|
||||||
|
hcMethod: z.string().optional(),
|
||||||
|
hcInterval: z
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.min(5, { message: t("healthCheckIntervalMin") }),
|
||||||
|
hcTimeout: z
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.min(1, { message: t("healthCheckTimeoutMin") }),
|
||||||
|
hcStatus: z.int().positive().min(100).optional().nullable(),
|
||||||
|
hcHeaders: z
|
||||||
|
.array(z.object({ name: z.string(), value: z.string() }))
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
hcScheme: z.string().optional(),
|
||||||
|
hcHostname: z.string(),
|
||||||
|
hcPort: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: t("healthCheckPortInvalid") })
|
||||||
|
.refine(
|
||||||
|
(val) => {
|
||||||
|
const port = parseInt(val);
|
||||||
|
return port > 0 && port <= 65535;
|
||||||
|
},
|
||||||
|
{ message: t("healthCheckPortInvalid") }
|
||||||
|
),
|
||||||
|
hcFollowRedirects: z.boolean(),
|
||||||
|
hcMode: z.string(),
|
||||||
|
hcUnhealthyInterval: z.int().positive().min(5),
|
||||||
|
hcTlsServerName: z.string(),
|
||||||
|
hcHealthyThreshold: z
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.min(1, { message: t("healthCheckHealthyThresholdMin") }),
|
||||||
|
hcUnhealthyThreshold: z
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.min(1, { message: t("healthCheckUnhealthyThresholdMin") })
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.hcMode !== "tcp") {
|
||||||
|
if (!data.hcPath || data.hcPath.length < 1) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t("healthCheckPathRequired"),
|
||||||
|
path: ["hcPath"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.hcMethod || data.hcMethod.length < 1) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t("healthCheckMethodRequired"),
|
||||||
|
path: ["hcMethod"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof healthCheckSchema>;
|
||||||
|
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(healthCheckSchema),
|
||||||
|
defaultValues: DEFAULT_VALUES
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
if (initialValues) {
|
||||||
|
let parsedHeaders: { name: string; value: string }[] = [];
|
||||||
|
if (initialValues.hcHeaders) {
|
||||||
|
try {
|
||||||
|
parsedHeaders = JSON.parse(initialValues.hcHeaders);
|
||||||
|
} catch {
|
||||||
|
parsedHeaders = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.reset({
|
||||||
|
name: initialValues.name,
|
||||||
|
hcEnabled: initialValues.hcEnabled,
|
||||||
|
hcMode: initialValues.hcMode ?? "http",
|
||||||
|
hcScheme: initialValues.hcScheme ?? "https",
|
||||||
|
hcMethod: initialValues.hcMethod ?? "GET",
|
||||||
|
hcHostname: initialValues.hcHostname ?? "",
|
||||||
|
hcPort: initialValues.hcPort
|
||||||
|
? initialValues.hcPort.toString()
|
||||||
|
: "",
|
||||||
|
hcPath: initialValues.hcPath ?? "/",
|
||||||
|
hcInterval: initialValues.hcInterval ?? 30,
|
||||||
|
hcUnhealthyInterval: initialValues.hcUnhealthyInterval ?? 30,
|
||||||
|
hcTimeout: initialValues.hcTimeout ?? 5,
|
||||||
|
hcHealthyThreshold: initialValues.hcHealthyThreshold ?? 1,
|
||||||
|
hcUnhealthyThreshold: initialValues.hcUnhealthyThreshold ?? 1,
|
||||||
|
hcFollowRedirects: initialValues.hcFollowRedirects ?? true,
|
||||||
|
hcTlsServerName: initialValues.hcTlsServerName ?? "",
|
||||||
|
hcStatus: initialValues.hcStatus ?? null,
|
||||||
|
hcHeaders: parsedHeaders
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.reset(DEFAULT_VALUES);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const watchedEnabled = form.watch("hcEnabled");
|
||||||
|
const watchedMode = form.watch("hcMode");
|
||||||
|
|
||||||
|
const onSubmit = async (values: FormValues) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
name: values.name,
|
||||||
|
hcEnabled: values.hcEnabled,
|
||||||
|
hcMode: values.hcMode,
|
||||||
|
hcScheme: values.hcScheme,
|
||||||
|
hcMethod: values.hcMethod,
|
||||||
|
hcHostname: values.hcHostname,
|
||||||
|
hcPort: parseInt(values.hcPort),
|
||||||
|
hcPath: values.hcPath ?? "",
|
||||||
|
hcInterval: values.hcInterval,
|
||||||
|
hcUnhealthyInterval: values.hcUnhealthyInterval,
|
||||||
|
hcTimeout: values.hcTimeout,
|
||||||
|
hcHealthyThreshold: values.hcHealthyThreshold,
|
||||||
|
hcUnhealthyThreshold: values.hcUnhealthyThreshold,
|
||||||
|
hcFollowRedirects: values.hcFollowRedirects,
|
||||||
|
hcTlsServerName: values.hcTlsServerName,
|
||||||
|
hcStatus: values.hcStatus || null,
|
||||||
|
hcHeaders:
|
||||||
|
values.hcHeaders && values.hcHeaders.length > 0
|
||||||
|
? JSON.stringify(values.hcHeaders)
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (initialValues) {
|
||||||
|
await api.post(
|
||||||
|
`/org/${orgId}/health-check/${initialValues.targetHealthCheckId}`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await api.put(
|
||||||
|
`/org/${orgId}/health-check`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({ title: t("standaloneHcSaved") });
|
||||||
|
onSaved();
|
||||||
|
setOpen(false);
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: formatAxiosError(e),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEditing = !!initialValues;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Credenza open={open} onOpenChange={setOpen}>
|
||||||
|
<CredenzaContent className="max-w-2xl">
|
||||||
|
<CredenzaHeader>
|
||||||
|
<CredenzaTitle>
|
||||||
|
{isEditing
|
||||||
|
? t("standaloneHcEditTitle")
|
||||||
|
: t("standaloneHcCreateTitle")}
|
||||||
|
</CredenzaTitle>
|
||||||
|
<CredenzaDescription>
|
||||||
|
{t("standaloneHcDescription")}
|
||||||
|
</CredenzaDescription>
|
||||||
|
</CredenzaHeader>
|
||||||
|
<CredenzaBody>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="standalone-hc-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
{/* Name */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("standaloneHcNameLabel")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={t(
|
||||||
|
"standaloneHcNamePlaceholder"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Enable Health Check */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcEnabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>
|
||||||
|
{t("enableHealthChecks")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"enableHealthChecksDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{watchedEnabled && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Mode */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcMode"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("healthCheckMode")}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="http">
|
||||||
|
HTTP
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tcp">
|
||||||
|
TCP
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"healthCheckModeDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Connection fields */}
|
||||||
|
{watchedMode === "tcp" ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcHostname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("healthHostname")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("healthPort")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcScheme"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("healthScheme")}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"healthSelectScheme"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="http">
|
||||||
|
HTTP
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="https">
|
||||||
|
HTTPS
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcHostname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("healthHostname")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("healthPort")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("healthCheckPath")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* HTTP Method */}
|
||||||
|
{watchedMode !== "tcp" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcMethod"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("httpMethod")}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"selectHttpMethod"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="GET">
|
||||||
|
GET
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="POST">
|
||||||
|
POST
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="HEAD">
|
||||||
|
HEAD
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="PUT">
|
||||||
|
PUT
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="DELETE">
|
||||||
|
DELETE
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Check Interval, Unhealthy Interval, and Timeout */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcInterval"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"healthyIntervalSeconds"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
parseInt(
|
||||||
|
e.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcUnhealthyInterval"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"unhealthyIntervalSeconds"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
parseInt(
|
||||||
|
e.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcTimeout"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("timeoutSeconds")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
parseInt(
|
||||||
|
e.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Healthy and Unhealthy Thresholds */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcHealthyThreshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("healthyThreshold")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
parseInt(
|
||||||
|
e.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"healthyThresholdDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcUnhealthyThreshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("unhealthyThreshold")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
parseInt(
|
||||||
|
e.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"unhealthyThresholdDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* HTTP-only fields */}
|
||||||
|
{watchedMode !== "tcp" && (
|
||||||
|
<>
|
||||||
|
{/* Expected Response Code */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcStatus"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"expectedResponseCodes"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={
|
||||||
|
field.value ??
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) => {
|
||||||
|
const val =
|
||||||
|
e.target
|
||||||
|
.value;
|
||||||
|
field.onChange(
|
||||||
|
val
|
||||||
|
? parseInt(
|
||||||
|
val
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"expectedResponseCodesDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* TLS Server Name */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcTlsServerName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("tlsServerName")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"tlsServerNameDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Custom Headers */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcHeaders"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("customHeaders")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<HeadersInput
|
||||||
|
value={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
onChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"customHeadersDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Follow Redirects */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="hcFollowRedirects"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"followRedirects"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"followRedirectsDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
onCheckedChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CredenzaBody>
|
||||||
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline" type="button">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</CredenzaClose>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
form="standalone-hc-form"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
|
</CredenzaFooter>
|
||||||
|
</CredenzaContent>
|
||||||
|
</Credenza>
|
||||||
|
);
|
||||||
|
}
|
||||||
299
src/components/StandaloneHealthChecksTable.tsx
Normal file
299
src/components/StandaloneHealthChecksTable.tsx
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
|
import StandaloneHealthCheckCredenza, {
|
||||||
|
HealthCheckRow
|
||||||
|
} from "@app/components/StandaloneHealthCheckCredenza";
|
||||||
|
import { Badge } from "@app/components/ui/badge";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { DataTable, ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from "@app/components/ui/dropdown-menu";
|
||||||
|
import { Switch } from "@app/components/ui/switch";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { orgQueries } from "@app/lib/queries";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type StandaloneHealthChecksTableProps = {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatTarget(row: HealthCheckRow): string {
|
||||||
|
if (!row.hcHostname) return "—";
|
||||||
|
if (row.hcMode === "tcp") {
|
||||||
|
if (!row.hcPort) return row.hcHostname;
|
||||||
|
return `${row.hcHostname}:${row.hcPort}`;
|
||||||
|
}
|
||||||
|
// HTTP / default
|
||||||
|
const scheme = row.hcScheme ?? "http";
|
||||||
|
const host = row.hcHostname;
|
||||||
|
const port = row.hcPort ? `:${row.hcPort}` : "";
|
||||||
|
const path = row.hcPath ?? "/";
|
||||||
|
return `${scheme}://${host}${port}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const healthLabel: Record<HealthCheckRow["hcHealth"], string> = {
|
||||||
|
healthy: "Healthy",
|
||||||
|
unhealthy: "Unhealthy",
|
||||||
|
unknown: "Unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
const healthVariant: Record<
|
||||||
|
HealthCheckRow["hcHealth"],
|
||||||
|
"green" | "red" | "secondary"
|
||||||
|
> = {
|
||||||
|
healthy: "green",
|
||||||
|
unhealthy: "red",
|
||||||
|
unknown: "secondary"
|
||||||
|
};
|
||||||
|
|
||||||
|
function HealthBadge({ health }: { health: HealthCheckRow["hcHealth"] }) {
|
||||||
|
return (
|
||||||
|
<Badge variant={healthVariant[health]}>{healthLabel[health]}</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StandaloneHealthChecksTable({
|
||||||
|
orgId
|
||||||
|
}: StandaloneHealthChecksTableProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const api = createApiClient(useEnvContext());
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [credenzaOpen, setCredenzaOpen] = useState(false);
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
const [selected, setSelected] = useState<HealthCheckRow | null>(null);
|
||||||
|
const [togglingId, setTogglingId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: rows = [],
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
isRefetching
|
||||||
|
} = useQuery(orgQueries.standaloneHealthChecks({ orgId }));
|
||||||
|
|
||||||
|
const invalidate = () =>
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
orgQueries.standaloneHealthChecks({ orgId })
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleEnabled = async (
|
||||||
|
row: HealthCheckRow,
|
||||||
|
enabled: boolean
|
||||||
|
) => {
|
||||||
|
setTogglingId(row.targetHealthCheckId);
|
||||||
|
try {
|
||||||
|
await api.post(
|
||||||
|
`/org/${orgId}/health-check/${row.targetHealthCheckId}`,
|
||||||
|
{ hcEnabled: enabled }
|
||||||
|
);
|
||||||
|
await invalidate();
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: formatAxiosError(e),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setTogglingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!selected) return;
|
||||||
|
try {
|
||||||
|
await api.delete(
|
||||||
|
`/org/${orgId}/health-check/${selected.targetHealthCheckId}`
|
||||||
|
);
|
||||||
|
await invalidate();
|
||||||
|
toast({ title: t("standaloneHcDeleted") });
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: formatAxiosError(e),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setDeleteOpen(false);
|
||||||
|
setSelected(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ExtendedColumnDef<HealthCheckRow>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
enableHiding: false,
|
||||||
|
friendlyName: t("name"),
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("name")}
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="font-medium">{row.original.name}</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mode",
|
||||||
|
friendlyName: t("standaloneHcColumnMode"),
|
||||||
|
header: () => (
|
||||||
|
<span className="p-3">{t("standaloneHcColumnMode")}</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="uppercase text-xs font-mono">
|
||||||
|
{row.original.hcMode?.toUpperCase() ?? "—"}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "target",
|
||||||
|
friendlyName: t("standaloneHcColumnTarget"),
|
||||||
|
header: () => (
|
||||||
|
<span className="p-3">{t("standaloneHcColumnTarget")}</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="font-mono text-xs text-muted-foreground truncate max-w-64 block">
|
||||||
|
{formatTarget(row.original)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "health",
|
||||||
|
friendlyName: t("standaloneHcColumnHealth"),
|
||||||
|
header: () => (
|
||||||
|
<span className="p-3">{t("standaloneHcColumnHealth")}</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<HealthBadge health={row.original.hcHealth} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "hcEnabled",
|
||||||
|
friendlyName: t("alertingColumnEnabled"),
|
||||||
|
header: () => (
|
||||||
|
<span className="p-3">{t("alertingColumnEnabled")}</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const r = row.original;
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={r.hcEnabled}
|
||||||
|
disabled={togglingId === r.targetHealthCheckId}
|
||||||
|
onCheckedChange={(v) => handleToggleEnabled(r, v)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rowActions",
|
||||||
|
enableHiding: false,
|
||||||
|
header: () => <span className="p-3" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const r = row.original;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">
|
||||||
|
{t("openMenu")}
|
||||||
|
</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(r);
|
||||||
|
setDeleteOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-red-500">
|
||||||
|
{t("delete")}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(r);
|
||||||
|
setCredenzaOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("edit")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{selected && deleteOpen && (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={deleteOpen}
|
||||||
|
setOpen={(val) => {
|
||||||
|
setDeleteOpen(val);
|
||||||
|
if (!val) setSelected(null);
|
||||||
|
}}
|
||||||
|
dialog={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{t("standaloneHcDeleteQuestion")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("delete")}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
string={selected.name}
|
||||||
|
title={t("standaloneHcDeleteTitle")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<StandaloneHealthCheckCredenza
|
||||||
|
open={credenzaOpen}
|
||||||
|
setOpen={(val) => {
|
||||||
|
setCredenzaOpen(val);
|
||||||
|
if (!val) setSelected(null);
|
||||||
|
}}
|
||||||
|
orgId={orgId}
|
||||||
|
initialValues={selected}
|
||||||
|
onSaved={invalidate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={rows}
|
||||||
|
persistPageSize="Org-standalone-health-checks-table"
|
||||||
|
title={t("standaloneHcTableTitle")}
|
||||||
|
searchPlaceholder={t("standaloneHcSearchPlaceholder")}
|
||||||
|
searchColumn="name"
|
||||||
|
onAdd={() => {
|
||||||
|
setSelected(null);
|
||||||
|
setCredenzaOpen(true);
|
||||||
|
}}
|
||||||
|
onRefresh={() => refetch()}
|
||||||
|
isRefreshing={isRefetching || isLoading}
|
||||||
|
addButtonText={t("standaloneHcAddButton")}
|
||||||
|
enableColumnVisibility
|
||||||
|
stickyLeftColumn="name"
|
||||||
|
stickyRightColumn="rowActions"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -182,7 +182,7 @@ function HealthCheckMultiSelect({
|
|||||||
const query = debounced.trim().toLowerCase();
|
const query = debounced.trim().toLowerCase();
|
||||||
const base = query
|
const base = query
|
||||||
? healthChecks.filter((hc) =>
|
? healthChecks.filter((hc) =>
|
||||||
hc.resourceName.toLowerCase().includes(query)
|
hc.name.toLowerCase().includes(query)
|
||||||
)
|
)
|
||||||
: healthChecks;
|
: healthChecks;
|
||||||
// Always keep already-selected items visible even if they fall outside the search
|
// Always keep already-selected items visible even if they fall outside the search
|
||||||
@@ -243,7 +243,7 @@ function HealthCheckMultiSelect({
|
|||||||
{shown.map((hc) => (
|
{shown.map((hc) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={hc.targetHealthCheckId}
|
key={hc.targetHealthCheckId}
|
||||||
value={`${hc.targetHealthCheckId}:${hc.resourceName}`}
|
value={`${hc.targetHealthCheckId}:${hc.name}`}
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
toggle(hc.targetHealthCheckId)
|
toggle(hc.targetHealthCheckId)
|
||||||
}
|
}
|
||||||
@@ -258,7 +258,7 @@ function HealthCheckMultiSelect({
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{hc.resourceName}
|
{hc.name}
|
||||||
</span>
|
</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { ListClientsResponse } from "@server/routers/client";
|
|||||||
import type { ListDomainsResponse } from "@server/routers/domain";
|
import type { ListDomainsResponse } from "@server/routers/domain";
|
||||||
import type {
|
import type {
|
||||||
GetResourceWhitelistResponse,
|
GetResourceWhitelistResponse,
|
||||||
ListHealthChecksResponse,
|
|
||||||
ListResourceNamesResponse,
|
ListResourceNamesResponse,
|
||||||
ListResourcesResponse
|
ListResourcesResponse
|
||||||
} from "@server/routers/resource";
|
} from "@server/routers/resource";
|
||||||
@@ -28,7 +27,7 @@ import type { AxiosResponse } from "axios";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { remote } from "./api";
|
import { remote } from "./api";
|
||||||
import { durationToMs } from "./durationToMs";
|
import { durationToMs } from "./durationToMs";
|
||||||
import { wait } from "./wait";
|
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
|
||||||
|
|
||||||
export type ProductUpdate = {
|
export type ProductUpdate = {
|
||||||
link: string | null;
|
link: string | null;
|
||||||
@@ -264,6 +263,44 @@ export const orgQueries = {
|
|||||||
>(`/org/${orgId}/alert-rules`, { signal });
|
>(`/org/${orgId}/alert-rules`, { signal });
|
||||||
return res.data.data.alertRules;
|
return res.data.data.alertRules;
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
standaloneHealthChecks: ({ orgId }: { orgId: string }) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["ORG", orgId, "STANDALONE_HEALTH_CHECKS"] as const,
|
||||||
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const res = await meta!.api.get<
|
||||||
|
AxiosResponse<{
|
||||||
|
healthChecks: {
|
||||||
|
targetHealthCheckId: number;
|
||||||
|
name: string;
|
||||||
|
hcEnabled: boolean;
|
||||||
|
hcHealth: "unknown" | "healthy" | "unhealthy";
|
||||||
|
hcMode: string | null;
|
||||||
|
hcHostname: string | null;
|
||||||
|
hcPort: number | null;
|
||||||
|
hcPath: string | null;
|
||||||
|
hcScheme: string | null;
|
||||||
|
hcMethod: string | null;
|
||||||
|
hcInterval: number | null;
|
||||||
|
hcUnhealthyInterval: number | null;
|
||||||
|
hcTimeout: number | null;
|
||||||
|
hcHeaders: string | null;
|
||||||
|
hcFollowRedirects: boolean | null;
|
||||||
|
hcStatus: number | null;
|
||||||
|
hcTlsServerName: string | null;
|
||||||
|
hcHealthyThreshold: number | null;
|
||||||
|
hcUnhealthyThreshold: number | null;
|
||||||
|
}[];
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
}>
|
||||||
|
>(`/org/${orgId}/health-checks`, { signal });
|
||||||
|
return res.data.data.healthChecks;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user