diff --git a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsDashboard.tsx b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsDashboard.tsx new file mode 100644 index 0000000000..2fa99cebfe --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsDashboard.tsx @@ -0,0 +1,352 @@ +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import Service from "Common/Models/DatabaseModels/Service"; +import MetricType from "Common/Models/DatabaseModels/MetricType"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import ProjectUtil from "Common/UI/Utils/Project"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import SortOrder from "Common/Types/BaseDatabase/SortOrder"; +import ListResult from "Common/Types/BaseDatabase/ListResult"; +import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; +import ObjectID from "Common/Types/ObjectID"; +import ServiceElement from "../Service/ServiceElement"; +import RouteMap, { RouteUtil } from "../../Utils/RouteMap"; +import PageMap from "../../Utils/PageMap"; +import Route from "Common/Types/API/Route"; +import AppLink from "../AppLink/AppLink"; +import Includes from "Common/Types/BaseDatabase/Includes"; + +interface ServiceMetricSummary { + service: Service; + metricCount: number; + metricNames: Array; +} + +const MetricsDashboard: FunctionComponent = (): ReactElement => { + const [serviceSummaries, setServiceSummaries] = useState< + Array + >([]); + const [totalMetricCount, setTotalMetricCount] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const loadDashboard: () => Promise = async (): Promise => { + try { + setIsLoading(true); + setError(""); + + // Load services + const servicesResult: ListResult = await ModelAPI.getList({ + modelType: Service, + query: { + projectId: ProjectUtil.getCurrentProjectId()!, + }, + select: { + serviceColor: true, + name: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: { + name: SortOrder.Ascending, + }, + }); + + const services: Array = servicesResult.data || []; + + // Load all metric types with their services + const metricsResult: ListResult = await ModelAPI.getList({ + modelType: MetricType, + query: { + projectId: ProjectUtil.getCurrentProjectId()!, + }, + select: { + name: true, + unit: true, + description: true, + }, + relationSelect: { + services: { + _id: true, + name: true, + serviceColor: true, + }, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: { + name: SortOrder.Ascending, + }, + }); + + const metrics: Array = metricsResult.data || []; + setTotalMetricCount(metrics.length); + + // Build per-service summaries + const summaryMap: Map = new Map(); + + for (const service of services) { + const serviceId: string = service.id?.toString() || ""; + summaryMap.set(serviceId, { + service, + metricCount: 0, + metricNames: [], + }); + } + + for (const metric of metrics) { + const metricServices: Array = metric.services || []; + + for (const metricService of metricServices) { + const serviceId: string = + metricService._id?.toString() || + metricService.id?.toString() || + ""; + let summary: ServiceMetricSummary | undefined = + summaryMap.get(serviceId); + + if (!summary) { + // Service exists in metric but wasn't in our services list + summary = { + service: metricService, + metricCount: 0, + metricNames: [], + }; + summaryMap.set(serviceId, summary); + } + + summary.metricCount += 1; + + const metricName: string = metric.name || ""; + if (metricName && summary.metricNames.length < 5) { + summary.metricNames.push(metricName); + } + } + } + + // Only show services that have metrics + const summariesWithData: Array = Array.from( + summaryMap.values(), + ).filter((s: ServiceMetricSummary) => { + return s.metricCount > 0; + }); + + // Sort by metric count descending + summariesWithData.sort( + (a: ServiceMetricSummary, b: ServiceMetricSummary) => { + return b.metricCount - a.metricCount; + }, + ); + + setServiceSummaries(summariesWithData); + } catch (err) { + setError(API.getFriendlyMessage(err as Error)); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + void loadDashboard(); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ( + { + void loadDashboard(); + }} + /> + ); + } + + if (serviceSummaries.length === 0) { + return ( +
+
+ + + + + + + +
+

+ No metrics data yet +

+

+ Once your services start sending metrics via OpenTelemetry, you{"'"}ll + see a summary of which services are reporting, what metrics they + collect, and more. +

+
+ ); + } + + return ( + + {/* Summary Stats */} +
+
+

Total Metrics

+

+ {totalMetricCount} +

+
+
+

Services Reporting

+

+ {serviceSummaries.length} +

+
+
+

Avg Metrics per Service

+

+ {serviceSummaries.length > 0 + ? Math.round(totalMetricCount / serviceSummaries.length) + : 0} +

+
+
+ + {/* Service Cards */} +
+
+
+

+ Services Reporting Metrics +

+

+ Each service and the metrics it collects +

+
+ + View all metrics + +
+
+ {serviceSummaries.map((summary: ServiceMetricSummary) => { + return ( +
+
+ + + Active + +
+ +
+

Metrics Collected

+

+ {summary.metricCount} +

+
+ +
+

Sample Metrics

+
+ {summary.metricNames.map((name: string) => { + return ( + + {name} + + ); + })} + {summary.metricCount > summary.metricNames.length && ( + + +{summary.metricCount - summary.metricNames.length} more + + )} +
+
+ +
+ + View service metrics + +
+
+ ); + })} +
+
+
+ ); +}; + +export default MetricsDashboard; diff --git a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsTable.tsx b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsTable.tsx index e68cdff394..09e414561f 100644 --- a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsTable.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricsTable.tsx @@ -43,9 +43,9 @@ const MetricsTable: FunctionComponent = ( sortBy="name" sortOrder={SortOrder.Ascending} cardProps={{ - title: "Metrics", + title: "All Metrics", description: - "Metrics are the individual data points that make up a service. They are the building blocks of a service and represent the work done by a single service.", + "All metrics collected from your services. Click on a metric to explore its data in the chart viewer.", }} onViewPage={async (item: MetricType) => { const route: Route = RouteUtil.populateRouteParams( diff --git a/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx b/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx index 7ba9cf1b38..9d3b6dcc98 100644 --- a/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx +++ b/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx @@ -108,7 +108,7 @@ const DashboardNavbar: FunctionComponent = ( }, { title: "Metrics", - description: "Monitor system metrics.", + description: "Monitor and visualize system metrics across your services.", route: RouteUtil.populateRouteParams(RouteMap[PageMap.METRICS] as Route), activeRoute: RouteMap[PageMap.METRICS], icon: IconProp.Heartbeat, diff --git a/App/FeatureSet/Dashboard/src/Pages/Metrics/Index.tsx b/App/FeatureSet/Dashboard/src/Pages/Metrics/Index.tsx index acfa75e49b..91c97d81a4 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Metrics/Index.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Metrics/Index.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useState, } from "react"; -import MetricsTable from "../../Components/Metrics/MetricsTable"; +import MetricsDashboard from "../../Components/Metrics/MetricsDashboard"; import Service from "Common/Models/DatabaseModels/Service"; import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; import API from "Common/UI/Utils/API/API"; @@ -62,7 +62,7 @@ const MetricsPage: FunctionComponent = ( return ; } - return ; + return ; }; export default MetricsPage; diff --git a/App/FeatureSet/Dashboard/src/Pages/Metrics/List.tsx b/App/FeatureSet/Dashboard/src/Pages/Metrics/List.tsx new file mode 100644 index 0000000000..5dd16e9780 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Metrics/List.tsx @@ -0,0 +1,8 @@ +import React, { FunctionComponent, ReactElement } from "react"; +import MetricsTable from "../../Components/Metrics/MetricsTable"; + +const MetricsListPage: FunctionComponent = (): ReactElement => { + return ; +}; + +export default MetricsListPage; diff --git a/App/FeatureSet/Dashboard/src/Pages/Metrics/SideMenu.tsx b/App/FeatureSet/Dashboard/src/Pages/Metrics/SideMenu.tsx index 1b25606201..f9d99871e2 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Metrics/SideMenu.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Metrics/SideMenu.tsx @@ -14,21 +14,30 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => { items: [ { link: { - title: "All Metrics", + title: "Overview", to: RouteUtil.populateRouteParams( RouteMap[PageMap.METRICS] as Route, ), }, + icon: IconProp.Home, + }, + { + link: { + title: "All Metrics", + to: RouteUtil.populateRouteParams( + RouteMap[PageMap.METRICS_LIST] as Route, + ), + }, icon: IconProp.ChartBar, }, ], }, { - title: "Documentation", + title: "Help", items: [ { link: { - title: "Documentation", + title: "Setup Guide", to: RouteUtil.populateRouteParams( RouteMap[PageMap.METRICS_DOCUMENTATION] as Route, ), diff --git a/App/FeatureSet/Dashboard/src/Pages/Metrics/View/Layout.tsx b/App/FeatureSet/Dashboard/src/Pages/Metrics/View/Layout.tsx index 8868af9778..8187f4256a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Metrics/View/Layout.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Metrics/View/Layout.tsx @@ -12,7 +12,7 @@ const MetricsViewLayout: FunctionComponent< const path: string = Navigation.getRoutePath(RouteUtil.getRoutes()); return ( diff --git a/App/FeatureSet/Dashboard/src/Routes/MetricsRoutes.tsx b/App/FeatureSet/Dashboard/src/Routes/MetricsRoutes.tsx index 0a310d0cbe..7cfd25e545 100644 --- a/App/FeatureSet/Dashboard/src/Routes/MetricsRoutes.tsx +++ b/App/FeatureSet/Dashboard/src/Routes/MetricsRoutes.tsx @@ -9,6 +9,7 @@ import { Route as PageRoute, Routes } from "react-router-dom"; // Pages import MetricsPage from "../Pages/Metrics/Index"; +import MetricsListPage from "../Pages/Metrics/List"; import MetricsDocumentationPage from "../Pages/Metrics/Documentation"; import MetricViewPage from "../Pages/Metrics/View/Index"; @@ -28,6 +29,12 @@ const MetricsRoutes: FunctionComponent = ( /> } /> + + } + /> | undefined { const breadcrumpLinksMap: Dictionary = { ...BuildBreadcrumbLinksByTitles(PageMap.METRICS, ["Project", "Metrics"]), + ...BuildBreadcrumbLinksByTitles(PageMap.METRICS_LIST, [ + "Project", + "Metrics", + "All Metrics", + ]), ...BuildBreadcrumbLinksByTitles(PageMap.METRIC_VIEW, [ "Project", "Metrics", - "Metrics Explorer", + "Metric Explorer", ]), ...BuildBreadcrumbLinksByTitles(PageMap.METRICS_DOCUMENTATION, [ "Project", "Metrics", - "Documentation", + "Setup Guide", ]), }; return breadcrumpLinksMap[path]; diff --git a/App/FeatureSet/Dashboard/src/Utils/PageMap.ts b/App/FeatureSet/Dashboard/src/Utils/PageMap.ts index 2fc164c50b..87b6668a16 100644 --- a/App/FeatureSet/Dashboard/src/Utils/PageMap.ts +++ b/App/FeatureSet/Dashboard/src/Utils/PageMap.ts @@ -15,6 +15,7 @@ enum PageMap { // Metrics (standalone product) METRICS_ROOT = "METRICS_ROOT", METRICS = "METRICS", + METRICS_LIST = "METRICS_LIST", METRIC_VIEW = "METRIC_VIEW", METRICS_DOCUMENTATION = "METRICS_DOCUMENTATION", diff --git a/App/FeatureSet/Dashboard/src/Utils/RouteMap.ts b/App/FeatureSet/Dashboard/src/Utils/RouteMap.ts index 5bca86b781..40083cbff8 100644 --- a/App/FeatureSet/Dashboard/src/Utils/RouteMap.ts +++ b/App/FeatureSet/Dashboard/src/Utils/RouteMap.ts @@ -124,6 +124,7 @@ export const LogsRoutePath: Dictionary = { // Metrics product routes export const MetricsRoutePath: Dictionary = { [PageMap.METRICS]: "", + [PageMap.METRICS_LIST]: "list", [PageMap.METRIC_VIEW]: "view", [PageMap.METRICS_DOCUMENTATION]: "documentation", }; @@ -2275,6 +2276,12 @@ const RouteMap: Dictionary = { [PageMap.METRICS]: new Route(`/dashboard/${RouteParams.ProjectID}/metrics`), + [PageMap.METRICS_LIST]: new Route( + `/dashboard/${RouteParams.ProjectID}/metrics/${ + MetricsRoutePath[PageMap.METRICS_LIST] + }`, + ), + [PageMap.METRIC_VIEW]: new Route( `/dashboard/${RouteParams.ProjectID}/metrics/${ MetricsRoutePath[PageMap.METRIC_VIEW]