check card details
This commit is contained in:
1 parent
9f32eb6362
commit
92fd405395
8 files changed
+307
-197
No files matched your search
@@ -0,0 +1,227 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Link from "@mui/material/Link";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { StatusLabel, ValueLabel } from "@/components/design-elements";
|
||||
import { BaseBox } from "@/components/design-elements";
|
||||
|
||||
import { useAppSelector } from "@/hooks/AppHooks";
|
||||
import type { ICheck } from "@/types/check";
|
||||
import type { MonitorStatus } from "@/types/monitor";
|
||||
import { formatDateWithTz } from "@/utils/TimeUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { getMonitorPath } from "@/utils/MonitorUtils";
|
||||
|
||||
type CheckDetailsCardProps = {
|
||||
check: ICheck;
|
||||
};
|
||||
|
||||
export const CheckDetailsCard = ({ check }: CheckDetailsCardProps) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useAppSelector((state: any) => state.ui.timezone);
|
||||
const phases = check?.timings?.phases;
|
||||
const phaseKeys = [
|
||||
"wait",
|
||||
"dns",
|
||||
"tcp",
|
||||
"tls",
|
||||
"request",
|
||||
"firstByte",
|
||||
"download",
|
||||
"total",
|
||||
] as const;
|
||||
|
||||
const status =
|
||||
(check?.status as MonitorStatus) ?? ("initializing" as MonitorStatus);
|
||||
const httpStatusCode = Number(check?.httpStatusCode ?? NaN);
|
||||
const httpValue: "positive" | "neutral" | "negative" = Number.isFinite(
|
||||
httpStatusCode
|
||||
)
|
||||
? httpStatusCode < 300
|
||||
? "positive"
|
||||
: httpStatusCode < 400
|
||||
? "neutral"
|
||||
: "negative"
|
||||
: "neutral";
|
||||
const httpText = Number.isFinite(httpStatusCode)
|
||||
? String(httpStatusCode)
|
||||
: "-";
|
||||
const typeText = check?.metadata?.type ?? "-";
|
||||
const checkedAtText = check?.createdAt
|
||||
? formatDateWithTz(check.createdAt, "ddd, MMM D, YYYY, HH:mm A", uiTimezone)
|
||||
: "-";
|
||||
const messageText = check?.message ?? "-";
|
||||
const getPhaseMs = (key: (typeof phaseKeys)[number]) => {
|
||||
const v = Number((phases as any)?.[key] ?? NaN);
|
||||
return Number.isFinite(v) ? `${Math.floor(Math.max(0, v))} ms` : "-";
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseBox p={4} flex={1} height={"100%"}>
|
||||
<Stack spacing={8} sx={{ width: "100%" }}>
|
||||
<Stack>
|
||||
<Typography>Check details</Typography>
|
||||
<Typography variant="body2">{`Total request time: ${getPhaseMs("total")}`}</Typography>
|
||||
{(() => {
|
||||
const href = `/${getMonitorPath(check.metadata.type)}/${check.metadata.monitorId}`;
|
||||
return href ? (
|
||||
<Typography variant="body2" sx={{ mt: 0.5 }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={href}
|
||||
underline="hover"
|
||||
color="primary"
|
||||
>
|
||||
View monitor details
|
||||
</Link>
|
||||
</Typography>
|
||||
) : null;
|
||||
})()}
|
||||
</Stack>
|
||||
{/* Basic info section */}
|
||||
<Grid container spacing={2}>
|
||||
<Grid
|
||||
size={12}
|
||||
borderBottom={1}
|
||||
borderColor={theme.palette.divider}
|
||||
mt={4}
|
||||
mb={2}
|
||||
>
|
||||
<Typography>Basic check info</Typography>
|
||||
</Grid>
|
||||
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Status
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box>
|
||||
<StatusLabel status={status} />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
HTTP Code
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box>
|
||||
<ValueLabel value={httpValue} text={httpText} />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Type
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{typeText}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Check time
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{checkedAtText}</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Message
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{messageText}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{/* Timing phases section */}
|
||||
<Grid container spacing={2}>
|
||||
<Grid
|
||||
size={12}
|
||||
borderBottom={1}
|
||||
borderColor={theme.palette.divider}
|
||||
mb={4}
|
||||
mt={2}
|
||||
>
|
||||
<Typography>Timing phases</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Wait
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{getPhaseMs("wait")}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
DNS
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{getPhaseMs("dns")}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
TCP
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{getPhaseMs("tcp")}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
TLS
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{getPhaseMs("tls")}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Request
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{getPhaseMs("request")}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
First Byte
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{getPhaseMs("firstByte")}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Download
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{getPhaseMs("download")}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Total
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{getPhaseMs("total")}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { StatusLabel, ValueLabel } from "@/components/design-elements";
|
||||
import { BaseBox } from "@/components/design-elements";
|
||||
|
||||
import { useAppSelector } from "@/hooks/AppHooks";
|
||||
import type { ICheck } from "@/types/check";
|
||||
import type { MonitorStatus } from "@/types/monitor";
|
||||
import { formatDateWithTz } from "@/utils/TimeUtils";
|
||||
|
||||
type CheckDetailsCardProps = {
|
||||
check: ICheck;
|
||||
};
|
||||
|
||||
export const CheckDetailsCard = ({ check }: CheckDetailsCardProps) => {
|
||||
const uiTimezone = useAppSelector((state: any) => state.ui.timezone);
|
||||
const phases = check?.timings?.phases;
|
||||
const phaseKeys = [
|
||||
"wait",
|
||||
"dns",
|
||||
"tcp",
|
||||
"tls",
|
||||
"request",
|
||||
"firstByte",
|
||||
"download",
|
||||
] as const;
|
||||
const totalMs = phases
|
||||
? phaseKeys.reduce(
|
||||
(sum, key) => sum + Math.max(0, Number((phases as any)[key] ?? 0)),
|
||||
0
|
||||
)
|
||||
: Number(check.responseTime ?? 0);
|
||||
|
||||
return (
|
||||
<BaseBox p={4} flex={1} height={"100%"}>
|
||||
<Stack spacing={2} sx={{ width: "100%" }}>
|
||||
<Typography>Check details</Typography>
|
||||
<Typography variant="body2">
|
||||
{`Total request time: ${check.timings.phases.total} ms`}
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Status
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box>
|
||||
<StatusLabel status={check.status as MonitorStatus} />
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
HTTP Code
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6} sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box>
|
||||
<ValueLabel
|
||||
value={
|
||||
check.httpStatusCode < 300
|
||||
? "positive"
|
||||
: check.httpStatusCode < 400
|
||||
? "neutral"
|
||||
: "negative"
|
||||
}
|
||||
text={String(check.httpStatusCode ?? "-")}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Type
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">
|
||||
{check.metadata?.type || "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{Math.round(totalMs)} ms</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Checked At
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">
|
||||
{formatDateWithTz(
|
||||
check.createdAt,
|
||||
"ddd, MMM D, YYYY, HH:mm A",
|
||||
uiTimezone
|
||||
)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Message
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography variant="body2">{check.message || "-"}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { PieChart, Pie, ResponsiveContainer, Tooltip, Legend } from "recharts";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
import { useState } from "react";
|
||||
import { BaseBox } from "@/components/design-elements";
|
||||
|
||||
type TimingsChartProps = { check: ICheck | null };
|
||||
@@ -26,13 +27,15 @@ const PHASE_ORDER: (keyof CheckTimingPhases)[] = [
|
||||
|
||||
const buildTimingSegments = (
|
||||
phases: CheckTimingPhases | undefined,
|
||||
stroke: string
|
||||
stroke: string,
|
||||
hoveredIndex: number | null,
|
||||
hoverFill: string
|
||||
): TimingSegment[] => {
|
||||
if (!phases) return [];
|
||||
return PHASE_ORDER.map((key) => ({
|
||||
return PHASE_ORDER.map((key, i) => ({
|
||||
name: String(key),
|
||||
value: Math.max(0, Number((phases as any)[key] ?? 0)),
|
||||
fill: "transparent",
|
||||
fill: hoveredIndex === i ? hoverFill : "transparent",
|
||||
stroke,
|
||||
}));
|
||||
};
|
||||
@@ -42,68 +45,21 @@ export const TimingsChart = ({ check }: TimingsChartProps) => {
|
||||
|
||||
const theme = useTheme();
|
||||
const strokeColor = alpha(theme.palette.primary.main, 0.8);
|
||||
const segments = buildTimingSegments(check?.timings?.phases, strokeColor);
|
||||
if (!segments.length) return null;
|
||||
const LABEL_THRESHOLD = 0.05;
|
||||
|
||||
const renderLabel = ({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
outerRadius,
|
||||
name,
|
||||
value,
|
||||
percent,
|
||||
}: any) => {
|
||||
if (!percent || percent < LABEL_THRESHOLD) return null;
|
||||
const RAD = Math.PI / 180;
|
||||
const r = (outerRadius || 0) + 24;
|
||||
const x = cx + r * Math.cos(-midAngle * RAD);
|
||||
const y = cy + r * Math.sin(-midAngle * RAD);
|
||||
const text = `${name}: ${Math.round(Number(value) || 0)} ms`;
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fill={strokeColor}
|
||||
textAnchor={x > cx ? "start" : "end"}
|
||||
dominantBaseline="central"
|
||||
style={{ fontSize: 12 }}
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLegend = () => (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{segments.map((s) => (
|
||||
<div
|
||||
key={s.name}
|
||||
style={{ display: "flex", alignItems: "center", gap: 8 }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: 14,
|
||||
height: 2,
|
||||
backgroundColor: strokeColor,
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 12, color: theme.palette.text.secondary }}>
|
||||
{s.name}: {Math.round(Number(s.value) || 0)} ms
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const segments = buildTimingSegments(
|
||||
check?.timings?.phases,
|
||||
strokeColor,
|
||||
hoveredIndex,
|
||||
theme.palette.primary.main
|
||||
);
|
||||
if (!segments.length) return null;
|
||||
|
||||
return (
|
||||
<BaseBox p={4} flex={1} height={"100%"}>
|
||||
<Stack spacing={2} sx={{ width: "100%", height: 260 }}>
|
||||
<Stack spacing={2} sx={{ width: "100%", height: 400 }}>
|
||||
<Typography>Request timings</Typography>
|
||||
<Typography variant="body2">
|
||||
{`Total request time: ${check.timings.phases.total} ms`}
|
||||
{`Total request time: ${Math.floor(Number(check?.timings?.phases?.total ?? 0))} ms`}
|
||||
</Typography>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
@@ -113,18 +69,22 @@ export const TimingsChart = ({ check }: TimingsChartProps) => {
|
||||
nameKey="name"
|
||||
stroke="none"
|
||||
minAngle={4}
|
||||
label={renderLabel}
|
||||
labelLine={{ stroke: strokeColor, strokeWidth: 1, opacity: 0.6 }}
|
||||
onMouseEnter={(_, index: number) => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value) => `${Math.round(value as number)} ms`}
|
||||
formatter={(value) => `${Math.floor(value as number)} ms`}
|
||||
/>
|
||||
<Legend
|
||||
layout="vertical"
|
||||
verticalAlign="middle"
|
||||
align="right"
|
||||
content={renderLegend}
|
||||
wrapperStyle={{ paddingLeft: 12 }}
|
||||
layout="horizontal"
|
||||
verticalAlign="bottom"
|
||||
align="center"
|
||||
payload={segments.map((s) => ({
|
||||
id: s.name,
|
||||
value: `${s.name}: ${Math.floor(Number(s.value) || 0)} ms`,
|
||||
type: "line",
|
||||
color: strokeColor,
|
||||
}))}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { TimingsChart } from "./ChartTiming";
|
||||
export { CheckDetailsCard } from "./CardChecktDetails";
|
||||
export { CheckDetailsCard } from "./CardCheckDetails";
|
||||
@@ -123,7 +123,7 @@ export const MonitorBasePageWithStates = ({
|
||||
|
||||
const showLoading = loading && (!items || items.length === 0);
|
||||
|
||||
if (isEmpty(items)) {
|
||||
if (!loading && isEmpty(items)) {
|
||||
return (
|
||||
<EmptyMonitorFallback
|
||||
page={page}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { MonitorType } from "./monitor";
|
||||
|
||||
export interface CheckTimingPhases {
|
||||
wait: number;
|
||||
dns: number;
|
||||
@@ -105,7 +107,7 @@ export interface ICheck {
|
||||
_id: string;
|
||||
metadata: {
|
||||
monitorId: string;
|
||||
type: string;
|
||||
type: MonitorType;
|
||||
teamId: string;
|
||||
};
|
||||
status: string;
|
||||
|
||||
+26
-4
@@ -179,10 +179,14 @@
|
||||
}
|
||||
},
|
||||
"/auth/logout": { "post": { "responses": { "200": { "description": "Logged out" } } } },
|
||||
"/auth/me": { "get": { "responses": { "200": { "description": "Current user" } } } },
|
||||
"/auth/cleanup": { "post": { "responses": { "200": { "description": "Cleanup executed" } } } },
|
||||
"/auth/cleanup-monitors": { "post": { "responses": { "200": { "description": "Monitor cleanup executed" } } } },
|
||||
|
||||
"/billing/plans": { "get": { "responses": { "200": { "description": "Available plans" } } } },
|
||||
"/billing/subscribe": { "post": { "responses": { "200": { "description": "Subscribed" } } } },
|
||||
"/billing/cancel": { "post": { "responses": { "200": { "description": "Canceled" } } } },
|
||||
"/billing/confirm": { "get": { "responses": { "200": { "description": "Subscription confirmed" } } } },
|
||||
|
||||
"/monitors": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
@@ -282,6 +286,9 @@
|
||||
"security": []
|
||||
}
|
||||
},
|
||||
"/diagnostic/logs": { "get": { "responses": { "200": { "description": "Logs" } } } },
|
||||
"/diagnostic/jobs": { "get": { "responses": { "200": { "description": "Jobs" } } } },
|
||||
"/diagnostic/env": { "get": { "responses": { "200": { "description": "Environment" } } } },
|
||||
|
||||
"/checks": {
|
||||
"get": {
|
||||
@@ -295,6 +302,14 @@
|
||||
"responses": { "200": { "description": "Checks list" } }
|
||||
}
|
||||
},
|
||||
"/checks/{id}": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
],
|
||||
"responses": { "200": { "description": "Check by id" }, "404": { "description": "Not found" } }
|
||||
}
|
||||
},
|
||||
|
||||
"/incidents": {
|
||||
"get": {
|
||||
@@ -310,10 +325,11 @@
|
||||
"/incidents/{id}/resolve": {
|
||||
"patch": {
|
||||
"parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ],
|
||||
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } },
|
||||
"responses": { "200": { "description": "Resolved" } }
|
||||
}
|
||||
},
|
||||
"/incidents/export": { "get": { "responses": { "200": { "description": "Export incidents" } } } },
|
||||
"/incidents/{id}": { "get": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Incident" } } } },
|
||||
|
||||
"/invite": {
|
||||
"get": { "responses": { "200": { "description": "Invites list" } } },
|
||||
@@ -399,9 +415,15 @@
|
||||
"post": { "responses": { "200": { "description": "Created" } } }
|
||||
},
|
||||
"/team-members/{id}": {
|
||||
"get": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Member" } } },
|
||||
"get": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Team member" } } },
|
||||
"patch": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Updated" } } },
|
||||
"delete": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Deleted" } } }
|
||||
}
|
||||
},
|
||||
|
||||
"/me": { "get": { "responses": { "200": { "description": "Current user" } } } },
|
||||
"/me/entitlements": { "get": { "responses": { "200": { "description": "Entitlements" } } } },
|
||||
"/me/permissions": { "get": { "responses": { "200": { "description": "Permissions" } } } },
|
||||
|
||||
"/stripe/webhook": { "post": { "responses": { "200": { "description": "Webhook received" } } } }
|
||||
}
|
||||
}
|
||||
+22
-7
@@ -12,6 +12,7 @@ const indexLogger = getChildLogger("index");
|
||||
import swaggerUi from "swagger-ui-express";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const createApp = async () => {
|
||||
await connectDatabase();
|
||||
@@ -38,13 +39,28 @@ const createApp = async () => {
|
||||
const routes = initRoutes(controllers, app);
|
||||
// expose services for route-level access (e.g., in me routes)
|
||||
app.set("services", services);
|
||||
// Swagger UI at /api-docs
|
||||
|
||||
try {
|
||||
// Load OpenAPI doc from project root within server folder
|
||||
const openapiPath = path.resolve(process.cwd(), "openapi.json");
|
||||
const openapiJson = JSON.parse(fs.readFileSync(openapiPath, "utf-8"));
|
||||
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openapiJson));
|
||||
indexLogger.info("Swagger UI available at /api-docs");
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const candidates = [
|
||||
path.resolve(process.cwd(), "openapi.json"),
|
||||
path.resolve(process.cwd(), "server/openapi.json"),
|
||||
path.resolve(__dirname, "../openapi.json"),
|
||||
];
|
||||
const existing = candidates.find((p) => fs.existsSync(p));
|
||||
if (!existing) {
|
||||
indexLogger.error(
|
||||
"OpenAPI file not found. Checked: " + candidates.join(", ")
|
||||
);
|
||||
} else {
|
||||
const openapiJson = JSON.parse(fs.readFileSync(existing, "utf-8"));
|
||||
const setup = swaggerUi.setup(openapiJson);
|
||||
app.use("/api-docs", swaggerUi.serve, setup);
|
||||
|
||||
indexLogger.info(
|
||||
`Swagger UI available at /api-docs and /api/v1/api-docs (spec: ${existing})`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
indexLogger.error("Failed to load OpenAPI document for Swagger UI", e);
|
||||
}
|
||||
@@ -53,7 +69,6 @@ const createApp = async () => {
|
||||
status: "OK",
|
||||
});
|
||||
});
|
||||
|
||||
const port = 52345;
|
||||
app.listen(port, "0.0.0.0", () => {
|
||||
indexLogger.info(`Server is running on http://localhost:${port}`);
|
||||
|
||||
Reference in new issue
Block a user