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/MetricExplorer.tsx b/Dashboard/src/Components/Metrics/MetricExplorer.tsx index c2e65f75f2..602194529d 100644 --- a/Dashboard/src/Components/Metrics/MetricExplorer.tsx +++ b/Dashboard/src/Components/Metrics/MetricExplorer.tsx @@ -1,51 +1,147 @@ 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 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 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 initialQueryConfigs: Array = + metricQueriesFromUrl.map( + ( + metricQuery: MetricQueryFromUrl, + index: number, + ): MetricQueryConfigData => { + return { + metricAliasData: { + metricVariable: Text.getLetterFromAByNumber(index), + title: metricQuery.alias?.title || "", + description: metricQuery.alias?.description || "", + legend: metricQuery.alias?.legend || "", + legendUnit: metricQuery.alias?.legendUnit || "", + }, + metricQueryData: { + filterData: { + metricName: metricQuery.metricName, + attributes: metricQuery.attributes, + aggegationType: + metricQuery.aggregationType || MetricsAggregationType.Avg, + }, + }, + }; + }, + ); const [metricViewData, setMetricViewData] = React.useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [ - { - metricAliasData: { - metricVariable: "a", - title: "", - description: "", - legend: "", - legendUnit: "", - }, - metricQueryData: { - filterData: { - metricName: metricName, - attributes: serviceName - ? { - "resource.oneuptime.telemetry.service.name": serviceName, - } - : {}, - aggegationType: MetricsAggregationType.Avg, - }, - }, - }, - ], + startAndEndDate: initialTimeRange, + queryConfigs: + initialQueryConfigs.length > 0 + ? initialQueryConfigs + : [ + { + metricAliasData: { + metricVariable: "a", + title: "", + description: "", + legend: "", + legendUnit: "", + }, + metricQueryData: { + filterData: { + metricName: "", + attributes: {}, + aggegationType: MetricsAggregationType.Avg, + }, + }, + }, + ], formulaConfigs: [], }); + const lastSerializedStateRef: React.MutableRefObject = + 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; + alias?: MetricQueryAliasFromUrl; +}; + +type MetricQueryAliasFromUrl = { + title?: string; + description?: string; + legend?: string; + legendUnit?: string; +}; + +function buildMetricQueriesFromState( + data: MetricViewData, +): Array { + return data.queryConfigs.map( + (queryConfig: MetricQueryConfigData): MetricQueryFromUrl => { + const filterData: FilterData = + queryConfig.metricQueryData.filterData; + const filterDataRecord: Record = filterData as Record< + string, + unknown + >; + + const metricNameValue: unknown = filterDataRecord["metricName"]; + + const metricName: string = + typeof metricNameValue === "string" ? metricNameValue : ""; + + const aggregationValue: unknown = filterDataRecord["aggegationType"]; + + const aggregationType: MetricsAggregationType | undefined = + getAggregationTypeFromValue(aggregationValue); + + const attributes: Dictionary = + sanitizeAttributes(filterDataRecord["attributes"]); + + const aliasData: MetricQueryAliasFromUrl | undefined = + buildAliasFromMetricAliasData(queryConfig.metricAliasData); + + return { + metricName, + attributes, + ...(aggregationType ? { aggregationType } : {}), + ...(aliasData ? { alias: aliasData } : {}), + }; + }, + ); +} + +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"]); + + const alias: MetricQueryAliasFromUrl | undefined = sanitizeAlias( + entryRecord["alias"], + entryRecord, + ); + + sanitizedQueries.push({ + metricName, + attributes, + ...(aggregationType ? { aggregationType } : {}), + ...(alias ? { alias } : {}), + }); + } + + return sanitizedQueries; + } catch { + 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 { + 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 { + 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 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< + string, + unknown + >; + + 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 { + 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; + } + + if (query.alias && Object.keys(query.alias).length > 0) { + return true; + } + + return false; +} diff --git a/Dashboard/src/Components/Metrics/MetricQuery.tsx b/Dashboard/src/Components/Metrics/MetricQuery.tsx index a9d393f122..15a427143d 100644 --- a/Dashboard/src/Components/Metrics/MetricQuery.tsx +++ b/Dashboard/src/Components/Metrics/MetricQuery.tsx @@ -29,8 +29,19 @@ export interface ComponentProps { const MetricFilter: FunctionComponent = ( props: ComponentProps, ): ReactElement => { - const [showAdvancedFilters, setShowAdvancedFilters] = - useState(false); + const [showAdvancedFilters, setShowAdvancedFilters] = useState(true); + + const initializedAdvancedFilters: React.MutableRefObject = + 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..b574920e70 100644 --- a/Dashboard/src/Components/Metrics/MetricView.tsx +++ b/Dashboard/src/Components/Metrics/MetricView.tsx @@ -35,6 +35,26 @@ 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 +121,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 +137,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]); diff --git a/Dashboard/src/Components/Metrics/MetricsTable.tsx b/Dashboard/src/Components/Metrics/MetricsTable.tsx index b1e34fcce5..54211e4437 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,19 @@ 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"; +import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType"; export interface ComponentProps { - telemetryServiceId?: ObjectID | undefined; - telemetryServiceName?: string | undefined; + telemetryServiceIds?: Array | undefined; } const MetricsTable: FunctionComponent = ( props: ComponentProps, ): ReactElement => { + const telemetryServiceFilterIds: Array = + props.telemetryServiceIds || []; + return ( @@ -41,42 +46,73 @@ 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) { - 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: props.telemetryServiceId, - }, + 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}&serviceName=${props.telemetryServiceName}`, ); + + 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()!, - 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 +126,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 +152,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/Alerts.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Alerts.tsx new file mode 100644 index 0000000000..598c99c4cb --- /dev/null +++ b/Dashboard/src/Pages/ServiceCatalog/View/Alerts.tsx @@ -0,0 +1,95 @@ +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< + 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 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..d60d75bb7b --- /dev/null +++ b/Dashboard/src/Pages/ServiceCatalog/View/Logs.tsx @@ -0,0 +1,97 @@ +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< + PageComponentProps +> = (): 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..1a401c095c --- /dev/null +++ b/Dashboard/src/Pages/ServiceCatalog/View/Metrics.tsx @@ -0,0 +1,89 @@ +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 { 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 ServiceCatalogMetrics: FunctionComponent< + PageComponentProps +> = (): 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 ServiceCatalogMetrics; diff --git a/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx b/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx index 7524bdd31e..13f8805e3b 100644 --- a/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/View/SideMenu.tsx @@ -66,18 +66,7 @@ const DashboardSideMenu: 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 ( + + ); + } + + const spanQuery: Query = { + serviceId: new Includes(telemetryServiceIds), + }; + + return ( + + + + ); +}; + +export default ServiceCatalogTraces; diff --git a/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx b/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx index b23ee44c8c..4187343140 100644 --- a/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx +++ b/Dashboard/src/Pages/Telemetry/Services/View/Metrics/Index.tsx @@ -71,12 +71,7 @@ const MetricsTablePage: FunctionComponent< return ; } - return ( - - ); + return ; }; export default MetricsTablePage; diff --git a/Dashboard/src/Routes/ServiceCatalogRoutes.tsx b/Dashboard/src/Routes/ServiceCatalogRoutes.tsx index a58bf7bd1b..71f38b4525 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]