mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
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:
@@ -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() || ""}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
73
App/FeatureSet/Dashboard/src/Pages/Profiles/Index.tsx
Normal file
73
App/FeatureSet/Dashboard/src/Pages/Profiles/Index.tsx
Normal 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;
|
||||
26
App/FeatureSet/Dashboard/src/Pages/Profiles/Layout.tsx
Normal file
26
App/FeatureSet/Dashboard/src/Pages/Profiles/Layout.tsx
Normal 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;
|
||||
45
App/FeatureSet/Dashboard/src/Pages/Profiles/SideMenu.tsx
Normal file
45
App/FeatureSet/Dashboard/src/Pages/Profiles/SideMenu.tsx
Normal 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;
|
||||
19
App/FeatureSet/Dashboard/src/Pages/Profiles/View/Index.tsx
Normal file
19
App/FeatureSet/Dashboard/src/Pages/Profiles/View/Index.tsx
Normal 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;
|
||||
23
App/FeatureSet/Dashboard/src/Pages/Profiles/View/Layout.tsx
Normal file
23
App/FeatureSet/Dashboard/src/Pages/Profiles/View/Layout.tsx
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
70
App/FeatureSet/Dashboard/src/Routes/ProfilesRoutes.tsx
Normal file
70
App/FeatureSet/Dashboard/src/Routes/ProfilesRoutes.tsx
Normal 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;
|
||||
@@ -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];
|
||||
}
|
||||
@@ -15,3 +15,4 @@ export * from "./KubernetesBreadcrumbs";
|
||||
export * from "./DashboardBreadCrumbs";
|
||||
export * from "./AIAgentTasksBreadcrumbs";
|
||||
export * from "./ExceptionsBreadcrumbs";
|
||||
export * from "./ProfilesBreadcrumbs";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/*`,
|
||||
|
||||
Reference in New Issue
Block a user