feat: Add Profiles feature with routing, documentation, and UI components

- Implemented ProfilesRoutes for navigating to profiles-related pages.
- Created Profiles page with service count and documentation display.
- Added Profiles documentation page for ingestion setup.
- Developed Profile view page for detailed profiling information.
- Introduced Profiles layout and side menu for navigation.
- Enhanced breadcrumbs for profiles navigation.
- Updated telemetry type to include profiles.
- Refactored ArgumentsForm to improve UI for additional queries.
- Adjusted DashboardToolbar and DashboardView styles for consistency.
This commit is contained in:
Nawaz Dhandala
2026-03-27 10:38:13 +00:00
parent 5ecf8ce881
commit 1d78ec8922
22 changed files with 564 additions and 153 deletions

View File

@@ -92,6 +92,15 @@ const ExceptionsRoutes: React.LazyExoticComponent<
};
});
});
const ProfilesRoutes: React.LazyExoticComponent<
AllRoutesModule["ProfilesRoutes"]
> = lazy(() => {
return import("./Routes/AllRoutes").then((m: AllRoutesModule) => {
return {
default: m.ProfilesRoutes,
};
});
});
const IncidentsRoutes: React.LazyExoticComponent<
AllRoutesModule["IncidentsRoutes"]
> = lazy(() => {
@@ -507,6 +516,12 @@ const App: () => JSX.Element = () => {
element={<TracesRoutes {...commonPageProps} />}
/>
{/* Profiles */}
<PageRoute
path={RouteMap[PageMap.PROFILES_ROOT]?.toString() || ""}
element={<ProfilesRoutes {...commonPageProps} />}
/>
{/* Monitors */}
<PageRoute
path={RouteMap[PageMap.MONITORS_ROOT]?.toString() || ""}

View File

@@ -264,106 +264,90 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
}
return (
<div className="mt-3">
<CollapsibleSection
title="Additional Queries"
description="Overlay more metric series on the same chart"
variant="bordered"
defaultCollapsed={multiQueryConfigs.length === 0}
>
<div>
{multiQueryConfigs.length === 0 && (
<p className="text-sm text-gray-400 mb-3">
No additional queries yet. Add one to overlay multiple metrics
on the same chart.
</p>
)}
<div className="mt-4">
{multiQueryConfigs.map(
(queryConfig: MetricQueryConfigData, index: number) => {
return (
<div
key={index}
className="mb-4 p-3 border border-gray-200 rounded-lg bg-gray-50"
>
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Query {index + 2}
</span>
<Button
title="Remove"
buttonSize={ButtonSize.Small}
buttonStyle={ButtonStyleType.DANGER_OUTLINE}
icon={IconProp.Trash}
onClick={() => {
const updated: Array<MetricQueryConfigData> = [
...multiQueryConfigs,
];
updated.splice(index, 1);
setMultiQueryConfigs(updated);
setComponent({
...component,
arguments: {
...((component.arguments as JSONObject) || {}),
metricQueryConfigs: updated as any,
},
});
}}
/>
</div>
<MetricQueryConfig
data={queryConfig}
metricTypes={props.metrics.metricTypes}
telemetryAttributes={props.metrics.telemetryAttributes}
hideCard={true}
onChange={(data: MetricQueryConfigData) => {
const updated: Array<MetricQueryConfigData> = [
...multiQueryConfigs,
];
updated[index] = data;
setMultiQueryConfigs(updated);
setComponent({
...component,
arguments: {
...((component.arguments as JSONObject) || {}),
metricQueryConfigs: updated as any,
},
});
}}
/>
</div>
);
},
)}
{multiQueryConfigs.map(
(queryConfig: MetricQueryConfigData, index: number) => {
return (
<div
key={index}
className="mb-4 p-3 border border-gray-200 rounded-lg bg-gray-50"
>
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">
Query {index + 2}
</span>
<Button
title="Remove"
buttonSize={ButtonSize.Small}
buttonStyle={ButtonStyleType.DANGER_OUTLINE}
icon={IconProp.Trash}
onClick={() => {
const updated: Array<MetricQueryConfigData> = [
...multiQueryConfigs,
];
updated.splice(index, 1);
setMultiQueryConfigs(updated);
setComponent({
...component,
arguments: {
...((component.arguments as JSONObject) || {}),
metricQueryConfigs: updated as any,
},
});
}}
/>
</div>
<MetricQueryConfig
data={queryConfig}
metricTypes={props.metrics.metricTypes}
telemetryAttributes={props.metrics.telemetryAttributes}
hideCard={true}
onChange={(data: MetricQueryConfigData) => {
const updated: Array<MetricQueryConfigData> = [
...multiQueryConfigs,
];
updated[index] = data;
setMultiQueryConfigs(updated);
setComponent({
...component,
arguments: {
...((component.arguments as JSONObject) || {}),
metricQueryConfigs: updated as any,
},
});
}}
/>
</div>
);
<Button
title="Add Query"
buttonSize={ButtonSize.Small}
buttonStyle={ButtonStyleType.OUTLINE}
icon={IconProp.Add}
onClick={() => {
const newQuery: MetricQueryConfigData = {
metricQueryData: {
filterData: {},
groupBy: undefined,
},
)}
<Button
title="Add Query"
buttonSize={ButtonSize.Small}
buttonStyle={ButtonStyleType.OUTLINE}
icon={IconProp.Add}
onClick={() => {
const newQuery: MetricQueryConfigData = {
metricQueryData: {
filterData: {},
groupBy: undefined,
},
};
const updated: Array<MetricQueryConfigData> = [
...multiQueryConfigs,
newQuery,
];
setMultiQueryConfigs(updated);
setComponent({
...component,
arguments: {
...((component.arguments as JSONObject) || {}),
metricQueryConfigs: updated as any,
},
});
}}
/>
</div>
</CollapsibleSection>
};
const updated: Array<MetricQueryConfigData> = [
...multiQueryConfigs,
newQuery,
];
setMultiQueryConfigs(updated);
setComponent({
...component,
arguments: {
...((component.arguments as JSONObject) || {}),
metricQueryConfigs: updated as any,
},
});
}}
/>
</div>
);
};
@@ -392,18 +376,19 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
variant="bordered"
defaultCollapsed={shouldCollapse}
>
<div>{renderSectionForm(sectionGroup)}</div>
<div>
{renderSectionForm(sectionGroup)}
{/* Render multi-query UI inside the Data Source section */}
{sectionGroup.section.name === "Data Source" &&
renderMultiQuerySection()}
</div>
</CollapsibleSection>
{/* Render multi-query section after the Data Source section */}
{sectionGroup.section.name === "Data Source" &&
renderMultiQuerySection()}
</div>
);
},
)}
{/* If no Data Source section but has multi-query, render at end */}
{/* If no Data Source section exists, render multi-query at end */}
{!sectionGroups.some(
(g: SectionGroup) => g.section.name === "Data Source",
) && renderMultiQuerySection()}

