From b98e7f13a5dcca86f91616bb9b60872c625e0275 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 30 Oct 2025 20:08:52 +0000 Subject: [PATCH 1/9] feat(service-catalog): add Alerts, Logs, Traces and Metrics pages, routes and menu - Add new ServiceCatalog view pages: Alerts.tsx, Logs.tsx, Traces.tsx, Metrics.tsx (fetch monitors/telemetry service ids and render respective tables/viewers). - Register lazy routes and PageRoute entries in ServiceCatalogRoutes for alerts, logs, traces and metrics. - Extend PageMap and RouteMap with new keys/paths and Route entries. - Update SideMenu to include Alerts under Operations and Logs/Traces/Metrics under a Telemetry section. - Add breadcrumbs entries for the new service catalog pages. --- .../src/Pages/ServiceCatalog/View/Alerts.tsx | 92 +++++++++++++ .../src/Pages/ServiceCatalog/View/Logs.tsx | 94 +++++++++++++ .../src/Pages/ServiceCatalog/View/Metrics.tsx | 127 ++++++++++++++++++ .../Pages/ServiceCatalog/View/SideMenu.tsx | 51 ++++++- .../src/Pages/ServiceCatalog/View/Traces.tsx | 97 +++++++++++++ Dashboard/src/Routes/ServiceCatalogRoutes.tsx | 88 ++++++++++++ .../Breadcrumbs/ServiceCatalogBreadcrumbs.ts | 24 ++++ Dashboard/src/Utils/PageMap.ts | 4 + Dashboard/src/Utils/RouteMap.ts | 28 ++++ 9 files changed, 603 insertions(+), 2 deletions(-) create mode 100644 Dashboard/src/Pages/ServiceCatalog/View/Alerts.tsx create mode 100644 Dashboard/src/Pages/ServiceCatalog/View/Logs.tsx create mode 100644 Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx create mode 100644 Dashboard/src/Pages/ServiceCatalog/View/Traces.tsx diff --git a/Dashboard/src/Pages/ServiceCatalog/View/Alerts.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Alerts.tsx new file mode 100644 index 0000000000..e05291160f --- /dev/null +++ b/Dashboard/src/Pages/ServiceCatalog/View/Alerts.tsx @@ -0,0 +1,92 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import AlertsTable from "../../../Components/Alert/AlertsTable"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import ListResult from "Common/Types/BaseDatabase/ListResult"; +import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; +import API from "Common/UI/Utils/API/API"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ServiceCatalogMonitor from "Common/Models/DatabaseModels/ServiceCatalogMonitor"; +import Includes from "Common/Types/BaseDatabase/Includes"; +import Query from "Common/Types/BaseDatabase/Query"; +import Alert from "Common/Models/DatabaseModels/Alert"; + +const ServiceCatalogAlerts: FunctionComponent = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [monitorIds, setMonitorIds] = useState | null>(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchMonitorsInService: PromiseVoidFunction = async (): Promise => { + try { + setIsLoading(true); + const serviceCatalogMonitors: ListResult = + await ModelAPI.getList({ + modelType: ServiceCatalogMonitor, + query: { + serviceCatalogId: modelId, + }, + select: { + monitorId: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: {}, + }); + + const ids: ObjectID[] = serviceCatalogMonitors.data.map( + (serviceCatalogMonitor: ServiceCatalogMonitor) => { + return serviceCatalogMonitor.monitorId!; + }, + ); + + setMonitorIds(ids); + setIsLoading(false); + } catch (err) { + setIsLoading(false); + setError(API.getFriendlyMessage(err)); + } + }; + + useEffect(() => { + fetchMonitorsInService().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (error) { + return ; + } + + if (isLoading || monitorIds === null) { + return ; + } + + const query: Query = { + monitorId: new Includes(monitorIds), + }; + + return ( + + + + ); +}; + +export default ServiceCatalogAlerts; diff --git a/Dashboard/src/Pages/ServiceCatalog/View/Logs.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Logs.tsx new file mode 100644 index 0000000000..d9c393f580 --- /dev/null +++ b/Dashboard/src/Pages/ServiceCatalog/View/Logs.tsx @@ -0,0 +1,94 @@ +import DashboardLogsViewer from "../../../Components/Logs/LogsViewer"; +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ServiceCatalogTelemetryService from "Common/Models/DatabaseModels/ServiceCatalogTelemetryService"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import ListResult from "Common/Types/BaseDatabase/ListResult"; +import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; +import API from "Common/UI/Utils/API/API"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; + +const ServiceCatalogLogs: FunctionComponent = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [telemetryServiceIds, setTelemetryServiceIds] = + useState | null>(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchTelemetryServices: PromiseVoidFunction = async (): Promise => { + try { + setIsLoading(true); + const response: ListResult = + await ModelAPI.getList({ + modelType: ServiceCatalogTelemetryService, + query: { + serviceCatalogId: modelId, + }, + select: { + telemetryServiceId: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: {}, + }); + + const ids: ObjectID[] = response.data.map( + (serviceCatalogTelemetryService: ServiceCatalogTelemetryService) => { + return serviceCatalogTelemetryService.telemetryServiceId!; + }, + ); + + setTelemetryServiceIds(ids); + setIsLoading(false); + } catch (err) { + setIsLoading(false); + setError(API.getFriendlyMessage(err)); + } + }; + + useEffect(() => { + fetchTelemetryServices().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (error) { + return ; + } + + if (isLoading || telemetryServiceIds === null) { + return ; + } + + if (telemetryServiceIds.length === 0) { + return ( + + ); + } + + return ( + + + + ); +}; + +export default ServiceCatalogLogs; diff --git a/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx new file mode 100644 index 0000000000..00a9a7c2ff --- /dev/null +++ b/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx @@ -0,0 +1,127 @@ +import MetricsTable from "../../../Components/Metrics/MetricsTable"; +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ServiceCatalogTelemetryService from "Common/Models/DatabaseModels/ServiceCatalogTelemetryService"; +import TelemetryService from "Common/Models/DatabaseModels/TelemetryService"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import ListResult from "Common/Types/BaseDatabase/ListResult"; +import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; +import API from "Common/UI/Utils/API/API"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import Includes from "Common/Types/BaseDatabase/Includes"; + +const ServiceCatalogMetrics: FunctionComponent = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [telemetryServiceIds, setTelemetryServiceIds] = + useState | null>(null); + const [telemetryServices, setTelemetryServices] = + useState>([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchTelemetryServices: PromiseVoidFunction = async (): Promise => { + try { + setIsLoading(true); + const response: ListResult = + await ModelAPI.getList({ + modelType: ServiceCatalogTelemetryService, + query: { + serviceCatalogId: modelId, + }, + select: { + telemetryServiceId: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: {}, + }); + + const ids: ObjectID[] = response.data.map( + (serviceCatalogTelemetryService: ServiceCatalogTelemetryService) => { + return serviceCatalogTelemetryService.telemetryServiceId!; + }, + ); + + setTelemetryServiceIds(ids); + + if (ids.length === 0) { + setTelemetryServices([]); + setIsLoading(false); + return; + } + + const telemetryServicesResponse: ListResult = + await ModelAPI.getList({ + modelType: TelemetryService, + query: { + _id: new Includes(ids), + }, + select: { + _id: true, + name: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: {}, + }); + + setTelemetryServices(telemetryServicesResponse.data || []); + setIsLoading(false); + } catch (err) { + setIsLoading(false); + setError(API.getFriendlyMessage(err)); + } + }; + + useEffect(() => { + fetchTelemetryServices().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (error) { + return ; + } + + if (isLoading || telemetryServiceIds === null) { + return ; + } + + if (telemetryServiceIds.length === 0) { + return ( + + ); + } + + const singleTelemetryService: TelemetryService | undefined = + telemetryServiceIds.length === 1 + ? telemetryServices.find((service: TelemetryService) => { + return service.id?.toString() === telemetryServiceIds[0]?.toString(); + }) + : undefined; + + return ( + + + + ); +}; + +export default ServiceCatalogMetrics; diff --git a/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx b/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx index 7524bdd31e..b6d521d0e5 100644 --- a/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx @@ -52,7 +52,7 @@ const DashboardSideMenu: FunctionComponent = ( /> - + = ( icon={IconProp.AltGlobe} /> + + = ( = ( /> + + + + + + + + + = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [telemetryServiceIds, setTelemetryServiceIds] = + useState | null>(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchTelemetryServices: PromiseVoidFunction = async (): Promise => { + try { + setIsLoading(true); + const response: ListResult = + await ModelAPI.getList({ + modelType: ServiceCatalogTelemetryService, + query: { + serviceCatalogId: modelId, + }, + select: { + telemetryServiceId: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: {}, + }); + + const ids: ObjectID[] = response.data.map( + (serviceCatalogTelemetryService: ServiceCatalogTelemetryService) => { + return serviceCatalogTelemetryService.telemetryServiceId!; + }, + ); + + setTelemetryServiceIds(ids); + setIsLoading(false); + } catch (err) { + setIsLoading(false); + setError(API.getFriendlyMessage(err)); + } + }; + + useEffect(() => { + fetchTelemetryServices().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (error) { + return ; + } + + if (isLoading || telemetryServiceIds === null) { + return ; + } + + if (telemetryServiceIds.length === 0) { + return ( + + ); + } + + const spanQuery: Query = { + serviceId: new Includes(telemetryServiceIds), + }; + + return ( + + + + ); +}; + +export default ServiceCatalogTraces; diff --git a/Dashboard/src/Routes/ServiceCatalogRoutes.tsx b/Dashboard/src/Routes/ServiceCatalogRoutes.tsx index a58bf7bd1b..0b29cc8003 100644 --- a/Dashboard/src/Routes/ServiceCatalogRoutes.tsx +++ b/Dashboard/src/Routes/ServiceCatalogRoutes.tsx @@ -45,12 +45,36 @@ const ServiceCatalogViewIncidents: LazyExoticComponent< return import("../Pages/ServiceCatalog/View/Incidents"); }); +const ServiceCatalogViewAlerts: LazyExoticComponent< + FunctionComponent +> = lazy(() => { + return import("../Pages/ServiceCatalog/View/Alerts"); +}); + const ServiceCatalogViewTelemetryServices: LazyExoticComponent< FunctionComponent > = lazy(() => { return import("../Pages/ServiceCatalog/View/TelemetryServices"); }); +const ServiceCatalogViewLogs: LazyExoticComponent< + FunctionComponent +> = lazy(() => { + return import("../Pages/ServiceCatalog/View/Logs"); +}); + +const ServiceCatalogViewTraces: LazyExoticComponent< + FunctionComponent +> = lazy(() => { + return import("../Pages/ServiceCatalog/View/Traces"); +}); + +const ServiceCatalogViewMetrics: LazyExoticComponent< + FunctionComponent +> = lazy(() => { + return import("../Pages/ServiceCatalog/View/Metrics"); +}); + const ServiceCatalogViewDelete: LazyExoticComponent< FunctionComponent > = lazy(() => { @@ -173,6 +197,22 @@ const ServiceCatalogRoutes: FunctionComponent = ( } /> + + + + } + /> + = ( } /> + + + + } + /> + + + + + } + /> + + + + + } + /> + = { [PageMap.SERVICE_CATALOG_VIEW_SETTINGS]: `${RouteParams.ModelID}/settings`, [PageMap.SERVICE_CATALOG_VIEW_MONITORS]: `${RouteParams.ModelID}/monitors`, [PageMap.SERVICE_CATALOG_VIEW_INCIDENTS]: `${RouteParams.ModelID}/incidents`, + [PageMap.SERVICE_CATALOG_VIEW_ALERTS]: `${RouteParams.ModelID}/alerts`, + [PageMap.SERVICE_CATALOG_VIEW_LOGS]: `${RouteParams.ModelID}/logs`, + [PageMap.SERVICE_CATALOG_VIEW_TRACES]: `${RouteParams.ModelID}/traces`, + [PageMap.SERVICE_CATALOG_VIEW_METRICS]: `${RouteParams.ModelID}/metrics`, [PageMap.SERVICE_CATALOG_VIEW_TELEMETRY_SERVICES]: `${RouteParams.ModelID}/telemetry-service`, }; @@ -947,6 +951,30 @@ const RouteMap: Dictionary = { }`, ), + [PageMap.SERVICE_CATALOG_VIEW_ALERTS]: new Route( + `/dashboard/${RouteParams.ProjectID}/service-catalog/${ + ServiceCatalogRoutePath[PageMap.SERVICE_CATALOG_VIEW_ALERTS] + }`, + ), + + [PageMap.SERVICE_CATALOG_VIEW_LOGS]: new Route( + `/dashboard/${RouteParams.ProjectID}/service-catalog/${ + ServiceCatalogRoutePath[PageMap.SERVICE_CATALOG_VIEW_LOGS] + }`, + ), + + [PageMap.SERVICE_CATALOG_VIEW_TRACES]: new Route( + `/dashboard/${RouteParams.ProjectID}/service-catalog/${ + ServiceCatalogRoutePath[PageMap.SERVICE_CATALOG_VIEW_TRACES] + }`, + ), + + [PageMap.SERVICE_CATALOG_VIEW_METRICS]: new Route( + `/dashboard/${RouteParams.ProjectID}/service-catalog/${ + ServiceCatalogRoutePath[PageMap.SERVICE_CATALOG_VIEW_METRICS] + }`, + ), + [PageMap.SERVICE_CATALOG_VIEW_TELEMETRY_SERVICES]: new Route( `/dashboard/${RouteParams.ProjectID}/service-catalog/${ ServiceCatalogRoutePath[PageMap.SERVICE_CATALOG_VIEW_TELEMETRY_SERVICES] From 803d0436ca6c83f359eea982ae594cfa7f433d7f Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 30 Oct 2025 20:12:52 +0000 Subject: [PATCH 2/9] feat(service-catalog): reorganize side menu and tidy view components - Move "Telemetry Services" into the Resources section and introduce an Operations section for Alerts and Incidents; update corresponding icons and route targets. - Reformat FunctionComponent type annotations and async fetch function bodies in Alerts, Logs, Traces and Metrics for consistent indentation and readability. - Minor formatting cleanup for pageRoute/path prop in ServiceCatalogRoutes. --- .../src/Pages/ServiceCatalog/View/Alerts.tsx | 61 +++++----- .../src/Pages/ServiceCatalog/View/Logs.tsx | 61 +++++----- .../src/Pages/ServiceCatalog/View/Metrics.tsx | 104 +++++++++--------- .../Pages/ServiceCatalog/View/SideMenu.tsx | 31 +++--- .../src/Pages/ServiceCatalog/View/Traces.tsx | 61 +++++----- Dashboard/src/Routes/ServiceCatalogRoutes.tsx | 8 +- 6 files changed, 168 insertions(+), 158 deletions(-) diff --git a/Dashboard/src/Pages/ServiceCatalog/View/Alerts.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Alerts.tsx index e05291160f..598c99c4cb 100644 --- a/Dashboard/src/Pages/ServiceCatalog/View/Alerts.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/View/Alerts.tsx @@ -21,43 +21,46 @@ import Includes from "Common/Types/BaseDatabase/Includes"; import Query from "Common/Types/BaseDatabase/Query"; import Alert from "Common/Models/DatabaseModels/Alert"; -const ServiceCatalogAlerts: FunctionComponent = (): ReactElement => { +const ServiceCatalogAlerts: FunctionComponent< + PageComponentProps +> = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); const [monitorIds, setMonitorIds] = useState | null>(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const fetchMonitorsInService: PromiseVoidFunction = async (): Promise => { - try { - setIsLoading(true); - const serviceCatalogMonitors: ListResult = - await ModelAPI.getList({ - modelType: ServiceCatalogMonitor, - query: { - serviceCatalogId: modelId, - }, - select: { - monitorId: true, - }, - limit: LIMIT_PER_PROJECT, - skip: 0, - sort: {}, - }); + const fetchMonitorsInService: PromiseVoidFunction = + async (): Promise => { + try { + setIsLoading(true); + const serviceCatalogMonitors: ListResult = + await ModelAPI.getList({ + modelType: ServiceCatalogMonitor, + query: { + serviceCatalogId: modelId, + }, + select: { + monitorId: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: {}, + }); - const ids: ObjectID[] = serviceCatalogMonitors.data.map( - (serviceCatalogMonitor: ServiceCatalogMonitor) => { - return serviceCatalogMonitor.monitorId!; - }, - ); + const ids: ObjectID[] = serviceCatalogMonitors.data.map( + (serviceCatalogMonitor: ServiceCatalogMonitor) => { + return serviceCatalogMonitor.monitorId!; + }, + ); - setMonitorIds(ids); - setIsLoading(false); - } catch (err) { - setIsLoading(false); - setError(API.getFriendlyMessage(err)); - } - }; + setMonitorIds(ids); + setIsLoading(false); + } catch (err) { + setIsLoading(false); + setError(API.getFriendlyMessage(err)); + } + }; useEffect(() => { fetchMonitorsInService().catch((err: Error) => { diff --git a/Dashboard/src/Pages/ServiceCatalog/View/Logs.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Logs.tsx index d9c393f580..d60d75bb7b 100644 --- a/Dashboard/src/Pages/ServiceCatalog/View/Logs.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/View/Logs.tsx @@ -18,7 +18,9 @@ import API from "Common/UI/Utils/API/API"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import PageLoader from "Common/UI/Components/Loader/PageLoader"; -const ServiceCatalogLogs: FunctionComponent = (): ReactElement => { +const ServiceCatalogLogs: FunctionComponent< + PageComponentProps +> = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); const [telemetryServiceIds, setTelemetryServiceIds] = @@ -26,36 +28,37 @@ const ServiceCatalogLogs: FunctionComponent = (): ReactEleme const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const fetchTelemetryServices: PromiseVoidFunction = async (): Promise => { - try { - setIsLoading(true); - const response: ListResult = - await ModelAPI.getList({ - modelType: ServiceCatalogTelemetryService, - query: { - serviceCatalogId: modelId, - }, - select: { - telemetryServiceId: true, - }, - limit: LIMIT_PER_PROJECT, - skip: 0, - sort: {}, - }); + const fetchTelemetryServices: PromiseVoidFunction = + async (): Promise => { + try { + setIsLoading(true); + const response: ListResult = + await ModelAPI.getList({ + modelType: ServiceCatalogTelemetryService, + query: { + serviceCatalogId: modelId, + }, + select: { + telemetryServiceId: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: {}, + }); - const ids: ObjectID[] = response.data.map( - (serviceCatalogTelemetryService: ServiceCatalogTelemetryService) => { - return serviceCatalogTelemetryService.telemetryServiceId!; - }, - ); + const ids: ObjectID[] = response.data.map( + (serviceCatalogTelemetryService: ServiceCatalogTelemetryService) => { + return serviceCatalogTelemetryService.telemetryServiceId!; + }, + ); - setTelemetryServiceIds(ids); - setIsLoading(false); - } catch (err) { - setIsLoading(false); - setError(API.getFriendlyMessage(err)); - } - }; + setTelemetryServiceIds(ids); + setIsLoading(false); + } catch (err) { + setIsLoading(false); + setError(API.getFriendlyMessage(err)); + } + }; useEffect(() => { fetchTelemetryServices().catch((err: Error) => { diff --git a/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx index 00a9a7c2ff..150bd81570 100644 --- a/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx @@ -20,69 +20,73 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import PageLoader from "Common/UI/Components/Loader/PageLoader"; import Includes from "Common/Types/BaseDatabase/Includes"; -const ServiceCatalogMetrics: FunctionComponent = (): ReactElement => { +const ServiceCatalogMetrics: FunctionComponent< + PageComponentProps +> = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); const [telemetryServiceIds, setTelemetryServiceIds] = useState | null>(null); - const [telemetryServices, setTelemetryServices] = - useState>([]); + const [telemetryServices, setTelemetryServices] = useState< + Array + >([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const fetchTelemetryServices: PromiseVoidFunction = async (): Promise => { - try { - setIsLoading(true); - const response: ListResult = - await ModelAPI.getList({ - modelType: ServiceCatalogTelemetryService, - query: { - serviceCatalogId: modelId, + const fetchTelemetryServices: PromiseVoidFunction = + async (): Promise => { + try { + setIsLoading(true); + const response: ListResult = + await ModelAPI.getList({ + modelType: ServiceCatalogTelemetryService, + query: { + serviceCatalogId: modelId, + }, + select: { + telemetryServiceId: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: {}, + }); + + const ids: ObjectID[] = response.data.map( + (serviceCatalogTelemetryService: ServiceCatalogTelemetryService) => { + return serviceCatalogTelemetryService.telemetryServiceId!; }, - select: { - telemetryServiceId: true, - }, - limit: LIMIT_PER_PROJECT, - skip: 0, - sort: {}, - }); + ); - const ids: ObjectID[] = response.data.map( - (serviceCatalogTelemetryService: ServiceCatalogTelemetryService) => { - return serviceCatalogTelemetryService.telemetryServiceId!; - }, - ); + setTelemetryServiceIds(ids); - setTelemetryServiceIds(ids); + if (ids.length === 0) { + setTelemetryServices([]); + setIsLoading(false); + return; + } - if (ids.length === 0) { - setTelemetryServices([]); + const telemetryServicesResponse: ListResult = + await ModelAPI.getList({ + modelType: TelemetryService, + query: { + _id: new Includes(ids), + }, + select: { + _id: true, + name: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: {}, + }); + + setTelemetryServices(telemetryServicesResponse.data || []); setIsLoading(false); - return; + } catch (err) { + setIsLoading(false); + setError(API.getFriendlyMessage(err)); } - - const telemetryServicesResponse: ListResult = - await ModelAPI.getList({ - modelType: TelemetryService, - query: { - _id: new Includes(ids), - }, - select: { - _id: true, - name: true, - }, - limit: LIMIT_PER_PROJECT, - skip: 0, - sort: {}, - }); - - setTelemetryServices(telemetryServicesResponse.data || []); - setIsLoading(false); - } catch (err) { - setIsLoading(false); - setError(API.getFriendlyMessage(err)); - } - }; + }; useEffect(() => { fetchTelemetryServices().catch((err: Error) => { diff --git a/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx b/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx index b6d521d0e5..13f8805e3b 100644 --- a/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx @@ -52,7 +52,7 @@ const DashboardSideMenu: FunctionComponent = ( /> - + = ( icon={IconProp.AltGlobe} /> + + + + = ( }} icon={IconProp.Alert} /> - - @@ -133,7 +135,6 @@ const DashboardSideMenu: FunctionComponent = ( }} icon={IconProp.Graph} /> - diff --git a/Dashboard/src/Pages/ServiceCatalog/View/Traces.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Traces.tsx index 0cfe2ee8f6..d5ce0992d0 100644 --- a/Dashboard/src/Pages/ServiceCatalog/View/Traces.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/View/Traces.tsx @@ -21,7 +21,9 @@ import Includes from "Common/Types/BaseDatabase/Includes"; import Query from "Common/Types/BaseDatabase/Query"; import Span from "Common/Models/AnalyticsModels/Span"; -const ServiceCatalogTraces: FunctionComponent = (): ReactElement => { +const ServiceCatalogTraces: FunctionComponent< + PageComponentProps +> = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); const [telemetryServiceIds, setTelemetryServiceIds] = @@ -29,36 +31,37 @@ const ServiceCatalogTraces: FunctionComponent = (): ReactEle const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const fetchTelemetryServices: PromiseVoidFunction = async (): Promise => { - try { - setIsLoading(true); - const response: ListResult = - await ModelAPI.getList({ - modelType: ServiceCatalogTelemetryService, - query: { - serviceCatalogId: modelId, - }, - select: { - telemetryServiceId: true, - }, - limit: LIMIT_PER_PROJECT, - skip: 0, - sort: {}, - }); + const fetchTelemetryServices: PromiseVoidFunction = + async (): Promise => { + try { + setIsLoading(true); + const response: ListResult = + await ModelAPI.getList({ + modelType: ServiceCatalogTelemetryService, + query: { + serviceCatalogId: modelId, + }, + select: { + telemetryServiceId: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + sort: {}, + }); - const ids: ObjectID[] = response.data.map( - (serviceCatalogTelemetryService: ServiceCatalogTelemetryService) => { - return serviceCatalogTelemetryService.telemetryServiceId!; - }, - ); + const ids: ObjectID[] = response.data.map( + (serviceCatalogTelemetryService: ServiceCatalogTelemetryService) => { + return serviceCatalogTelemetryService.telemetryServiceId!; + }, + ); - setTelemetryServiceIds(ids); - setIsLoading(false); - } catch (err) { - setIsLoading(false); - setError(API.getFriendlyMessage(err)); - } - }; + setTelemetryServiceIds(ids); + setIsLoading(false); + } catch (err) { + setIsLoading(false); + setError(API.getFriendlyMessage(err)); + } + }; useEffect(() => { fetchTelemetryServices().catch((err: Error) => { diff --git a/Dashboard/src/Routes/ServiceCatalogRoutes.tsx b/Dashboard/src/Routes/ServiceCatalogRoutes.tsx index 0b29cc8003..71f38b4525 100644 --- a/Dashboard/src/Routes/ServiceCatalogRoutes.tsx +++ b/Dashboard/src/Routes/ServiceCatalogRoutes.tsx @@ -248,16 +248,12 @@ const ServiceCatalogRoutes: FunctionComponent = ( /> } From 65c999b5fc91f468717a9f7ea4f65d91883d1bf6 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 30 Oct 2025 20:32:35 +0000 Subject: [PATCH 3/9] feat(metrics): support filtering by multiple telemetry services Replace telemetryServiceId/telemetryServiceName props with telemetryServiceIds array. Update MetricsTable to accept multiple IDs, construct view route when exactly one telemetry service is selected, query with Includes for multiple services, and add select/filters/column to show telemetry service info. Update ServiceCatalog and Telemetry Service Metrics pages to pass telemetryServiceIds and remove redundant single-service fetching/state. --- .../src/Components/Metrics/MetricsTable.tsx | 68 ++++++++++++++++--- .../src/Pages/ServiceCatalog/View/Metrics.tsx | 40 ----------- .../Telemetry/Services/View/Metrics/Index.tsx | 3 +- 3 files changed, 61 insertions(+), 50 deletions(-) diff --git a/Dashboard/src/Components/Metrics/MetricsTable.tsx b/Dashboard/src/Components/Metrics/MetricsTable.tsx index b1e34fcce5..fccdf6a3b3 100644 --- a/Dashboard/src/Components/Metrics/MetricsTable.tsx +++ b/Dashboard/src/Components/Metrics/MetricsTable.tsx @@ -2,6 +2,7 @@ import ProjectUtil from "Common/UI/Utils/Project"; import SortOrder from "Common/Types/BaseDatabase/SortOrder"; import ObjectID from "Common/Types/ObjectID"; import FieldType from "Common/UI/Components/Types/FieldType"; +import TelemetryService from "Common/Models/DatabaseModels/TelemetryService"; import Navigation from "Common/UI/Utils/Navigation"; import RouteMap, { RouteUtil } from "../../Utils/RouteMap"; import PageMap from "../../Utils/PageMap"; @@ -11,15 +12,23 @@ import React, { Fragment, FunctionComponent, ReactElement } from "react"; import ModelTable from "Common/UI/Components/ModelTable/ModelTable"; import MetricType from "Common/Models/DatabaseModels/MetricType"; import Includes from "Common/Types/BaseDatabase/Includes"; +import TelemetryServicesElement from "../TelemetryService/TelemetryServiceElements"; export interface ComponentProps { - telemetryServiceId?: ObjectID | undefined; - telemetryServiceName?: string | undefined; + telemetryServiceIds?: Array | undefined; } const MetricsTable: FunctionComponent = ( props: ComponentProps, ): ReactElement => { + const telemetryServiceFilterIds: Array = + props.telemetryServiceIds || []; + + const telemetryServiceIdForRoute: ObjectID | undefined = + telemetryServiceFilterIds.length === 1 + ? telemetryServiceFilterIds[0] + : undefined; + return ( @@ -41,7 +50,7 @@ const MetricsTable: FunctionComponent = ( "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.", }} onViewPage={async (item: MetricType) => { - if (!props.telemetryServiceId || !props.telemetryServiceName) { + if (!telemetryServiceIdForRoute) { const route: Route = RouteUtil.populateRouteParams( RouteMap[PageMap.TELEMETRY_METRIC_VIEW]!, ); @@ -59,7 +68,7 @@ const MetricsTable: FunctionComponent = ( const route: Route = RouteUtil.populateRouteParams( RouteMap[PageMap.TELEMETRY_SERVICES_VIEW_METRIC]!, { - modelId: props.telemetryServiceId, + modelId: telemetryServiceIdForRoute, }, ); @@ -69,14 +78,22 @@ const MetricsTable: FunctionComponent = ( currentUrl.protocol, currentUrl.hostname, route, - `metricName=${item.name}&serviceName=${props.telemetryServiceName}`, + `metricName=${item.name}`, ); }} query={{ projectId: ProjectUtil.getCurrentProjectId()!, - telemetryServices: props.telemetryServiceId - ? new Includes([props.telemetryServiceId]) - : undefined, + telemetryServices: + telemetryServiceFilterIds.length > 0 + ? new Includes(telemetryServiceFilterIds) + : undefined, + }} + selectMoreFields={{ + telemetryServices: { + _id: true, + name: true, + serviceColor: true, + }, }} showViewIdButton={false} noItemsMessage={"No metrics found for this service."} @@ -90,6 +107,23 @@ const MetricsTable: FunctionComponent = ( title: "Name", type: FieldType.Text, }, + { + field: { + telemetryServices: { + name: true, + }, + }, + title: "Telemetry Service", + type: FieldType.EntityArray, + filterEntityType: TelemetryService, + filterQuery: { + projectId: ProjectUtil.getCurrentProjectId()!, + }, + filterDropdownField: { + label: "name", + value: "_id", + }, + }, ]} columns={[ { @@ -99,6 +133,24 @@ const MetricsTable: FunctionComponent = ( title: "Name", type: FieldType.Text, }, + { + field: { + telemetryServices: { + name: true, + _id: true, + serviceColor: true, + }, + }, + title: "Telemetry Services", + type: FieldType.Element, + getElement: (item: MetricType): ReactElement => { + return ( + + ); + }, + }, ]} /> diff --git a/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx index 150bd81570..ce22d1b72e 100644 --- a/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx @@ -10,7 +10,6 @@ import React, { useState, } from "react"; import ServiceCatalogTelemetryService from "Common/Models/DatabaseModels/ServiceCatalogTelemetryService"; -import TelemetryService from "Common/Models/DatabaseModels/TelemetryService"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; import ListResult from "Common/Types/BaseDatabase/ListResult"; @@ -18,7 +17,6 @@ import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; import API from "Common/UI/Utils/API/API"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import PageLoader from "Common/UI/Components/Loader/PageLoader"; -import Includes from "Common/Types/BaseDatabase/Includes"; const ServiceCatalogMetrics: FunctionComponent< PageComponentProps @@ -27,9 +25,6 @@ const ServiceCatalogMetrics: FunctionComponent< const [telemetryServiceIds, setTelemetryServiceIds] = useState | null>(null); - const [telemetryServices, setTelemetryServices] = useState< - Array - >([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -58,29 +53,6 @@ const ServiceCatalogMetrics: FunctionComponent< ); setTelemetryServiceIds(ids); - - if (ids.length === 0) { - setTelemetryServices([]); - setIsLoading(false); - return; - } - - const telemetryServicesResponse: ListResult = - await ModelAPI.getList({ - modelType: TelemetryService, - query: { - _id: new Includes(ids), - }, - select: { - _id: true, - name: true, - }, - limit: LIMIT_PER_PROJECT, - skip: 0, - sort: {}, - }); - - setTelemetryServices(telemetryServicesResponse.data || []); setIsLoading(false); } catch (err) { setIsLoading(false); @@ -107,22 +79,10 @@ const ServiceCatalogMetrics: FunctionComponent< ); } - - const singleTelemetryService: TelemetryService | undefined = - telemetryServiceIds.length === 1 - ? telemetryServices.find((service: TelemetryService) => { - return service.id?.toString() === telemetryServiceIds[0]?.toString(); - }) - : undefined; - return ( ); diff --git a/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx b/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx index b23ee44c8c..7b4ded09f2 100644 --- a/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx +++ b/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx @@ -73,8 +73,7 @@ const MetricsTablePage: FunctionComponent< return ( ); }; From df1507b31485e3644eaa1d8d436220b600aca540 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 30 Oct 2025 21:33:30 +0000 Subject: [PATCH 4/9] feat(metrics): persist multiple metric queries & time range in URL - Refactor MetricExplorer to parse/serialize a metricQueries URL param (array of metricName, attributes, aggregationType) and startTime/endTime. - Add helpers to sanitize attributes, map aggregation types, and build metric query state. - Update MetricsTable to generate metricQueries payload (with attributes and aggregationType) when navigating to metric view. - Minor JSX/formatting cleanup. --- .../src/Components/Metrics/MetricExplorer.tsx | 328 ++++++++++++++++-- .../src/Components/Metrics/MetricsTable.tsx | 73 ++-- .../src/Pages/ServiceCatalog/View/Metrics.tsx | 4 +- .../Telemetry/Services/View/Metrics/Index.tsx | 6 +- 4 files changed, 352 insertions(+), 59 deletions(-) diff --git a/Dashboard/src/Components/Metrics/MetricExplorer.tsx b/Dashboard/src/Components/Metrics/MetricExplorer.tsx index c2e65f75f2..26af09a858 100644 --- a/Dashboard/src/Components/Metrics/MetricExplorer.tsx +++ b/Dashboard/src/Components/Metrics/MetricExplorer.tsx @@ -1,30 +1,41 @@ import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType"; import MetricView from "./MetricView"; import Navigation from "Common/UI/Utils/Navigation"; -import React, { FunctionComponent, ReactElement } from "react"; +import React, { + FunctionComponent, + ReactElement, + useEffect, + useRef, +} from "react"; import OneUptimeDate from "Common/Types/Date"; import InBetween from "Common/Types/BaseDatabase/InBetween"; import MetricViewData from "Common/Types/Metrics/MetricViewData"; +import Dictionary from "Common/Types/Dictionary"; +import JSONFunctions from "Common/Types/JSONFunctions"; +import Text from "Common/Types/Text"; const MetricExplorer: FunctionComponent = (): ReactElement => { - const metricName: string = - Navigation.getQueryStringByName("metricName") || ""; + const metricQueriesFromUrl: Array = + getMetricQueriesFromQuery(); - const serviceName: string = - Navigation.getQueryStringByName("serviceName") || ""; + const defaultEndDate: Date = OneUptimeDate.getCurrentDate(); + const defaultStartDate: Date = OneUptimeDate.addRemoveHours( + defaultEndDate, + -1, + ); + const defaultStartAndEndDate: InBetween = new InBetween( + defaultStartDate, + defaultEndDate, + ); - // set it to past 1 hour - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -1); + const initialTimeRange: InBetween = + getTimeRangeFromQuery() ?? defaultStartAndEndDate; - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const [metricViewData, setMetricViewData] = React.useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [ - { + const initialQueryConfigs = metricQueriesFromUrl.map( + (metricQuery: MetricQueryFromUrl, index: number) => { + return { metricAliasData: { - metricVariable: "a", + metricVariable: Text.getLetterFromAByNumber(index), title: "", description: "", legend: "", @@ -32,20 +43,97 @@ const MetricExplorer: FunctionComponent = (): ReactElement => { }, metricQueryData: { filterData: { - metricName: metricName, - attributes: serviceName - ? { - "resource.oneuptime.telemetry.service.name": serviceName, - } - : {}, - aggegationType: MetricsAggregationType.Avg, + metricName: metricQuery.metricName, + attributes: metricQuery.attributes, + aggegationType: + metricQuery.aggregationType || MetricsAggregationType.Avg, }, }, - }, - ], + }; + }, + ); + + const [metricViewData, setMetricViewData] = React.useState({ + startAndEndDate: initialTimeRange, + queryConfigs: + initialQueryConfigs.length > 0 + ? initialQueryConfigs + : [ + { + metricAliasData: { + metricVariable: "a", + title: "", + description: "", + legend: "", + legendUnit: "", + }, + metricQueryData: { + filterData: { + metricName: "", + attributes: {}, + aggegationType: MetricsAggregationType.Avg, + }, + }, + }, + ], formulaConfigs: [], }); + const lastSerializedStateRef = useRef(""); + + useEffect(() => { + const metricQueriesFromState: Array = + buildMetricQueriesFromState(metricViewData); + + const metricQueriesForUrl: Array = + metricQueriesFromState.filter(isMeaningfulMetricQuery); + + const startTimeValue: Date | undefined = + metricViewData.startAndEndDate?.startValue; + const endTimeValue: Date | undefined = + metricViewData.startAndEndDate?.endValue; + + const serializedState: string = JSON.stringify({ + metricQueries: metricQueriesForUrl, + startTime: startTimeValue ? OneUptimeDate.toString(startTimeValue) : null, + endTime: endTimeValue ? OneUptimeDate.toString(endTimeValue) : null, + }); + + if (serializedState === lastSerializedStateRef.current) { + return; + } + + const params: URLSearchParams = new URLSearchParams(window.location.search); + + if (metricQueriesForUrl.length > 0) { + params.set("metricQueries", JSON.stringify(metricQueriesForUrl)); + } else { + params.delete("metricQueries"); + } + + if (startTimeValue && endTimeValue) { + params.set("startTime", OneUptimeDate.toString(startTimeValue)); + params.set("endTime", OneUptimeDate.toString(endTimeValue)); + } else { + params.delete("startTime"); + params.delete("endTime"); + } + + params.delete("metricName"); + params.delete("attributes"); + params.delete("serviceName"); + + const newQueryString: string = params.toString(); + const newUrl: string = + newQueryString.length > 0 + ? `${window.location.pathname}?${newQueryString}` + : window.location.pathname; + + window.history.replaceState({}, "", newUrl); + + lastSerializedStateRef.current = serializedState; + }, [metricViewData]); + return ( { }; export default MetricExplorer; + +type MetricQueryFromUrl = { + metricName: string; + attributes: Dictionary; + aggregationType?: MetricsAggregationType; +}; + +function buildMetricQueriesFromState( + data: MetricViewData, +): Array { + return data.queryConfigs.map((queryConfig) => { + const filterData = queryConfig.metricQueryData?.filterData || {}; + + const metricNameValue: unknown = (filterData as Record)[ + "metricName" + ]; + + const metricName: string = + typeof metricNameValue === "string" ? metricNameValue : ""; + + const aggregationValue: unknown = (filterData as Record)[ + "aggegationType" + ]; + + const aggregationType: MetricsAggregationType | undefined = + getAggregationTypeFromValue(aggregationValue); + + const attributes: Dictionary = + sanitizeAttributes((filterData as Record)["attributes"]); + + return { + metricName, + attributes, + ...(aggregationType ? { aggregationType } : {}), + }; + }); +} + +function getMetricQueriesFromQuery(): Array { + const metricQueriesParam: string | null = + Navigation.getQueryStringByName("metricQueries"); + + if (!metricQueriesParam) { + return []; + } + + try { + const parsedValue: unknown = JSONFunctions.parse(metricQueriesParam); + + if (!Array.isArray(parsedValue)) { + return []; + } + + const sanitizedQueries: Array = []; + + for (const entry of parsedValue) { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + continue; + } + + const entryRecord: Record = entry as Record< + string, + unknown + >; + + const metricName: string = + typeof entryRecord["metricName"] === "string" + ? (entryRecord["metricName"] as string) + : ""; + + const attributes: Dictionary = + sanitizeAttributes(entryRecord["attributes"]); + + const aggregationType: MetricsAggregationType | undefined = + getAggregationTypeFromValue(entryRecord["aggregationType"]); + + sanitizedQueries.push({ + metricName, + attributes, + ...(aggregationType ? { aggregationType } : {}), + }); + } + + return sanitizedQueries; + } catch (err) { + return []; + } +} + +function getTimeRangeFromQuery(): InBetween | null { + const startTimeParam: string | null = + Navigation.getQueryStringByName("startTime"); + const endTimeParam: string | null = + Navigation.getQueryStringByName("endTime"); + + if (!startTimeParam || !endTimeParam) { + return null; + } + + if ( + !OneUptimeDate.isValidDateString(startTimeParam) || + !OneUptimeDate.isValidDateString(endTimeParam) + ) { + return null; + } + + try { + const startDate: Date = OneUptimeDate.fromString(startTimeParam); + const endDate: Date = OneUptimeDate.fromString(endTimeParam); + + if (!OneUptimeDate.isOnOrBefore(startDate, endDate)) { + return null; + } + + return new InBetween(startDate, endDate); + } catch (err) { + return null; + } +} + +function sanitizeAttributes( + value: unknown, +): Dictionary { + if (value === null || value === undefined) { + return {}; + } + + let candidate: unknown = value; + + if (typeof value === "string") { + try { + candidate = JSONFunctions.parse(value); + } catch (err) { + return {}; + } + } + + if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) { + return {}; + } + + const attributes: Dictionary = {}; + + for (const key in candidate as Record) { + const attributeValue: unknown = (candidate as Record)[key]; + + if ( + typeof attributeValue === "string" || + typeof attributeValue === "number" || + typeof attributeValue === "boolean" + ) { + attributes[key] = attributeValue; + } + } + + return attributes; +} + +function getAggregationTypeFromValue( + value: unknown, +): MetricsAggregationType | undefined { + if (typeof value === "string") { + const aggregationTypeValues: Array = Object.values( + MetricsAggregationType, + ) as Array; + + if (aggregationTypeValues.includes(value)) { + return value as MetricsAggregationType; + } + } + + return undefined; +} + +function isMeaningfulMetricQuery(query: MetricQueryFromUrl): boolean { + if (query.metricName) { + return true; + } + + if (Object.keys(query.attributes).length > 0) { + return true; + } + + if ( + query.aggregationType && + query.aggregationType !== MetricsAggregationType.Avg + ) { + return true; + } + + return false; +} diff --git a/Dashboard/src/Components/Metrics/MetricsTable.tsx b/Dashboard/src/Components/Metrics/MetricsTable.tsx index fccdf6a3b3..54211e4437 100644 --- a/Dashboard/src/Components/Metrics/MetricsTable.tsx +++ b/Dashboard/src/Components/Metrics/MetricsTable.tsx @@ -13,6 +13,7 @@ import ModelTable from "Common/UI/Components/ModelTable/ModelTable"; import MetricType from "Common/Models/DatabaseModels/MetricType"; import Includes from "Common/Types/BaseDatabase/Includes"; import TelemetryServicesElement from "../TelemetryService/TelemetryServiceElements"; +import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType"; export interface ComponentProps { telemetryServiceIds?: Array | undefined; @@ -24,11 +25,6 @@ const MetricsTable: FunctionComponent = ( const telemetryServiceFilterIds: Array = props.telemetryServiceIds || []; - const telemetryServiceIdForRoute: ObjectID | undefined = - telemetryServiceFilterIds.length === 1 - ? telemetryServiceFilterIds[0] - : undefined; - return ( @@ -50,36 +46,59 @@ const MetricsTable: FunctionComponent = ( "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.", }} onViewPage={async (item: MetricType) => { - if (!telemetryServiceIdForRoute) { - const route: Route = RouteUtil.populateRouteParams( - RouteMap[PageMap.TELEMETRY_METRIC_VIEW]!, - ); - - const currentUrl: URL = Navigation.getCurrentURL(); - - return new URL( - currentUrl.protocol, - currentUrl.hostname, - route, - `metricName=${item.name}`, - ); - } - const route: Route = RouteUtil.populateRouteParams( - RouteMap[PageMap.TELEMETRY_SERVICES_VIEW_METRIC]!, - { - modelId: telemetryServiceIdForRoute, - }, + RouteMap[PageMap.TELEMETRY_METRIC_VIEW]!, ); const currentUrl: URL = Navigation.getCurrentURL(); - - return new URL( + const metricUrl: URL = new URL( currentUrl.protocol, currentUrl.hostname, route, - `metricName=${item.name}`, ); + + const metricAttributes: Record = {}; + + if (telemetryServiceFilterIds.length === 1) { + const telemetryServiceId: ObjectID | undefined = + telemetryServiceFilterIds[0]; + + const serviceIdString: string | undefined = + telemetryServiceId?.toString(); + + if (serviceIdString) { + metricAttributes["oneuptime.service.id"] = serviceIdString; + + const matchingService: TelemetryService | undefined = ( + item.telemetryServices || [] + ).find((service: TelemetryService) => { + return service._id?.toString() === serviceIdString; + }); + + if (matchingService?.name) { + metricAttributes["oneuptime.service.name"] = + matchingService.name; + } + } + } + + const metricQueriesPayload: Array> = [ + { + metricName: item.name || "", + ...(Object.keys(metricAttributes).length > 0 + ? { attributes: metricAttributes } + : {}), + aggregationType: MetricsAggregationType.Avg, + }, + ]; + + metricUrl.addQueryParam( + "metricQueries", + JSON.stringify(metricQueriesPayload), + true, + ); + + return metricUrl; }} query={{ projectId: ProjectUtil.getCurrentProjectId()!, diff --git a/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx index ce22d1b72e..1a401c095c 100644 --- a/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx @@ -81,9 +81,7 @@ const ServiceCatalogMetrics: FunctionComponent< } return ( - + ); }; diff --git a/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx b/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx index 7b4ded09f2..4187343140 100644 --- a/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx +++ b/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx @@ -71,11 +71,7 @@ const MetricsTablePage: FunctionComponent< return ; } - return ( - - ); + return ; }; export default MetricsTablePage; From 38c29664ea13c445f8749cc2409aa6844f56edb9 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 30 Oct 2025 21:36:44 +0000 Subject: [PATCH 5/9] refactor(metrics): strengthen typings and clean up MetricExplorer - Add explicit types for initialQueryConfigs (MetricQueryConfigData) and map callbacks - Use FilterData and explicit Record for safer access - Type lastSerializedStateRef as React.MutableRefObject - Simplify catch blocks (remove unused error variables) and tidy parsing/sanitization logic - Minor formatting and type-safe attribute sanitization improvements --- .../src/Components/Metrics/MetricExplorer.tsx | 99 ++++++++++--------- 1 file changed, 55 insertions(+), 44 deletions(-) diff --git a/Dashboard/src/Components/Metrics/MetricExplorer.tsx b/Dashboard/src/Components/Metrics/MetricExplorer.tsx index 26af09a858..be69819ed3 100644 --- a/Dashboard/src/Components/Metrics/MetricExplorer.tsx +++ b/Dashboard/src/Components/Metrics/MetricExplorer.tsx @@ -10,9 +10,12 @@ import React, { import OneUptimeDate from "Common/Types/Date"; import InBetween from "Common/Types/BaseDatabase/InBetween"; import MetricViewData from "Common/Types/Metrics/MetricViewData"; +import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData"; import Dictionary from "Common/Types/Dictionary"; import JSONFunctions from "Common/Types/JSONFunctions"; import Text from "Common/Types/Text"; +import FilterData from "Common/UI/Components/Filters/Types/FilterData"; +import MetricsQuery from "Common/Types/Metrics/MetricsQuery"; const MetricExplorer: FunctionComponent = (): ReactElement => { const metricQueriesFromUrl: Array = @@ -31,27 +34,31 @@ const MetricExplorer: FunctionComponent = (): ReactElement => { const initialTimeRange: InBetween = getTimeRangeFromQuery() ?? defaultStartAndEndDate; - const initialQueryConfigs = metricQueriesFromUrl.map( - (metricQuery: MetricQueryFromUrl, index: number) => { - return { - metricAliasData: { - metricVariable: Text.getLetterFromAByNumber(index), - title: "", - description: "", - legend: "", - legendUnit: "", - }, - metricQueryData: { - filterData: { - metricName: metricQuery.metricName, - attributes: metricQuery.attributes, - aggegationType: - metricQuery.aggregationType || MetricsAggregationType.Avg, + const initialQueryConfigs: Array = + metricQueriesFromUrl.map( + ( + metricQuery: MetricQueryFromUrl, + index: number, + ): MetricQueryConfigData => { + return { + metricAliasData: { + metricVariable: Text.getLetterFromAByNumber(index), + title: "", + description: "", + legend: "", + legendUnit: "", }, - }, - }; - }, - ); + metricQueryData: { + filterData: { + metricName: metricQuery.metricName, + attributes: metricQuery.attributes, + aggegationType: + metricQuery.aggregationType || MetricsAggregationType.Avg, + }, + }, + }; + }, + ); const [metricViewData, setMetricViewData] = React.useState({ startAndEndDate: initialTimeRange, @@ -79,7 +86,8 @@ const MetricExplorer: FunctionComponent = (): ReactElement => { formulaConfigs: [], }); - const lastSerializedStateRef = useRef(""); + const lastSerializedStateRef: React.MutableRefObject = + useRef(""); useEffect(() => { const metricQueriesFromState: Array = @@ -155,32 +163,35 @@ type MetricQueryFromUrl = { function buildMetricQueriesFromState( data: MetricViewData, ): Array { - return data.queryConfigs.map((queryConfig) => { - const filterData = queryConfig.metricQueryData?.filterData || {}; + return data.queryConfigs.map( + (queryConfig: MetricQueryConfigData): MetricQueryFromUrl => { + const filterData: FilterData = + queryConfig.metricQueryData.filterData; + const filterDataRecord: Record = filterData as Record< + string, + unknown + >; - const metricNameValue: unknown = (filterData as Record)[ - "metricName" - ]; + const metricNameValue: unknown = filterDataRecord["metricName"]; - const metricName: string = - typeof metricNameValue === "string" ? metricNameValue : ""; + const metricName: string = + typeof metricNameValue === "string" ? metricNameValue : ""; - const aggregationValue: unknown = (filterData as Record)[ - "aggegationType" - ]; + const aggregationValue: unknown = filterDataRecord["aggegationType"]; - const aggregationType: MetricsAggregationType | undefined = - getAggregationTypeFromValue(aggregationValue); + const aggregationType: MetricsAggregationType | undefined = + getAggregationTypeFromValue(aggregationValue); - const attributes: Dictionary = - sanitizeAttributes((filterData as Record)["attributes"]); + const attributes: Dictionary = + sanitizeAttributes(filterDataRecord["attributes"]); - return { - metricName, - attributes, - ...(aggregationType ? { aggregationType } : {}), - }; - }); + return { + metricName, + attributes, + ...(aggregationType ? { aggregationType } : {}), + }; + }, + ); } function getMetricQueriesFromQuery(): Array { @@ -229,7 +240,7 @@ function getMetricQueriesFromQuery(): Array { } return sanitizedQueries; - } catch (err) { + } catch { return []; } } @@ -260,7 +271,7 @@ function getTimeRangeFromQuery(): InBetween | null { } return new InBetween(startDate, endDate); - } catch (err) { + } catch { return null; } } @@ -277,7 +288,7 @@ function sanitizeAttributes( if (typeof value === "string") { try { candidate = JSONFunctions.parse(value); - } catch (err) { + } catch { return {}; } } From d8fedc0b1935653673d698acc6974249db04027c Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Mon, 3 Nov 2025 19:33:12 +0000 Subject: [PATCH 6/9] refactor(filters,metrics): default to showing advanced filters and optimize metric fetching - add showAdvancedFiltersByDefault prop to FiltersForm and use it to initialize advanced filter visibility - MetricQuery: default showAdvancedFilters to true, pass showAdvancedFiltersByDefault to FiltersForm and call onAdvancedFiltersToggle once on mount - MetricView: introduce getFetchRelevantState and lastFetchSnapshotRef; only fetch aggregated results when relevant state (start/end dates or queryConfigs) actually changes --- Common/UI/Components/Filters/FiltersForm.tsx | 5 ++- .../src/Components/Metrics/MetricQuery.tsx | 14 +++++- .../src/Components/Metrics/MetricView.tsx | 44 ++++++++++++++++--- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/Common/UI/Components/Filters/FiltersForm.tsx b/Common/UI/Components/Filters/FiltersForm.tsx index 2bb6ef3e00..857ed190ec 100644 --- a/Common/UI/Components/Filters/FiltersForm.tsx +++ b/Common/UI/Components/Filters/FiltersForm.tsx @@ -27,6 +27,7 @@ export interface ComponentProps { onAdvancedFiltersToggle?: | undefined | ((showAdvancedFilters: boolean) => void); + showAdvancedFiltersByDefault?: boolean | undefined; } type FiltersFormFunction = ( @@ -56,7 +57,9 @@ const FiltersForm: FiltersFormFunction = ( }), ); - const [showMoreFilters, setShowMoreFilters] = React.useState(false); + const [showMoreFilters, setShowMoreFilters] = React.useState( + props.showAdvancedFiltersByDefault ?? false, + ); return (
diff --git a/Dashboard/src/Components/Metrics/MetricQuery.tsx b/Dashboard/src/Components/Metrics/MetricQuery.tsx index a9d393f122..c1c8ef1766 100644 --- a/Dashboard/src/Components/Metrics/MetricQuery.tsx +++ b/Dashboard/src/Components/Metrics/MetricQuery.tsx @@ -30,7 +30,18 @@ const MetricFilter: FunctionComponent = ( props: ComponentProps, ): ReactElement => { const [showAdvancedFilters, setShowAdvancedFilters] = - useState(false); + useState(true); + + const initializedAdvancedFilters = React.useRef(false); + + React.useEffect(() => { + if (initializedAdvancedFilters.current) { + return; + } + + initializedAdvancedFilters.current = true; + props.onAdvancedFiltersToggle?.(true); + }, [props.onAdvancedFiltersToggle]); return ( @@ -49,6 +60,7 @@ const MetricFilter: FunctionComponent = ( setShowAdvancedFilters(show); props.onAdvancedFiltersToggle?.(show); }} + showAdvancedFiltersByDefault={true} isFilterLoading={ showAdvancedFilters ? props.isAttributesLoading : false } diff --git a/Dashboard/src/Components/Metrics/MetricView.tsx b/Dashboard/src/Components/Metrics/MetricView.tsx index 09fcfde0f3..cf81b181fc 100644 --- a/Dashboard/src/Components/Metrics/MetricView.tsx +++ b/Dashboard/src/Components/Metrics/MetricView.tsx @@ -35,6 +35,24 @@ import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal"; import JSONFunctions from "Common/Types/JSONFunctions"; import MetricType from "Common/Models/DatabaseModels/MetricType"; +const getFetchRelevantState: (data: MetricViewData) => unknown = ( + data: MetricViewData, +) => { + return { + startAndEndDate: data.startAndEndDate + ? { + startValue: data.startAndEndDate.startValue, + endValue: data.startAndEndDate.endValue, + } + : null, + queryConfigs: data.queryConfigs.map((queryConfig: MetricQueryConfigData) => { + return { + metricQueryData: queryConfig.metricQueryData, + }; + }), + }; +}; + export interface ComponentProps { data: MetricViewData; hideQueryElements?: boolean; @@ -101,6 +119,9 @@ const MetricView: FunctionComponent = ( const metricViewDataRef: React.MutableRefObject = React.useRef(props.data); + const lastFetchSnapshotRef: React.MutableRefObject = React.useRef( + JSON.stringify(getFetchRelevantState(props.data)), + ); useEffect(() => { loadMetricTypes().catch((err: Error) => { @@ -114,20 +135,29 @@ const MetricView: FunctionComponent = ( props.data, ); - if ( - hasChanged && - props.data && - props.data.startAndEndDate && - props.data.startAndEndDate.startValue && - props.data.startAndEndDate.endValue - ) { + if (hasChanged) { setCurrentQueryVariable( Text.getLetterFromAByNumber(props.data.queryConfigs.length), ); + } + + const currentFetchSnapshot: string = JSON.stringify( + getFetchRelevantState(props.data), + ); + + const shouldFetch: boolean = + currentFetchSnapshot !== lastFetchSnapshotRef.current && + Boolean(props.data?.startAndEndDate?.startValue) && + Boolean(props.data?.startAndEndDate?.endValue); + + if (shouldFetch) { + lastFetchSnapshotRef.current = currentFetchSnapshot; fetchAggregatedResults().catch((err: Error) => { setMetricResultsError(API.getFriendlyErrorMessage(err as Error)); }); + } + if (hasChanged) { metricViewDataRef.current = props.data; } }, [props.data]); From 3264322054f22bf05ca04552c84a6371668d707d Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Mon, 3 Nov 2025 19:41:48 +0000 Subject: [PATCH 7/9] refactor(metrics): support metric query alias in URL/state - add MetricQueryAliasFromUrl type - populate initial metric alias fields from parsed URL into initial query configs - include alias when building metricQueries for the URL and when parsing metricQueries from the URL - add sanitizeAlias and buildAliasFromMetricAliasData helpers to validate/serialize alias fields --- .../src/Components/Metrics/MetricExplorer.tsx | 110 +++++++++++++++++- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/Dashboard/src/Components/Metrics/MetricExplorer.tsx b/Dashboard/src/Components/Metrics/MetricExplorer.tsx index be69819ed3..78b8eb14fd 100644 --- a/Dashboard/src/Components/Metrics/MetricExplorer.tsx +++ b/Dashboard/src/Components/Metrics/MetricExplorer.tsx @@ -43,10 +43,10 @@ const MetricExplorer: FunctionComponent = (): ReactElement => { return { metricAliasData: { metricVariable: Text.getLetterFromAByNumber(index), - title: "", - description: "", - legend: "", - legendUnit: "", + title: metricQuery.alias?.title || "", + description: metricQuery.alias?.description || "", + legend: metricQuery.alias?.legend || "", + legendUnit: metricQuery.alias?.legendUnit || "", }, metricQueryData: { filterData: { @@ -158,6 +158,14 @@ type MetricQueryFromUrl = { metricName: string; attributes: Dictionary; aggregationType?: MetricsAggregationType; + alias?: MetricQueryAliasFromUrl; +}; + +type MetricQueryAliasFromUrl = { + title?: string; + description?: string; + legend?: string; + legendUnit?: string; }; function buildMetricQueriesFromState( @@ -185,10 +193,14 @@ function buildMetricQueriesFromState( const attributes: Dictionary = sanitizeAttributes(filterDataRecord["attributes"]); + const aliasData: MetricQueryAliasFromUrl | undefined = + buildAliasFromMetricAliasData(queryConfig.metricAliasData); + return { metricName, attributes, ...(aggregationType ? { aggregationType } : {}), + ...(aliasData ? { alias: aliasData } : {}), }; }, ); @@ -232,10 +244,16 @@ function getMetricQueriesFromQuery(): Array { const aggregationType: MetricsAggregationType | undefined = getAggregationTypeFromValue(entryRecord["aggregationType"]); + const alias: MetricQueryAliasFromUrl | undefined = sanitizeAlias( + entryRecord["alias"], + entryRecord, + ); + sanitizedQueries.push({ metricName, attributes, ...(aggregationType ? { aggregationType } : {}), + ...(alias ? { alias } : {}), }); } @@ -314,6 +332,90 @@ function sanitizeAttributes( return attributes; } +function buildAliasFromMetricAliasData( + data: MetricQueryConfigData["metricAliasData"], +): MetricQueryAliasFromUrl | undefined { + if (!data) { + return undefined; + } + + const alias: MetricQueryAliasFromUrl = {}; + + if (typeof data.title === "string" && data.title.trim() !== "") { + alias.title = data.title; + } + + if ( + typeof data.description === "string" && data.description.trim() !== "" + ) { + alias.description = data.description; + } + + if (typeof data.legend === "string" && data.legend.trim() !== "") { + alias.legend = data.legend; + } + + if (typeof data.legendUnit === "string" && data.legendUnit.trim() !== "") { + alias.legendUnit = data.legendUnit; + } + + return Object.keys(alias).length > 0 ? alias : undefined; +} + +function sanitizeAlias( + value: unknown, + fallback?: Record, +): MetricQueryAliasFromUrl | undefined { + const alias: MetricQueryAliasFromUrl = {}; + + if (value && typeof value === "object" && !Array.isArray(value)) { + const aliasRecord: Record = value as Record; + + if (typeof aliasRecord["title"] === "string") { + alias.title = aliasRecord["title"] as string; + } + + if (typeof aliasRecord["description"] === "string") { + alias.description = aliasRecord["description"] as string; + } + + if (typeof aliasRecord["legend"] === "string") { + alias.legend = aliasRecord["legend"] as string; + } + + if (typeof aliasRecord["legendUnit"] === "string") { + alias.legendUnit = aliasRecord["legendUnit"] as string; + } + } + + // Backward compatibility: allow flat keys on the main query record. + if (fallback) { + if (alias.title === undefined && typeof fallback["title"] === "string") { + alias.title = fallback["title"] as string; + } + + if ( + alias.description === undefined && + typeof fallback["description"] === "string" + ) { + alias.description = fallback["description"] as string; + } + + if (alias.legend === undefined && typeof fallback["legend"] === "string") { + alias.legend = fallback["legend"] as string; + } + + if ( + alias.legendUnit === undefined && + typeof fallback["legendUnit"] === "string" + ) { + alias.legendUnit = fallback["legendUnit"] as string; + } + } + + return Object.keys(alias).length > 0 ? alias : undefined; +} + function getAggregationTypeFromValue( value: unknown, ): MetricsAggregationType | undefined { From 72fc633bf10723f09ca20ca2e91f572b2bb31f8d Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Mon, 3 Nov 2025 19:44:21 +0000 Subject: [PATCH 8/9] refactor(metrics): treat metric query alias as meaningful so alias-only queries are preserved in URL/state --- Dashboard/src/Components/Metrics/MetricExplorer.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dashboard/src/Components/Metrics/MetricExplorer.tsx b/Dashboard/src/Components/Metrics/MetricExplorer.tsx index 78b8eb14fd..ae504ffce2 100644 --- a/Dashboard/src/Components/Metrics/MetricExplorer.tsx +++ b/Dashboard/src/Components/Metrics/MetricExplorer.tsx @@ -448,5 +448,9 @@ function isMeaningfulMetricQuery(query: MetricQueryFromUrl): boolean { return true; } + if (query.alias && Object.keys(query.alias).length > 0) { + return true; + } + return false; } From 9edc6ac428565430ea3b644ab15b26c16a349f7b Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Mon, 3 Nov 2025 19:47:27 +0000 Subject: [PATCH 9/9] refactor(metrics): normalize type annotations and formatting in MetricExplorer/MetricQuery/MetricView --- Dashboard/src/Components/Metrics/MetricExplorer.tsx | 9 +++++---- Dashboard/src/Components/Metrics/MetricQuery.tsx | 6 +++--- Dashboard/src/Components/Metrics/MetricView.tsx | 12 +++++++----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Dashboard/src/Components/Metrics/MetricExplorer.tsx b/Dashboard/src/Components/Metrics/MetricExplorer.tsx index ae504ffce2..602194529d 100644 --- a/Dashboard/src/Components/Metrics/MetricExplorer.tsx +++ b/Dashboard/src/Components/Metrics/MetricExplorer.tsx @@ -345,9 +345,7 @@ function buildAliasFromMetricAliasData( alias.title = data.title; } - if ( - typeof data.description === "string" && data.description.trim() !== "" - ) { + if (typeof data.description === "string" && data.description.trim() !== "") { alias.description = data.description; } @@ -369,7 +367,10 @@ function sanitizeAlias( const alias: MetricQueryAliasFromUrl = {}; if (value && typeof value === "object" && !Array.isArray(value)) { - const aliasRecord: Record = value as Record; + const aliasRecord: Record = value as Record< + string, + unknown + >; if (typeof aliasRecord["title"] === "string") { alias.title = aliasRecord["title"] as string; diff --git a/Dashboard/src/Components/Metrics/MetricQuery.tsx b/Dashboard/src/Components/Metrics/MetricQuery.tsx index c1c8ef1766..15a427143d 100644 --- a/Dashboard/src/Components/Metrics/MetricQuery.tsx +++ b/Dashboard/src/Components/Metrics/MetricQuery.tsx @@ -29,10 +29,10 @@ export interface ComponentProps { const MetricFilter: FunctionComponent = ( props: ComponentProps, ): ReactElement => { - const [showAdvancedFilters, setShowAdvancedFilters] = - useState(true); + const [showAdvancedFilters, setShowAdvancedFilters] = useState(true); - const initializedAdvancedFilters = React.useRef(false); + const initializedAdvancedFilters: React.MutableRefObject = + React.useRef(false); React.useEffect(() => { if (initializedAdvancedFilters.current) { diff --git a/Dashboard/src/Components/Metrics/MetricView.tsx b/Dashboard/src/Components/Metrics/MetricView.tsx index cf81b181fc..b574920e70 100644 --- a/Dashboard/src/Components/Metrics/MetricView.tsx +++ b/Dashboard/src/Components/Metrics/MetricView.tsx @@ -45,11 +45,13 @@ const getFetchRelevantState: (data: MetricViewData) => unknown = ( endValue: data.startAndEndDate.endValue, } : null, - queryConfigs: data.queryConfigs.map((queryConfig: MetricQueryConfigData) => { - return { - metricQueryData: queryConfig.metricQueryData, - }; - }), + queryConfigs: data.queryConfigs.map( + (queryConfig: MetricQueryConfigData) => { + return { + metricQueryData: queryConfig.metricQueryData, + }; + }, + ), }; };