feat: Add Exceptions documentation page and update routing, breadcrumbs, and side menu

This commit is contained in:
Nawaz Dhandala
2026-03-18 11:55:17 +00:00
parent ffc49d83eb
commit ad999313c3
13 changed files with 194 additions and 212 deletions

View File

@@ -1,20 +1,7 @@
import PageComponentProps from "../PageComponentProps";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useCallback,
useState,
} from "react";
import React, { FunctionComponent, ReactElement } from "react";
import ExceptionsTable from "../../Components/Exceptions/ExceptionsTable";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
import Button, {
ButtonSize,
ButtonStyleType,
} from "Common/UI/Components/Button/Button";
import IconProp from "Common/Types/Icon/IconProp";
import TelemetryException from "Common/Models/DatabaseModels/TelemetryException";
const ArchivedExceptionsPage: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
@@ -22,19 +9,6 @@ const ArchivedExceptionsPage: FunctionComponent<PageComponentProps> = (
const disableTelemetryForThisProject: boolean =
props.currentProject?.reseller?.enableTelemetryFeatures === false;
const [hasData, setHasData] = useState<boolean>(false);
const [showDocs, setShowDocs] = useState<boolean>(false);
const handleFetchSuccess: (
data: Array<TelemetryException>,
totalCount: number,
) => void = useCallback(
(_data: Array<TelemetryException>, totalCount: number) => {
setHasData(totalCount > 0);
},
[],
);
if (disableTelemetryForThisProject) {
return (
<ErrorMessage message="Looks like you have bought this plan from a reseller. It did not include telemetry features in your plan. Telemetry features are disabled for this project." />
@@ -42,33 +16,13 @@ const ArchivedExceptionsPage: FunctionComponent<PageComponentProps> = (
}
return (
<Fragment>
<ExceptionsTable
query={{
isArchived: true,
}}
title="Archived Exceptions"
description="All the exceptions that have been archived. You will not be notified about these exceptions."
onFetchSuccess={handleFetchSuccess}
/>
{!hasData && <TelemetryDocumentation telemetryType="exceptions" />}
{hasData && !showDocs && (
<div className="flex justify-center mt-4 mb-4">
<Button
title="View Setup Documentation"
icon={IconProp.Book}
buttonSize={ButtonSize.Small}
buttonStyle={ButtonStyleType.OUTLINE}
onClick={() => {
setShowDocs(true);
}}
/>
</div>
)}
{hasData && showDocs && (
<TelemetryDocumentation telemetryType="exceptions" />
)}
</Fragment>
<ExceptionsTable
query={{
isArchived: true,
}}
title="Archived Exceptions"
description="All the exceptions that have been archived. You will not be notified about these exceptions."
/>
);
};

View File

@@ -0,0 +1,11 @@
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
const ExceptionsDocumentationPage: FunctionComponent<PageComponentProps> = (
_props: PageComponentProps,
): ReactElement => {
return <TelemetryDocumentation telemetryType="exceptions" />;
};
export default ExceptionsDocumentationPage;

View File

@@ -1,20 +1,7 @@
import PageComponentProps from "../PageComponentProps";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useCallback,
useState,
} from "react";
import React, { FunctionComponent, ReactElement } from "react";
import ExceptionsTable from "../../Components/Exceptions/ExceptionsTable";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
import Button, {
ButtonSize,
ButtonStyleType,
} from "Common/UI/Components/Button/Button";
import IconProp from "Common/Types/Icon/IconProp";
import TelemetryException from "Common/Models/DatabaseModels/TelemetryException";
const ResolvedExceptionsPage: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
@@ -22,19 +9,6 @@ const ResolvedExceptionsPage: FunctionComponent<PageComponentProps> = (
const disableTelemetryForThisProject: boolean =
props.currentProject?.reseller?.enableTelemetryFeatures === false;
const [hasData, setHasData] = useState<boolean>(false);
const [showDocs, setShowDocs] = useState<boolean>(false);
const handleFetchSuccess: (
data: Array<TelemetryException>,
totalCount: number,
) => void = useCallback(
(_data: Array<TelemetryException>, totalCount: number) => {
setHasData(totalCount > 0);
},
[],
);
if (disableTelemetryForThisProject) {
return (
<ErrorMessage message="Looks like you have bought this plan from a reseller. It did not include telemetry features in your plan. Telemetry features are disabled for this project." />
@@ -42,34 +16,14 @@ const ResolvedExceptionsPage: FunctionComponent<PageComponentProps> = (
}
return (
<Fragment>
<ExceptionsTable
query={{
isResolved: true,
isArchived: false,
}}
title="Resolved Exceptions"
description="All the exceptions that have been resolved."
onFetchSuccess={handleFetchSuccess}
/>
{!hasData && <TelemetryDocumentation telemetryType="exceptions" />}
{hasData && !showDocs && (
<div className="flex justify-center mt-4 mb-4">
<Button
title="View Setup Documentation"
icon={IconProp.Book}
buttonSize={ButtonSize.Small}
buttonStyle={ButtonStyleType.OUTLINE}
onClick={() => {
setShowDocs(true);
}}
/>
</div>
)}
{hasData && showDocs && (
<TelemetryDocumentation telemetryType="exceptions" />
)}
</Fragment>
<ExceptionsTable
query={{
isResolved: true,
isArchived: false,
}}
title="Resolved Exceptions"
description="All the exceptions that have been resolved."
/>
);
};

View File

@@ -49,6 +49,15 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
},
icon: IconProp.Archive,
},
{
link: {
title: "Documentation",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.EXCEPTIONS_DOCUMENTATION] as Route,
),
},
icon: IconProp.Book,
},
],
},
];

