Merge pull request #2067 from OneUptime/service-catalog-pages

Service catalog pages
This commit is contained in:
Simon Larsen
2025-11-03 19:50:54 +00:00
committed by GitHub
15 changed files with 1167 additions and 87 deletions

View File

@@ -27,6 +27,7 @@ export interface ComponentProps<T extends GenericObject> {
onAdvancedFiltersToggle?:
| undefined
| ((showAdvancedFilters: boolean) => void);
showAdvancedFiltersByDefault?: boolean | undefined;
}
type FiltersFormFunction = <T extends GenericObject>(
@@ -56,7 +57,9 @@ const FiltersForm: FiltersFormFunction = <T extends GenericObject>(
}),
);
const [showMoreFilters, setShowMoreFilters] = React.useState<boolean>(false);
const [showMoreFilters, setShowMoreFilters] = React.useState<boolean>(
props.showAdvancedFiltersByDefault ?? false,
);
return (
<div id={props.id}>

View File

@@ -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<MetricQueryFromUrl> =
getMetricQueriesFromQuery();
const serviceName: string =
Navigation.getQueryStringByName("serviceName") || "";
const defaultEndDate: Date = OneUptimeDate.getCurrentDate();
const defaultStartDate: Date = OneUptimeDate.addRemoveHours(
defaultEndDate,
-1,
);
const defaultStartAndEndDate: InBetween<Date> = 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<Date> =
getTimeRangeFromQuery() ?? defaultStartAndEndDate;
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
const initialQueryConfigs: Array<MetricQueryConfigData> =
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<MetricViewData>({
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<string> =
useRef<string>("");
useEffect(() => {
const metricQueriesFromState: Array<MetricQueryFromUrl> =
buildMetricQueriesFromState(metricViewData);
const metricQueriesForUrl: Array<MetricQueryFromUrl> =
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 (
<MetricView
data={metricViewData}
@@ -57,3 +153,305 @@ const MetricExplorer: FunctionComponent = (): ReactElement => {
};
export default MetricExplorer;
type MetricQueryFromUrl = {
metricName: string;
attributes: Dictionary<string | number | boolean>;
aggregationType?: MetricsAggregationType;
alias?: MetricQueryAliasFromUrl;
};
type MetricQueryAliasFromUrl = {
title?: string;
description?: string;
legend?: string;
legendUnit?: string;
};
function buildMetricQueriesFromState(
data: MetricViewData,
): Array<MetricQueryFromUrl> {
return data.queryConfigs.map(
(queryConfig: MetricQueryConfigData): MetricQueryFromUrl => {
const filterData: FilterData<MetricsQuery> =
queryConfig.metricQueryData.filterData;
const filterDataRecord: Record<string, unknown> = 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<string | number | boolean> =
sanitizeAttributes(filterDataRecord["attributes"]);
const aliasData: MetricQueryAliasFromUrl | undefined =
buildAliasFromMetricAliasData(queryConfig.metricAliasData);
return {
metricName,
attributes,
...(aggregationType ? { aggregationType } : {}),
...(aliasData ? { alias: aliasData } : {}),
};
},
);
}
function getMetricQueriesFromQuery(): Array<MetricQueryFromUrl> {
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<MetricQueryFromUrl> = [];
for (const entry of parsedValue) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
continue;
}
const entryRecord: Record<string, unknown> = entry as Record<
string,
unknown
>;
const metricName: string =
typeof entryRecord["metricName"] === "string"
? (entryRecord["metricName"] as string)
: "";
const attributes: Dictionary<string | number | boolean> =
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<Date> | 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<string | number | boolean> {
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<string | number | boolean> = {};
for (const key in candidate as Record<string, unknown>) {
const attributeValue: unknown = (candidate as Record<string, unknown>)[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<string, unknown>,
): MetricQueryAliasFromUrl | undefined {
const alias: MetricQueryAliasFromUrl = {};
if (value && typeof value === "object" && !Array.isArray(value)) {
const aliasRecord: Record<string, unknown> = 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<string> = Object.values(
MetricsAggregationType,
) as Array<string>;
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;
}

View File

@@ -29,8 +29,19 @@ export interface ComponentProps {
const MetricFilter: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [showAdvancedFilters, setShowAdvancedFilters] =
useState<boolean>(false);
const [showAdvancedFilters, setShowAdvancedFilters] = useState<boolean>(true);
const initializedAdvancedFilters: React.MutableRefObject<boolean> =
React.useRef<boolean>(false);
React.useEffect(() => {
if (initializedAdvancedFilters.current) {
return;
}
initializedAdvancedFilters.current = true;
props.onAdvancedFiltersToggle?.(true);
}, [props.onAdvancedFiltersToggle]);
return (
<Fragment>
@@ -49,6 +60,7 @@ const MetricFilter: FunctionComponent<ComponentProps> = (
setShowAdvancedFilters(show);
props.onAdvancedFiltersToggle?.(show);
}}
showAdvancedFiltersByDefault={true}
isFilterLoading={
showAdvancedFilters ? props.isAttributesLoading : false
}

View File

@@ -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<ComponentProps> = (
const metricViewDataRef: React.MutableRefObject<MetricViewData> =
React.useRef(props.data);
const lastFetchSnapshotRef: React.MutableRefObject<string> = React.useRef(
JSON.stringify(getFetchRelevantState(props.data)),
);
useEffect(() => {
loadMetricTypes().catch((err: Error) => {
@@ -114,20 +137,29 @@ const MetricView: FunctionComponent<ComponentProps> = (
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]);

View File

@@ -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<ObjectID> | undefined;
}
const MetricsTable: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const telemetryServiceFilterIds: Array<ObjectID> =
props.telemetryServiceIds || [];
return (
<Fragment>
<ModelTable<MetricType>
@@ -41,42 +46,73 @@ const MetricsTable: FunctionComponent<ComponentProps> = (
"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<string, string> = {};
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<Record<string, unknown>> = [
{
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<ComponentProps> = (
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<ComponentProps> = (
title: "Name",
type: FieldType.Text,
},
{
field: {
telemetryServices: {
name: true,
_id: true,
serviceColor: true,
},
},
title: "Telemetry Services",
type: FieldType.Element,
getElement: (item: MetricType): ReactElement => {
return (
<TelemetryServicesElement
telemetryServices={item.telemetryServices || []}
/>
);
},
},
]}
/>
</Fragment>

View File

@@ -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<Array<ObjectID> | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const fetchMonitorsInService: PromiseVoidFunction =
async (): Promise<void> => {
try {
setIsLoading(true);
const serviceCatalogMonitors: ListResult<ServiceCatalogMonitor> =
await ModelAPI.getList<ServiceCatalogMonitor>({
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 <ErrorMessage message={error} />;
}
if (isLoading || monitorIds === null) {
return <PageLoader isVisible={true} />;
}
const query: Query<Alert> = {
monitorId: new Includes(monitorIds),
};
return (
<Fragment>
<AlertsTable
query={query}
title="Service Alerts"
description="Alerts generated by monitors attached to this service."
noItemsMessage={"No alerts found for this service."}
/>
</Fragment>
);
};
export default ServiceCatalogAlerts;

View File

@@ -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<Array<ObjectID> | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const fetchTelemetryServices: PromiseVoidFunction =
async (): Promise<void> => {
try {
setIsLoading(true);
const response: ListResult<ServiceCatalogTelemetryService> =
await ModelAPI.getList<ServiceCatalogTelemetryService>({
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 <ErrorMessage message={error} />;
}
if (isLoading || telemetryServiceIds === null) {
return <PageLoader isVisible={true} />;
}
if (telemetryServiceIds.length === 0) {
return (
<ErrorMessage message="Assign telemetry services to this service to view logs." />
);
}
return (
<Fragment>
<DashboardLogsViewer
id="service-catalog-logs"
telemetryServiceIds={telemetryServiceIds}
showFilters={true}
enableRealtime={true}
limit={100}
noLogsMessage="No logs found for this service."
/>
</Fragment>
);
};
export default ServiceCatalogLogs;

View File

@@ -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<Array<ObjectID> | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const fetchTelemetryServices: PromiseVoidFunction =
async (): Promise<void> => {
try {
setIsLoading(true);
const response: ListResult<ServiceCatalogTelemetryService> =
await ModelAPI.getList<ServiceCatalogTelemetryService>({
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 <ErrorMessage message={error} />;
}
if (isLoading || telemetryServiceIds === null) {
return <PageLoader isVisible={true} />;
}
if (telemetryServiceIds.length === 0) {
return (
<ErrorMessage message="Assign telemetry services to this service to view metrics." />
);
}
return (
<Fragment>
<MetricsTable telemetryServiceIds={telemetryServiceIds} />
</Fragment>
);
};
export default ServiceCatalogMetrics;

View File

@@ -66,18 +66,7 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
<SideMenuItem
link={{
title: "Incidents",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_CATALOG_VIEW_INCIDENTS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Alert}
/>
<SideMenuItem
link={{
title: "Telemetry",
title: "Telemetry Services",
to: RouteUtil.populateRouteParams(
RouteMap[
PageMap.SERVICE_CATALOG_VIEW_TELEMETRY_SERVICES
@@ -89,6 +78,65 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
/>
</SideMenuSection>
<SideMenuSection title="Operations">
<SideMenuItem
link={{
title: "Alerts",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_CATALOG_VIEW_ALERTS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.BellRinging}
/>
<SideMenuItem
link={{
title: "Incidents",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_CATALOG_VIEW_INCIDENTS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Alert}
/>
</SideMenuSection>
<SideMenuSection title="Telemetry">
<SideMenuItem
link={{
title: "Logs",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_CATALOG_VIEW_LOGS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Logs}
/>
<SideMenuItem
link={{
title: "Traces",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_CATALOG_VIEW_TRACES] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Workflow}
/>
<SideMenuItem
link={{
title: "Metrics",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SERVICE_CATALOG_VIEW_METRICS] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.Graph}
/>
</SideMenuSection>
<SideMenuSection title="Advanced">
<SideMenuItem
link={{

View File

@@ -0,0 +1,100 @@
import TraceTable from "../../../Components/Traces/TraceTable";
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";
import Includes from "Common/Types/BaseDatabase/Includes";
import Query from "Common/Types/BaseDatabase/Query";
import Span from "Common/Models/AnalyticsModels/Span";
const ServiceCatalogTraces: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [telemetryServiceIds, setTelemetryServiceIds] =
useState<Array<ObjectID> | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const fetchTelemetryServices: PromiseVoidFunction =
async (): Promise<void> => {
try {
setIsLoading(true);
const response: ListResult<ServiceCatalogTelemetryService> =
await ModelAPI.getList<ServiceCatalogTelemetryService>({
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 <ErrorMessage message={error} />;
}
if (isLoading || telemetryServiceIds === null) {
return <PageLoader isVisible={true} />;
}
if (telemetryServiceIds.length === 0) {
return (
<ErrorMessage message="Assign telemetry services to this service to view traces." />
);
}
const spanQuery: Query<Span> = {
serviceId: new Includes(telemetryServiceIds),
};
return (
<Fragment>
<TraceTable
spanQuery={spanQuery}
noItemsMessage="No traces found for this service."
/>
</Fragment>
);
};
export default ServiceCatalogTraces;

View File

@@ -71,12 +71,7 @@ const MetricsTablePage: FunctionComponent<
return <ErrorMessage message="Telemetry Service not found." />;
}
return (
<MetricsTable
telemetryServiceId={telemetryService.id!}
telemetryServiceName={telemetryService.name!}
/>
);
return <MetricsTable telemetryServiceIds={[telemetryService.id!]} />;
};
export default MetricsTablePage;

View File

@@ -45,12 +45,36 @@ const ServiceCatalogViewIncidents: LazyExoticComponent<
return import("../Pages/ServiceCatalog/View/Incidents");
});
const ServiceCatalogViewAlerts: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
return import("../Pages/ServiceCatalog/View/Alerts");
});
const ServiceCatalogViewTelemetryServices: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
return import("../Pages/ServiceCatalog/View/TelemetryServices");
});
const ServiceCatalogViewLogs: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
return import("../Pages/ServiceCatalog/View/Logs");
});
const ServiceCatalogViewTraces: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
return import("../Pages/ServiceCatalog/View/Traces");
});
const ServiceCatalogViewMetrics: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
return import("../Pages/ServiceCatalog/View/Metrics");
});
const ServiceCatalogViewDelete: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
@@ -173,6 +197,22 @@ const ServiceCatalogRoutes: FunctionComponent<ComponentProps> = (
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.SERVICE_CATALOG_VIEW_ALERTS,
)}
element={
<Suspense fallback={Loader}>
<ServiceCatalogViewAlerts
{...props}
pageRoute={
RouteMap[PageMap.SERVICE_CATALOG_VIEW_ALERTS] as Route
}
/>
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.SERVICE_CATALOG_VIEW_INCIDENTS,
@@ -207,6 +247,50 @@ const ServiceCatalogRoutes: FunctionComponent<ComponentProps> = (
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.SERVICE_CATALOG_VIEW_LOGS)}
element={
<Suspense fallback={Loader}>
<ServiceCatalogViewLogs
{...props}
pageRoute={RouteMap[PageMap.SERVICE_CATALOG_VIEW_LOGS] as Route}
/>
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.SERVICE_CATALOG_VIEW_TRACES,
)}
element={
<Suspense fallback={Loader}>
<ServiceCatalogViewTraces
{...props}
pageRoute={
RouteMap[PageMap.SERVICE_CATALOG_VIEW_TRACES] as Route
}
/>
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.SERVICE_CATALOG_VIEW_METRICS,
)}
element={
<Suspense fallback={Loader}>
<ServiceCatalogViewMetrics
{...props}
pageRoute={
RouteMap[PageMap.SERVICE_CATALOG_VIEW_METRICS] as Route
}
/>
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.SERVICE_CATALOG_VIEW_OWNERS,

View File

@@ -51,12 +51,36 @@ export function getServiceCatalogBreadcrumbs(
"View Service",
"Monitors",
]),
...BuildBreadcrumbLinksByTitles(PageMap.SERVICE_CATALOG_VIEW_ALERTS, [
"Project",
"Service Catalog",
"View Service",
"Alerts",
]),
...BuildBreadcrumbLinksByTitles(PageMap.SERVICE_CATALOG_VIEW_INCIDENTS, [
"Project",
"Service Catalog",
"View Service",
"Incidents",
]),
...BuildBreadcrumbLinksByTitles(PageMap.SERVICE_CATALOG_VIEW_LOGS, [
"Project",
"Service Catalog",
"View Service",
"Logs",
]),
...BuildBreadcrumbLinksByTitles(PageMap.SERVICE_CATALOG_VIEW_TRACES, [
"Project",
"Service Catalog",
"View Service",
"Traces",
]),
...BuildBreadcrumbLinksByTitles(PageMap.SERVICE_CATALOG_VIEW_METRICS, [
"Project",
"Service Catalog",
"View Service",
"Metrics",
]),
...BuildBreadcrumbLinksByTitles(
PageMap.SERVICE_CATALOG_VIEW_TELEMETRY_SERVICES,
["Project", "Service Catalog", "View Service", "Telemetry"],

View File

@@ -165,6 +165,10 @@ enum PageMap {
SERVICE_CATALOG_VIEW_SETTINGS = "SERVICE_CATALOG_VIEW_SETTINGS",
SERVICE_CATALOG_VIEW_MONITORS = "SERVICE_CATALOG_VIEW_MONITORS",
SERVICE_CATALOG_VIEW_INCIDENTS = "SERVICE_CATALOG_VIEW_INCIDENTS",
SERVICE_CATALOG_VIEW_ALERTS = "SERVICE_CATALOG_VIEW_ALERTS",
SERVICE_CATALOG_VIEW_LOGS = "SERVICE_CATALOG_VIEW_LOGS",
SERVICE_CATALOG_VIEW_TRACES = "SERVICE_CATALOG_VIEW_TRACES",
SERVICE_CATALOG_VIEW_METRICS = "SERVICE_CATALOG_VIEW_METRICS",
SERVICE_CATALOG_VIEW_TELEMETRY_SERVICES = "SERVICE_CATALOG_VIEW_TELEMETRY_SERVICES",
SERVICE_CATALOG_VIEW_OWNERS = "SERVICE_CATALOG_VIEW_OWNERS",
SERVICE_CATALOG_VIEW_DEPENDENCIES = "SERVICE_CATALOG_VIEW_DEPENDENCIES",

View File

@@ -39,6 +39,10 @@ export const ServiceCatalogRoutePath: Dictionary<string> = {
[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<Route> = {
}`,
),
[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]