View File

@@ -274,7 +274,7 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
width: `calc(100% - ${sideBarWidth}px)`,
background: isEditMode
? "linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)"
: "#fafbfc",
: "#f8f9fb",
}}
>
<DashboardToolbar

View File

@@ -56,25 +56,16 @@ const DashboardToolbar: FunctionComponent<ComponentProps> = (
return (
<div
className="mx-3 mt-3 mb-2 rounded-lg bg-white border border-gray-200"
className="mx-4 mt-4 mb-3 rounded-xl bg-white border border-gray-100"
style={{
boxShadow:
"0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.04)",
"0 1px 3px 0 rgba(0, 0, 0, 0.04), 0 1px 2px -1px rgba(0, 0, 0, 0.03)",
}}
>
{/* Accent top bar */}
<div
className="h-0.5 rounded-t-lg"
style={{
background: isEditMode
? "linear-gradient(90deg, #3b82f6 0%, #6366f1 50%, #8b5cf6 100%)"
: "linear-gradient(90deg, #6366f1 0%, #8b5cf6 100%)",
}}
></div>
{/* Top row: Dashboard name + action buttons */}
<div className="flex items-center justify-between px-5 py-3">
<div className="flex items-center justify-between px-5 py-4">
<div className="flex items-center gap-3 min-w-0">
<h1 className="text-lg font-semibold text-gray-900 truncate">
<h1 className="text-base font-semibold text-gray-800 truncate">
{props.dashboardName}
</h1>
{isEditMode && (
@@ -84,7 +75,7 @@ const DashboardToolbar: FunctionComponent<ComponentProps> = (
</span>
)}
{hasComponents && !isEditMode && (
<span className="text-xs text-gray-400 tabular-nums">
<span className="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium text-gray-500 bg-gray-50">
{props.dashboardViewConfig.components.length} widget
{props.dashboardViewConfig.components.length !== 1 ? "s" : ""}
</span>
@@ -100,7 +91,7 @@ const DashboardToolbar: FunctionComponent<ComponentProps> = (
</div>
{!isSaving && (
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1">
{isEditMode ? (
<>
<MoreMenu menuIcon={IconProp.Add} text="Add Widget">
@@ -229,7 +220,7 @@ const DashboardToolbar: FunctionComponent<ComponentProps> = (
tooltip="Full Screen"
/>
<div className="w-px h-6 bg-gray-200 mx-0.5"></div>
<div className="w-px h-5 bg-gray-200 mx-0.5"></div>
<Button
icon={IconProp.Pencil}
@@ -253,8 +244,8 @@ const DashboardToolbar: FunctionComponent<ComponentProps> = (
{/* Bottom row: Time range + variables (only when components exist and not in edit mode) */}
{hasComponents && !isEditMode && (
<div className="flex items-center gap-3 px-5 pb-3 pt-0 flex-wrap">
<div>
<div className="flex items-center gap-3 px-5 pb-4 pt-0 flex-wrap border-t border-gray-50">
<div className="pt-3">
<RangeStartAndEndDateView
dashboardStartAndEndDate={props.startAndEndDate}
onChange={(startAndEndDate: RangeStartAndEndDateTime) => {
@@ -268,11 +259,13 @@ const DashboardToolbar: FunctionComponent<ComponentProps> = (
props.variables.length > 0 &&
props.onVariableValueChange && (
<>
<div className="w-px h-5 bg-gray-200"></div>
<DashboardVariableSelector
variables={props.variables}
onVariableValueChange={props.onVariableValueChange}
/>
<div className="w-px h-5 bg-gray-200 mt-3"></div>
<div className="pt-3">
<DashboardVariableSelector
variables={props.variables}
onVariableValueChange={props.onVariableValueChange}
/>
</div>
</>
)}
</div>

View File

@@ -34,6 +34,7 @@ import {
buildKubernetesMonitorConfig,
} from "Common/Types/Monitor/KubernetesAlertTemplates";
import { KubernetesMetricDefinition } from "Common/Types/Monitor/KubernetesMetricCatalog";
import MonitorCriteria from "Common/Types/Monitor/MonitorCriteria";
import Navigation from "Common/UI/Utils/Navigation";
export type KubernetesFormMode = "quick" | "custom" | "advanced";
@@ -43,9 +44,16 @@ export interface ComponentProps {
onChange: (
monitorStepKubernetesMonitor: MonitorStepKubernetesMonitor,
) => void;
onMonitorCriteriaChange?: ((criteria: MonitorCriteria) => void) | undefined;
onModeChange?: ((mode: KubernetesFormMode) => void) | undefined;
initialTemplateId?: string | undefined;
initialClusterId?: string | undefined;
// These IDs are needed to build proper criteria from templates
onlineMonitorStatusId?: ObjectID | undefined;
offlineMonitorStatusId?: ObjectID | undefined;
defaultIncidentSeverityId?: ObjectID | undefined;
defaultAlertSeverityId?: ObjectID | undefined;
monitorName?: string | undefined;
}
const resourceScopeOptions: Array<DropdownOption> = [
@@ -222,25 +230,40 @@ const KubernetesMonitorStepForm: FunctionComponent<ComponentProps> = (
monitorStepKubernetesMonitor.clusterIdentifier;
/*
* Get a dummy monitor step from the template to extract the kubernetes config
* Build even without a cluster so the metricViewConfig is populated for the METRIC dropdown
* Use real monitor status and severity IDs if available,
* so the template criteria are properly configured
*/
const dummyStep: MonitorStep = template.getMonitorStep({
const onlineMonitorStatusId: ObjectID =
props.onlineMonitorStatusId || ObjectID.generate();
const offlineMonitorStatusId: ObjectID =
props.offlineMonitorStatusId || ObjectID.generate();
const defaultIncidentSeverityId: ObjectID =
props.defaultIncidentSeverityId || ObjectID.generate();
const defaultAlertSeverityId: ObjectID =
props.defaultAlertSeverityId || ObjectID.generate();
const monitorName: string = props.monitorName || template.name;
const templateStep: MonitorStep = template.getMonitorStep({
clusterIdentifier: clusterIdentifier || "",
onlineMonitorStatusId: ObjectID.generate(),
offlineMonitorStatusId: ObjectID.generate(),
defaultIncidentSeverityId: ObjectID.generate(),
defaultAlertSeverityId: ObjectID.generate(),
monitorName: template.name,
onlineMonitorStatusId,
offlineMonitorStatusId,
defaultIncidentSeverityId,
defaultAlertSeverityId,
monitorName,
});
// Extract the kubernetes monitor config
if (dummyStep.data?.kubernetesMonitor) {
if (templateStep.data?.kubernetesMonitor) {
props.onChange({
...dummyStep.data.kubernetesMonitor,
...templateStep.data.kubernetesMonitor,
clusterIdentifier: clusterIdentifier || "",
});
}
// Also apply the template's criteria (alert rules, thresholds, incidents, etc.)
if (templateStep.data?.monitorCriteria && props.onMonitorCriteriaChange) {
props.onMonitorCriteriaChange(templateStep.data.monitorCriteria);
}
};
const handleCustomMetricSelection: (

View File

@@ -110,6 +110,12 @@ export interface ComponentProps {
allMonitorSteps: MonitorSteps;
probes: Array<Probe>;
monitorId?: ObjectID | undefined; // this is used to populate secrets when testing the monitor.
// IDs needed for Kubernetes template criteria
onlineMonitorStatusId?: ObjectID | undefined;
offlineMonitorStatusId?: ObjectID | undefined;
defaultIncidentSeverityId?: ObjectID | undefined;
defaultAlertSeverityId?: ObjectID | undefined;
monitorName?: string | undefined;
}
const MonitorStepElement: FunctionComponent<ComponentProps> = (
@@ -760,6 +766,15 @@ return {
monitorStep.setKubernetesMonitor(value);
props.onChange?.(MonitorStep.clone(monitorStep));
}}
onMonitorCriteriaChange={(criteria: MonitorCriteria) => {
monitorStep.setMonitorCriteria(criteria);
props.onChange?.(MonitorStep.clone(monitorStep));
}}
onlineMonitorStatusId={props.onlineMonitorStatusId}
offlineMonitorStatusId={props.offlineMonitorStatusId}
defaultIncidentSeverityId={props.defaultIncidentSeverityId}
defaultAlertSeverityId={props.defaultAlertSeverityId}
monitorName={props.monitorName}
/>
</Card>
)}

View File

@@ -74,6 +74,16 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
const [probes, setProbes] = React.useState<Array<Probe>>([]);
// IDs needed for Kubernetes template criteria
const [onlineMonitorStatusId, setOnlineMonitorStatusId] =
React.useState<ObjectID | undefined>(undefined);
const [offlineMonitorStatusId, setOfflineMonitorStatusId] =
React.useState<ObjectID | undefined>(undefined);
const [defaultIncidentSeverityId, setDefaultIncidentSeverityId] =
React.useState<ObjectID | undefined>(undefined);
const [defaultAlertSeverityId, setDefaultAlertSeverityId] =
React.useState<ObjectID | undefined>(undefined);
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [error, setError] = React.useState<string>();
@@ -109,6 +119,23 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
};
}),
);
// Extract online (operational) and offline status IDs for template criteria
const onlineStatus: MonitorStatus | undefined =
monitorStatusList.data.find((i: MonitorStatus) => {
return i.isOperationalState;
});
const offlineStatus: MonitorStatus | undefined =
monitorStatusList.data.find((i: MonitorStatus) => {
return i.isOfflineState;
});
if (onlineStatus?._id) {
setOnlineMonitorStatusId(new ObjectID(onlineStatus._id));
}
if (offlineStatus?._id) {
setOfflineMonitorStatusId(new ObjectID(offlineStatus._id));
}
}
const incidentSeverityList: ListResult<IncidentSeverity> =
@@ -162,6 +189,11 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
};
}),
);
// Use the first (highest priority) severity as default for templates
if (incidentSeverityList.data.length > 0 && incidentSeverityList.data[0]?._id) {
setDefaultIncidentSeverityId(new ObjectID(incidentSeverityList.data[0]._id));
}
}
if (alertSeverityList.data) {
@@ -173,6 +205,11 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
};
}),
);
// Use the first (highest priority) severity as default for templates
if (alertSeverityList.data.length > 0 && alertSeverityList.data[0]?._id) {
setDefaultAlertSeverityId(new ObjectID(alertSeverityList.data[0]._id));
}
}
if (onCallPolicyList.data) {
@@ -356,6 +393,11 @@ const MonitorStepsElement: FunctionComponent<ComponentProps> = (
value={i}
probes={probes}
monitorId={props.monitorId}
onlineMonitorStatusId={onlineMonitorStatusId}
offlineMonitorStatusId={offlineMonitorStatusId}
defaultIncidentSeverityId={defaultIncidentSeverityId}
defaultAlertSeverityId={defaultAlertSeverityId}
monitorName={props.monitorName}
/*
* onDelete={() => {
* // remove the criteria filter

View File

@@ -124,6 +124,17 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
iconColor: "yellow",
category: "Observability",
},
{
title: "Profiles",
description: "CPU and memory profiling.",
route: RouteUtil.populateRouteParams(
RouteMap[PageMap.PROFILES] as Route,
),
activeRoute: RouteMap[PageMap.PROFILES],
icon: IconProp.Fire,
iconColor: "red",
category: "Observability",
},
{
title: "Exceptions",
description: "Catch and fix bugs early.",

View File

@@ -23,7 +23,7 @@ import Dropdown, {
} from "Common/UI/Components/Dropdown/Dropdown";
import Protocol from "Common/Types/API/Protocol";
export type TelemetryType = "logs" | "metrics" | "traces" | "exceptions";
export type TelemetryType = "logs" | "metrics" | "traces" | "exceptions" | "profiles";
export interface ComponentProps {
telemetryType?: TelemetryType | undefined;
@@ -893,6 +893,7 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
metrics: "Metrics Ingestion Setup",
traces: "Trace Ingestion Setup",
exceptions: "Exception Tracking Setup",
profiles: "Profiles Ingestion Setup",
};
const descriptionForType: Record<TelemetryType, string> = {
@@ -903,6 +904,8 @@ const TelemetryDocumentation: FunctionComponent<ComponentProps> = (
"Send distributed traces from your application to OneUptime using OpenTelemetry SDKs.",
exceptions:
"Capture and track exceptions from your application using OpenTelemetry SDKs.",
profiles:
"Send continuous profiling data from your application to OneUptime using OpenTelemetry SDKs.",
};
const installSnippet: { code: string; language: string } = useMemo(() => {

View File

@@ -880,6 +880,11 @@ const KubernetesClusterOverview: FunctionComponent<
<span className="text-sm font-medium text-gray-900 truncate group-hover:text-indigo-700">
{pod.name}
</span>
{pod.namespace && (
<span className="flex-shrink-0 inline-flex px-1.5 py-0.5 text-xs font-medium rounded bg-indigo-50 text-indigo-600">
{pod.namespace}
</span>
)}
</div>
<span className="flex-shrink-0 text-sm font-semibold text-gray-700 tabular-nums ml-2">
{KubernetesResourceUtils.formatCpuValue(
@@ -887,13 +892,8 @@ const KubernetesClusterOverview: FunctionComponent<
)}
</span>
</div>
<div className="flex items-center gap-2 pl-6">
{pod.namespace && (
<span className="flex-shrink-0 inline-flex px-1.5 py-0.5 text-xs font-medium rounded bg-indigo-50 text-indigo-600">
{pod.namespace}
</span>
)}
<div className="flex-1 bg-gray-100 rounded-full h-1.5">
<div className="pl-6">
<div className="w-full bg-gray-100 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all duration-300 ${
pct > 80
@@ -970,6 +970,11 @@ const KubernetesClusterOverview: FunctionComponent<
<span className="text-sm font-medium text-gray-900 truncate group-hover:text-indigo-700">
{pod.name}
</span>
{pod.namespace && (
<span className="flex-shrink-0 inline-flex px-1.5 py-0.5 text-xs font-medium rounded bg-indigo-50 text-indigo-600">
{pod.namespace}
</span>
)}
</div>
<span className="flex-shrink-0 text-sm font-semibold text-gray-700 tabular-nums ml-2">
{KubernetesResourceUtils.formatMemoryValue(
@@ -977,13 +982,8 @@ const KubernetesClusterOverview: FunctionComponent<
)}
</span>
</div>
<div className="flex items-center gap-2 pl-6">
{pod.namespace && (
<span className="flex-shrink-0 inline-flex px-1.5 py-0.5 text-xs font-medium rounded bg-indigo-50 text-indigo-600">
{pod.namespace}
</span>
)}
<div className="flex-1 bg-gray-100 rounded-full h-1.5">
<div className="pl-6">
<div className="w-full bg-gray-100 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all duration-300 ${
memPercent > 85

View File

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

View File

@@ -0,0 +1,73 @@
import PageComponentProps from "../PageComponentProps";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import TelemetryDocumentation from "../../Components/Telemetry/Documentation";
import React, {
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import Service from "Common/Models/DatabaseModels/Service";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import API from "Common/UI/Utils/API/API";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
const ProfilesPage: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
): ReactElement => {
const disableTelemetryForThisProject: boolean =
props.currentProject?.reseller?.enableTelemetryFeatures === false;
const [serviceCount, setServiceCount] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const fetchServiceCount: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
const count: number = await ModelAPI.count({
modelType: Service,
query: {},
});
setServiceCount(count);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchServiceCount().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
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." />
);
}
if (isLoading) {
return <PageLoader isVisible={true} />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (serviceCount === 0) {
return <TelemetryDocumentation telemetryType="profiles" />;
}
return (
<InfoCard
title="Profiles"
value="Continuous profiling data from your services will appear here. Profiles help you understand CPU, memory, and allocation hotspots in your applications. Use the OpenTelemetry eBPF profiler agent or async-profiler with OTLP export to send profiling data."
/>
);
};
export default ProfilesPage;

View File

@@ -0,0 +1,26 @@
import { getProfilesBreadcrumbs } from "../../Utils/Breadcrumbs";
import { RouteUtil } from "../../Utils/RouteMap";
import PageComponentProps from "../PageComponentProps";
import SideMenu from "./SideMenu";
import Page from "Common/UI/Components/Page/Page";
import Navigation from "Common/UI/Utils/Navigation";
import React, { FunctionComponent, ReactElement } from "react";
import { Outlet } from "react-router-dom";
const ProfilesLayout: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
return (
<Page
title="Profiles"
breadcrumbLinks={getProfilesBreadcrumbs(path)}
sideMenu={<SideMenu />}
>
<Outlet />
</Page>
);
};
export default ProfilesLayout;

View File

@@ -0,0 +1,45 @@
import PageMap from "../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import IconProp from "Common/Types/Icon/IconProp";
import SideMenu, {
SideMenuSectionProps,
} from "Common/UI/Components/SideMenu/SideMenu";
import React, { FunctionComponent, ReactElement } from "react";
const DashboardSideMenu: FunctionComponent = (): ReactElement => {
const sections: SideMenuSectionProps[] = [
{
title: "Profiles",
items: [
{
link: {
title: "All Profiles",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.PROFILES] as Route,
),
},
icon: IconProp.Fire,
},
],
},
{
title: "Documentation",
items: [
{
link: {
title: "Documentation",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.PROFILES_DOCUMENTATION] as Route,
),
},
icon: IconProp.Book,
},
],
},
];
return <SideMenu sections={sections} />;
};
export default DashboardSideMenu;

View File

@@ -0,0 +1,19 @@
import PageComponentProps from "../../PageComponentProps";
import Navigation from "Common/UI/Utils/Navigation";
import React, { FunctionComponent, ReactElement } from "react";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
const ProfileViewPage: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const profileId: string = Navigation.getLastParamAsString(0);
return (
<InfoCard
title={`Profile: ${profileId}`}
value="Profile flamegraph visualization will be available here. This view will show CPU, memory, and allocation hotspots as an interactive flamegraph with function-level detail."
/>
);
};
export default ProfileViewPage;

View File

@@ -0,0 +1,23 @@
import { getProfilesBreadcrumbs } from "../../../Utils/Breadcrumbs";
import { RouteUtil } from "../../../Utils/RouteMap";
import PageComponentProps from "../../PageComponentProps";
import Page from "Common/UI/Components/Page/Page";
import Navigation from "Common/UI/Utils/Navigation";
import React, { FunctionComponent, ReactElement } from "react";
import { Outlet } from "react-router-dom";
const ProfilesViewLayout: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
return (
<Page
title="Profile Explorer"
breadcrumbLinks={getProfilesBreadcrumbs(path)}
>
<Outlet />
</Page>
);
};
export default ProfilesViewLayout;

View File

@@ -3,6 +3,7 @@ export { default as LogsRoutes } from "./LogsRoutes";
export { default as MetricsRoutes } from "./MetricsRoutes";
export { default as TracesRoutes } from "./TracesRoutes";
export { default as ExceptionsRoutes } from "./ExceptionsRoutes";
export { default as ProfilesRoutes } from "./ProfilesRoutes";
// Incident management
export { default as IncidentsRoutes } from "./IncidentsRoutes";

View File

@@ -0,0 +1,70 @@
import ComponentProps from "../Pages/PageComponentProps";
import ProfilesLayout from "../Pages/Profiles/Layout";
import ProfilesViewLayout from "../Pages/Profiles/View/Layout";
import PageMap from "../Utils/PageMap";
import RouteMap, { RouteUtil, ProfilesRoutePath } from "../Utils/RouteMap";
import Route from "Common/Types/API/Route";
import React, { FunctionComponent, ReactElement } from "react";
import { Route as PageRoute, Routes } from "react-router-dom";
// Pages
import ProfilesPage from "../Pages/Profiles/Index";
import ProfilesDocumentationPage from "../Pages/Profiles/Documentation";
import ProfileViewPage from "../Pages/Profiles/View/Index";
const ProfilesRoutes: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
return (
<Routes>
<PageRoute path="/" element={<ProfilesLayout {...props} />}>
<PageRoute
index
element={
<ProfilesPage
{...props}
pageRoute={RouteMap[PageMap.PROFILES] as Route}
/>
}
/>
<PageRoute
path={ProfilesRoutePath[PageMap.PROFILES_DOCUMENTATION] || ""}
element={
<ProfilesDocumentationPage
{...props}
pageRoute={RouteMap[PageMap.PROFILES_DOCUMENTATION] as Route}
/>
}
/>
</PageRoute>
{/* Profile View */}
<PageRoute
path={ProfilesRoutePath[PageMap.PROFILE_VIEW] || ""}
element={<ProfilesViewLayout {...props} />}
>
<PageRoute
index
element={
<ProfileViewPage
{...props}
pageRoute={RouteMap[PageMap.PROFILE_VIEW] as Route}
/>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.PROFILE_VIEW)}
element={
<ProfileViewPage
{...props}
pageRoute={RouteMap[PageMap.PROFILE_VIEW] as Route}
/>
}
/>
</PageRoute>
</Routes>
);
};
export default ProfilesRoutes;

View File

@@ -0,0 +1,21 @@
import PageMap from "../PageMap";
import { BuildBreadcrumbLinksByTitles } from "./Helper";
import Dictionary from "Common/Types/Dictionary";
import Link from "Common/Types/Link";
export function getProfilesBreadcrumbs(path: string): Array<Link> | undefined {
const breadcrumpLinksMap: Dictionary<Link[]> = {
...BuildBreadcrumbLinksByTitles(PageMap.PROFILES, ["Project", "Profiles"]),
...BuildBreadcrumbLinksByTitles(PageMap.PROFILE_VIEW, [
"Project",
"Profiles",
"Profile Explorer",
]),
...BuildBreadcrumbLinksByTitles(PageMap.PROFILES_DOCUMENTATION, [
"Project",
"Profiles",
"Documentation",
]),
};
return breadcrumpLinksMap[path];
}

View File

@@ -15,3 +15,4 @@ export * from "./KubernetesBreadcrumbs";
export * from "./DashboardBreadCrumbs";
export * from "./AIAgentTasksBreadcrumbs";
export * from "./ExceptionsBreadcrumbs";
export * from "./ProfilesBreadcrumbs";

View File

@@ -24,6 +24,12 @@ enum PageMap {
TRACE_VIEW = "TRACE_VIEW",
TRACES_DOCUMENTATION = "TRACES_DOCUMENTATION",
// Profiles (standalone product)
PROFILES_ROOT = "PROFILES_ROOT",
PROFILES = "PROFILES",
PROFILE_VIEW = "PROFILE_VIEW",
PROFILES_DOCUMENTATION = "PROFILES_DOCUMENTATION",
HOME = "HOME",
HOME_NOT_OPERATIONAL_MONITORS = "HOME_NOT_OPERATIONAL_MONITORS",
HOME_ONGOING_SCHEDULED_MAINTENANCE_EVENTS = "HOME_ONGOING_SCHEDULED_MAINTENANCE_EVENTS",

View File

@@ -133,6 +133,13 @@ export const TracesRoutePath: Dictionary<string> = {
[PageMap.TRACES_DOCUMENTATION]: "documentation",
};
// Profiles product routes
export const ProfilesRoutePath: Dictionary<string> = {
[PageMap.PROFILES]: "",
[PageMap.PROFILE_VIEW]: `view/${RouteParams.ModelID}`,
[PageMap.PROFILES_DOCUMENTATION]: "documentation",
};
export const ExceptionsRoutePath: Dictionary<string> = {
[PageMap.EXCEPTIONS]: "unresolved",
[PageMap.EXCEPTIONS_UNRESOLVED]: "unresolved",
@@ -2282,6 +2289,27 @@ const RouteMap: Dictionary<Route> = {
}`,
),
// Profiles Product Routes
[PageMap.PROFILES_ROOT]: new Route(
`/dashboard/${RouteParams.ProjectID}/profiles/*`,
),
[PageMap.PROFILES]: new Route(
`/dashboard/${RouteParams.ProjectID}/profiles`,
),
[PageMap.PROFILE_VIEW]: new Route(
`/dashboard/${RouteParams.ProjectID}/profiles/${
ProfilesRoutePath[PageMap.PROFILE_VIEW]
}`,
),
[PageMap.PROFILES_DOCUMENTATION]: new Route(
`/dashboard/${RouteParams.ProjectID}/profiles/${
ProfilesRoutePath[PageMap.PROFILES_DOCUMENTATION]
}`,
),
// User Settings Routes
[PageMap.USER_SETTINGS_ROOT]: new Route(
`/dashboard/${RouteParams.ProjectID}/user-settings/*`,