View File

@@ -1,20 +1,7 @@
import PageComponentProps from "../PageComponentProps";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useCallback,
useState,
} from "react";
import React, { FunctionComponent, ReactElement } from "react";
import ExceptionsTable from "../../Components/Exceptions/ExceptionsTable";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
import Button, {
ButtonSize,
ButtonStyleType,
} from "Common/UI/Components/Button/Button";
import IconProp from "Common/Types/Icon/IconProp";
import TelemetryException from "Common/Models/DatabaseModels/TelemetryException";
const UnresolvedExceptionsPage: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
@@ -22,19 +9,6 @@ const UnresolvedExceptionsPage: FunctionComponent<PageComponentProps> = (
const disableTelemetryForThisProject: boolean =
props.currentProject?.reseller?.enableTelemetryFeatures === false;
const [hasData, setHasData] = useState<boolean>(false);
const [showDocs, setShowDocs] = useState<boolean>(false);
const handleFetchSuccess: (
data: Array<TelemetryException>,
totalCount: number,
) => void = useCallback(
(_data: Array<TelemetryException>, totalCount: number) => {
setHasData(totalCount > 0);
},
[],
);
if (disableTelemetryForThisProject) {
return (
<ErrorMessage message="Looks like you have bought this plan from a reseller. It did not include telemetry features in your plan. Telemetry features are disabled for this project." />
@@ -42,34 +16,14 @@ const UnresolvedExceptionsPage: FunctionComponent<PageComponentProps> = (
}
return (
<Fragment>
<ExceptionsTable
query={{
isResolved: false,
isArchived: false,
}}
title="Unresolved Exceptions"
description="All the exceptions that have not been resolved."
onFetchSuccess={handleFetchSuccess}
/>
{!hasData && <TelemetryDocumentation telemetryType="exceptions" />}
{hasData && !showDocs && (
<div className="flex justify-center mt-4 mb-4">
<Button
title="View Setup Documentation"
icon={IconProp.Book}
buttonSize={ButtonSize.Small}
buttonStyle={ButtonStyleType.OUTLINE}
onClick={() => {
setShowDocs(true);
}}
/>
</div>
)}
{hasData && showDocs && (
<TelemetryDocumentation telemetryType="exceptions" />
)}
</Fragment>
<ExceptionsTable
query={{
isResolved: false,
isArchived: false,
}}
title="Unresolved Exceptions"
description="All the exceptions that have not been resolved."
/>
);
};

View File

@@ -23,6 +23,7 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import { JSONObject } from "Common/Types/JSON";
import InBetween from "Common/Types/BaseDatabase/InBetween";
interface KubernetesEvent {
timestamp: string;
@@ -69,10 +70,7 @@ const KubernetesClusterEvents: FunctionComponent<
modelType: Log,
query: {
projectId: ProjectUtil.getCurrentProjectId()!.toString(),
time: {
startValue: startDate,
endValue: endDate,
} as any,
time: new InBetween<Date>(startDate, endDate),
},
limit: 200,
skip: 0,
@@ -88,6 +86,63 @@ const KubernetesClusterEvents: FunctionComponent<
requestOptions: {},
});
// Helper to extract a string value from OTLP kvlistValue
const getKvValue = (
kvList: JSONObject | undefined,
key: string,
): string => {
if (!kvList) {
return "";
}
const values = (kvList as JSONObject)["values"] as Array<JSONObject> | undefined;
if (!values) {
return "";
}
for (const entry of values) {
if (entry["key"] === key) {
const val = entry["value"] as JSONObject | undefined;
if (!val) {
return "";
}
if (val["stringValue"]) {
return val["stringValue"] as string;
}
if (val["intValue"]) {
return String(val["intValue"]);
}
// Nested kvlist (e.g., regarding, metadata)
if (val["kvlistValue"]) {
return val["kvlistValue"] as unknown as string;
}
}
}
return "";
};
// Helper to get nested kvlist value
const getNestedKvValue = (
kvList: JSONObject | undefined,
parentKey: string,
childKey: string,
): string => {
if (!kvList) {
return "";
}
const values = (kvList as JSONObject)["values"] as Array<JSONObject> | undefined;
if (!values) {
return "";
}
for (const entry of values) {
if (entry["key"] === parentKey) {
const val = entry["value"] as JSONObject | undefined;
if (val && val["kvlistValue"]) {
return getKvValue(val["kvlistValue"] as JSONObject, childKey);
}
}
}
return "";
};
const k8sEvents: Array<KubernetesEvent> = [];
for (const log of listResult.data) {
@@ -95,35 +150,55 @@ const KubernetesClusterEvents: FunctionComponent<
// Filter to only k8s events from this cluster
if (
attrs["k8s.cluster.name"] !== item.clusterIdentifier &&
attrs["k8s_cluster_name"] !== item.clusterIdentifier
attrs["resource.k8s.cluster.name"] !== item.clusterIdentifier &&
attrs["k8s.cluster.name"] !== item.clusterIdentifier
) {
continue;
}
// k8sobjects receiver events have k8s event attributes
const eventType: string =
(attrs["k8s.event.type"] as string) ||
(attrs["type"] as string) ||
"";
const reason: string =
(attrs["k8s.event.reason"] as string) ||
(attrs["reason"] as string) ||
"";
const objectKind: string =
(attrs["k8s.object.kind"] as string) ||
(attrs["involvedObject.kind"] as string) ||
"";
const objectName: string =
(attrs["k8s.object.name"] as string) ||
(attrs["involvedObject.name"] as string) ||
"";
const namespace: string =
(attrs["k8s.namespace.name"] as string) ||
(attrs["namespace"] as string) ||
"";
// Only process k8s event logs (from k8sobjects receiver)
if (attrs["logAttributes.event.domain"] !== "k8s") {
continue;
}
if (eventType || reason || objectKind) {
// Parse the body which is OTLP kvlistValue JSON
let bodyObj: JSONObject | null = null;
try {
if (typeof log.body === "string") {
bodyObj = JSON.parse(log.body) as JSONObject;
}
} catch {
continue;
}
if (!bodyObj) {
continue;
}
// The body has a top-level kvlistValue with "type" (ADDED/MODIFIED) and "object" keys
const topKvList = bodyObj["kvlistValue"] as JSONObject | undefined;
if (!topKvList) {
continue;
}
// Get the "object" which is the actual k8s Event
const objectKvListRaw = getKvValue(topKvList, "object");
if (!objectKvListRaw || typeof objectKvListRaw === "string") {
continue;
}
const objectKvList = objectKvListRaw as unknown as JSONObject;
const eventType: string = getKvValue(objectKvList, "type") || "";
const reason: string = getKvValue(objectKvList, "reason") || "";
const note: string = getKvValue(objectKvList, "note") || "";
// Get object details from "regarding" sub-object
const objectKind: string = getNestedKvValue(objectKvList, "regarding", "kind") || "";
const objectName: string = getNestedKvValue(objectKvList, "regarding", "name") || "";
const namespace: string = getNestedKvValue(objectKvList, "regarding", "namespace") ||
getNestedKvValue(objectKvList, "metadata", "namespace") || "";
if (eventType || reason) {
k8sEvents.push({
timestamp: log.time
? OneUptimeDate.getDateAsLocalFormattedString(log.time)
@@ -133,7 +208,7 @@ const KubernetesClusterEvents: FunctionComponent<
objectKind: objectKind || "Unknown",
objectName: objectName || "Unknown",
namespace: namespace || "default",
message: log.body || "",
message: note || "",
});
}
}

View File

@@ -72,7 +72,7 @@ const KubernetesClusterNodes: FunctionComponent<
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const nodeName: string =
(attributes["k8s.node.name"] as string) || "Unknown Node";
(attributes["resource.k8s.node.name"] as string) || "Unknown Node";
return { title: nodeName };
};
@@ -88,7 +88,7 @@ const KubernetesClusterNodes: FunctionComponent<
filterData: {
metricName: "k8s.node.cpu.utilization",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
@@ -112,7 +112,7 @@ const KubernetesClusterNodes: FunctionComponent<
filterData: {
metricName: "k8s.node.memory.usage",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
@@ -136,7 +136,7 @@ const KubernetesClusterNodes: FunctionComponent<
filterData: {
metricName: "k8s.node.filesystem.usage",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
@@ -158,9 +158,10 @@ const KubernetesClusterNodes: FunctionComponent<
},
metricQueryData: {
filterData: {
metricName: "k8s.node.network.io.receive",
metricName: "k8s.node.network.io",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"resource.k8s.cluster.name": clusterIdentifier,
"metricAttributes.direction": "receive",
},
aggegationType: AggregationType.Avg,
aggregateBy: {},

View File

@@ -72,9 +72,9 @@ const KubernetesClusterPods: FunctionComponent<
const attributes: Record<string, unknown> =
(data["attributes"] as Record<string, unknown>) || {};
const podName: string =
(attributes["k8s.pod.name"] as string) || "Unknown Pod";
(attributes["resource.k8s.pod.name"] as string) || "Unknown Pod";
const namespace: string =
(attributes["k8s.namespace.name"] as string) || "";
(attributes["resource.k8s.namespace.name"] as string) || "";
return { title: namespace ? `${namespace}/${podName}` : podName };
};
@@ -90,7 +90,7 @@ const KubernetesClusterPods: FunctionComponent<
filterData: {
metricName: "k8s.pod.cpu.utilization",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
@@ -114,7 +114,7 @@ const KubernetesClusterPods: FunctionComponent<
filterData: {
metricName: "k8s.pod.memory.usage",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"resource.k8s.cluster.name": clusterIdentifier,
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
@@ -136,9 +136,10 @@ const KubernetesClusterPods: FunctionComponent<
},
metricQueryData: {
filterData: {
metricName: "k8s.pod.network.io.receive",
metricName: "k8s.pod.network.io",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"resource.k8s.cluster.name": clusterIdentifier,
"metricAttributes.direction": "receive",
},
aggegationType: AggregationType.Avg,
aggregateBy: {},
@@ -160,9 +161,10 @@ const KubernetesClusterPods: FunctionComponent<
},
metricQueryData: {
filterData: {
metricName: "k8s.pod.network.io.transmit",
metricName: "k8s.pod.network.io",
attributes: {
"k8s.cluster.name": clusterIdentifier,
"resource.k8s.cluster.name": clusterIdentifier,
"metricAttributes.direction": "transmit",
},
aggegationType: AggregationType.Avg,
aggregateBy: {},

View File

@@ -9,11 +9,9 @@ import { Route as PageRoute, Routes } from "react-router-dom";
// Pages
import ExceptionsUnresolved from "../Pages/Exceptions/Unresolved";
import ExceptionsResolved from "../Pages/Exceptions/Resolved";
import ExceptionsArchived from "../Pages/Exceptions/Archived";
import ExceptionsDocumentationPage from "../Pages/Exceptions/Documentation";
import ExceptionView from "../Pages/Exceptions/View/Index";
const ExceptionsRoutes: FunctionComponent<ComponentProps> = (
@@ -61,6 +59,17 @@ const ExceptionsRoutes: FunctionComponent<ComponentProps> = (
/>
}
/>
<PageRoute
path={ExceptionsRoutePath[PageMap.EXCEPTIONS_DOCUMENTATION] || ""}
element={
<ExceptionsDocumentationPage
{...props}
pageRoute={
RouteMap[PageMap.EXCEPTIONS_DOCUMENTATION] as Route
}
/>
}
/>
</PageRoute>
{/* Exception View - separate from main layout */}

View File

@@ -31,6 +31,11 @@ export function getExceptionsBreadcrumbs(
"Exceptions",
"View Exception",
]),
...BuildBreadcrumbLinksByTitles(PageMap.EXCEPTIONS_DOCUMENTATION, [
"Project",
"Exceptions",
"Documentation",
]),
};
return breadcrumpLinksMap[path];
}

View File

@@ -471,6 +471,7 @@ enum PageMap {
EXCEPTIONS_ARCHIVED = "EXCEPTIONS_ARCHIVED",
EXCEPTIONS_VIEW_ROOT = "EXCEPTIONS_VIEW_ROOT",
EXCEPTIONS_VIEW = "EXCEPTIONS_VIEW",
EXCEPTIONS_DOCUMENTATION = "EXCEPTIONS_DOCUMENTATION",
// Push Logs in resource views
}

View File

@@ -117,6 +117,7 @@ export const ExceptionsRoutePath: Dictionary<string> = {
[PageMap.EXCEPTIONS_ARCHIVED]: "archived",
[PageMap.EXCEPTIONS_VIEW_ROOT]: "",
[PageMap.EXCEPTIONS_VIEW]: `${RouteParams.ModelID}`,
[PageMap.EXCEPTIONS_DOCUMENTATION]: "documentation",
};
export const DashboardsRoutePath: Dictionary<string> = {
@@ -2579,6 +2580,12 @@ const RouteMap: Dictionary<Route> = {
ExceptionsRoutePath[PageMap.EXCEPTIONS_VIEW]
}`,
),
[PageMap.EXCEPTIONS_DOCUMENTATION]: new Route(
`/dashboard/${RouteParams.ProjectID}/exceptions/${
ExceptionsRoutePath[PageMap.EXCEPTIONS_DOCUMENTATION]
}`,
),
};
export class RouteUtil {

View File

@@ -39,7 +39,7 @@ const NavBarMenu: FunctionComponent<ComponentProps> = (
);
return (
<div className="absolute left-0 z-10 mt-8 w-screen max-w-5xl transform px-2 sm:px-0">
<div className="absolute left-0 z-50 mt-8 w-screen max-w-5xl transform px-2 sm:px-0">
<div className="overflow-hidden rounded-2xl shadow-xl ring-1 ring-black ring-opacity-5 bg-white">
{/* Sections */}
<div className="p-6">
@@ -127,7 +127,7 @@ const NavBarMenu: FunctionComponent<ComponentProps> = (
return (
<div
className={`absolute left-1/2 z-10 mt-8 w-screen max-w-md -translate-x-1/2 transform px-2 sm:px-0 ${maxWidthClass}`}
className={`absolute left-1/2 z-50 mt-8 w-screen max-w-md -translate-x-1/2 transform px-2 sm:px-0 ${maxWidthClass}`}
>
<div className="overflow-hidden rounded-2xl shadow-xl ring-1 ring-black ring-opacity-5 bg-white">
{/* Menu Items */}