mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Add dashboard template selection and creation functionality
- Introduced DashboardTemplateCard component for displaying dashboard templates. - Added DashboardTemplates enum and DashboardTemplate interface to define available templates. - Implemented template selection in the Dashboards page to allow users to create dashboards from predefined templates. - Enhanced MetricQueryConfig to manage metric attributes and display settings more effectively. - Updated MetricView to improve loading states and error handling for metric results. - Refactored DashboardChartComponent to streamline metric alias data handling and improve UI presentation.
This commit is contained in:
@@ -209,22 +209,14 @@ const DashboardChartComponentElement: FunctionComponent<ComponentProps> = (
|
||||
|
||||
const chartMetricViewData: MetricViewData = {
|
||||
queryConfigs: queryConfigs.map(
|
||||
(config: MetricQueryConfigData, index: number) => {
|
||||
(config: MetricQueryConfigData) => {
|
||||
return {
|
||||
...config,
|
||||
metricAliasData: {
|
||||
metricVariable:
|
||||
config.metricAliasData?.metricVariable || undefined,
|
||||
title:
|
||||
(index === 0
|
||||
? config.metricAliasData?.title ||
|
||||
props.component.arguments.chartTitle
|
||||
: config.metricAliasData?.title) || undefined,
|
||||
description:
|
||||
(index === 0
|
||||
? config.metricAliasData?.description ||
|
||||
props.component.arguments.chartDescription
|
||||
: config.metricAliasData?.description) || undefined,
|
||||
title: config.metricAliasData?.title || undefined,
|
||||
description: config.metricAliasData?.description || undefined,
|
||||
legend: config.metricAliasData?.legend || undefined,
|
||||
legendUnit: config.metricAliasData?.legendUnit || undefined,
|
||||
},
|
||||
@@ -246,6 +238,21 @@ const DashboardChartComponentElement: FunctionComponent<ComponentProps> = (
|
||||
transition: "opacity 0.2s ease-in-out",
|
||||
}}
|
||||
>
|
||||
{(props.component.arguments.chartTitle ||
|
||||
props.component.arguments.chartDescription) && (
|
||||
<div className="px-2 pt-2 pb-1">
|
||||
{props.component.arguments.chartTitle && (
|
||||
<h3 className="text-sm font-semibold text-gray-700 tracking-tight">
|
||||
{props.component.arguments.chartTitle}
|
||||
</h3>
|
||||
)}
|
||||
{props.component.arguments.chartDescription && (
|
||||
<p className="mt-0.5 text-xs text-gray-400">
|
||||
{props.component.arguments.chartDescription}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<MetricCharts
|
||||
metricResults={metricResults}
|
||||
metricTypes={props.metricTypes}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import Icon, { SizeProp } from "Common/UI/Components/Icon/Icon";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: IconProp;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const DashboardTemplateCard: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div
|
||||
className="cursor-pointer border border-gray-200 rounded-lg p-4 hover:border-indigo-500 hover:shadow-md transition-all duration-200 bg-white"
|
||||
onClick={props.onClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
props.onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="flex-shrink-0 mr-3 text-indigo-500">
|
||||
<Icon icon={props.icon} size={SizeProp.Large} />
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-gray-900">{props.title}</h3>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 leading-relaxed">
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardTemplateCard;
|
||||
@@ -8,6 +8,7 @@ export interface ComponentProps {
|
||||
data: MetricAliasData;
|
||||
isFormula: boolean;
|
||||
onDataChanged: (data: MetricAliasData) => void;
|
||||
hideVariableBadge?: boolean | undefined;
|
||||
}
|
||||
|
||||
const MetricAlias: FunctionComponent<ComponentProps> = (
|
||||
@@ -16,25 +17,26 @@ const MetricAlias: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="space-y-3">
|
||||
{/* Variable badge row */}
|
||||
{((!props.isFormula && props.data.metricVariable) ||
|
||||
props.isFormula) && (
|
||||
<div className="flex items-center space-x-2">
|
||||
{!props.isFormula && props.data.metricVariable && (
|
||||
<div className="bg-indigo-500 h-7 w-7 min-w-7 rounded flex items-center justify-center text-xs font-semibold text-white">
|
||||
{props.data.metricVariable}
|
||||
</div>
|
||||
)}
|
||||
{props.isFormula && (
|
||||
<div className="bg-indigo-500 h-7 w-7 min-w-7 rounded flex items-center justify-center text-white">
|
||||
<Icon thick={ThickProp.Thick} icon={IconProp.ChevronRight} />
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xs font-medium text-gray-400 uppercase tracking-wide">
|
||||
Display Settings
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Variable badge row — hidden when parent already shows it */}
|
||||
{!props.hideVariableBadge &&
|
||||
((!props.isFormula && props.data.metricVariable) ||
|
||||
props.isFormula) && (
|
||||
<div className="flex items-center space-x-2">
|
||||
{!props.isFormula && props.data.metricVariable && (
|
||||
<div className="bg-indigo-500 h-7 w-7 min-w-7 rounded flex items-center justify-center text-xs font-semibold text-white">
|
||||
{props.data.metricVariable}
|
||||
</div>
|
||||
)}
|
||||
{props.isFormula && (
|
||||
<div className="bg-indigo-500 h-7 w-7 min-w-7 rounded flex items-center justify-center text-white">
|
||||
<Icon thick={ThickProp.Thick} icon={IconProp.ChevronRight} />
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xs font-medium text-gray-400 uppercase tracking-wide">
|
||||
Display Settings
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title and Description */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import React, { FunctionComponent, ReactElement, useState } from "react";
|
||||
import MetricAlias from "./MetricAlias";
|
||||
import MetricQuery from "./MetricQuery";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import Button, {
|
||||
ButtonSize,
|
||||
ButtonStyleType,
|
||||
} from "Common/UI/Components/Button/Button";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import MetricAliasData from "Common/Types/Metrics/MetricAliasData";
|
||||
import MetricQueryData from "Common/Types/Metrics/MetricQueryData";
|
||||
import { GetReactElementFunction } from "Common/UI/Types/FunctionTypes";
|
||||
import MetricType from "Common/Models/DatabaseModels/MetricType";
|
||||
import Input, { InputType } from "Common/UI/Components/Input/Input";
|
||||
import Icon from "Common/UI/Components/Icon/Icon";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import Dictionary from "Common/Types/Dictionary";
|
||||
|
||||
export interface ComponentProps {
|
||||
data: MetricQueryConfigData;
|
||||
@@ -35,6 +33,10 @@ export interface ComponentProps {
|
||||
const MetricGraphConfig: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(true);
|
||||
const [showDisplaySettings, setShowDisplaySettings] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const defaultAliasData: MetricAliasData = {
|
||||
metricVariable: undefined,
|
||||
title: undefined,
|
||||
@@ -43,139 +45,365 @@ const MetricGraphConfig: FunctionComponent<ComponentProps> = (
|
||||
legendUnit: undefined,
|
||||
};
|
||||
|
||||
const getContent: GetReactElementFunction = (): ReactElement => {
|
||||
// Compute active attribute count for the header summary
|
||||
const attributes: Dictionary<string | number | boolean> | undefined = (
|
||||
props.data?.metricQueryData?.filterData as Record<string, unknown>
|
||||
)?.["attributes"] as Dictionary<string | number | boolean> | undefined;
|
||||
|
||||
const activeAttributeCount: number = attributes
|
||||
? Object.keys(attributes).length
|
||||
: 0;
|
||||
|
||||
const metricName: string =
|
||||
props.data?.metricQueryData?.filterData?.metricName?.toString() ||
|
||||
"No metric selected";
|
||||
|
||||
const aggregationType: string =
|
||||
props.data?.metricQueryData?.filterData?.aggegationType?.toString() ||
|
||||
"Avg";
|
||||
|
||||
// Remove a single attribute filter
|
||||
const handleRemoveAttribute: (key: string) => void = (
|
||||
key: string,
|
||||
): void => {
|
||||
if (!attributes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newAttributes: Dictionary<string | number | boolean> = {
|
||||
...attributes,
|
||||
};
|
||||
delete newAttributes[key];
|
||||
|
||||
const newFilterData: Record<string, unknown> = {
|
||||
...(props.data.metricQueryData.filterData as Record<string, unknown>),
|
||||
};
|
||||
|
||||
if (Object.keys(newAttributes).length > 0) {
|
||||
newFilterData["attributes"] = newAttributes;
|
||||
} else {
|
||||
delete newFilterData["attributes"];
|
||||
}
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
metricQueryData: {
|
||||
...props.data.metricQueryData,
|
||||
filterData: newFilterData as MetricQueryData["filterData"],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Clear all attribute filters
|
||||
const handleClearAllAttributes: () => void = (): void => {
|
||||
const newFilterData: Record<string, unknown> = {
|
||||
...(props.data.metricQueryData.filterData as Record<string, unknown>),
|
||||
};
|
||||
delete newFilterData["attributes"];
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
metricQueryData: {
|
||||
...props.data.metricQueryData,
|
||||
filterData: newFilterData as MetricQueryData["filterData"],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getHeader: () => ReactElement = (): ReactElement => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Metric query selection — always on top */}
|
||||
{props.data?.metricQueryData && (
|
||||
<MetricQuery
|
||||
data={props.data?.metricQueryData || {}}
|
||||
onDataChanged={(data: MetricQueryData) => {
|
||||
props.onBlur?.();
|
||||
props.onFocus?.();
|
||||
if (props.onChange) {
|
||||
const selectedMetricName: string | undefined =
|
||||
data.filterData?.metricName?.toString();
|
||||
const previousMetricName: string | undefined =
|
||||
props.data?.metricQueryData?.filterData?.metricName?.toString();
|
||||
|
||||
// If metric changed, prefill all alias fields from MetricType
|
||||
if (
|
||||
selectedMetricName &&
|
||||
selectedMetricName !== previousMetricName
|
||||
) {
|
||||
const metricType: MetricType | undefined =
|
||||
props.metricTypes.find((m: MetricType) => {
|
||||
return m.name === selectedMetricName;
|
||||
});
|
||||
|
||||
if (metricType) {
|
||||
const currentAlias: MetricAliasData =
|
||||
props.data.metricAliasData || defaultAliasData;
|
||||
|
||||
props.onChange({
|
||||
...props.data,
|
||||
metricQueryData: data,
|
||||
metricAliasData: {
|
||||
...currentAlias,
|
||||
title: metricType.name || "",
|
||||
description: metricType.description || "",
|
||||
legend: metricType.name || "",
|
||||
legendUnit: metricType.unit || "",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
props.onChange({ ...props.data, metricQueryData: data });
|
||||
}
|
||||
}}
|
||||
metricTypes={props.metricTypes}
|
||||
telemetryAttributes={props.telemetryAttributes}
|
||||
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
|
||||
isAttributesLoading={props.attributesLoading}
|
||||
attributesError={props.attributesError}
|
||||
onAttributesRetry={props.onAttributesRetry}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Display settings — title, description, legend, unit */}
|
||||
<div className="border-t border-gray-200 pt-3">
|
||||
<MetricAlias
|
||||
data={props.data?.metricAliasData || defaultAliasData}
|
||||
onDataChanged={(data: MetricAliasData) => {
|
||||
props.onBlur?.();
|
||||
props.onFocus?.();
|
||||
if (props.onChange) {
|
||||
props.onChange({ ...props.data, metricAliasData: data });
|
||||
}
|
||||
}}
|
||||
isFormula={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Thresholds */}
|
||||
<div className="flex space-x-3">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
Warning Threshold
|
||||
</label>
|
||||
<Input
|
||||
value={props.data?.warningThreshold?.toString() || ""}
|
||||
type={InputType.NUMBER}
|
||||
onChange={(value: string) => {
|
||||
props.onBlur?.();
|
||||
props.onFocus?.();
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
warningThreshold: value ? Number(value) : undefined,
|
||||
});
|
||||
}
|
||||
}}
|
||||
placeholder="e.g. 80"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
Critical Threshold
|
||||
</label>
|
||||
<Input
|
||||
value={props.data?.criticalThreshold?.toString() || ""}
|
||||
type={InputType.NUMBER}
|
||||
onChange={(value: string) => {
|
||||
props.onBlur?.();
|
||||
props.onFocus?.();
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
criticalThreshold: value ? Number(value) : undefined,
|
||||
});
|
||||
}
|
||||
}}
|
||||
placeholder="e.g. 95"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
{/* Variable badge */}
|
||||
{props.data?.metricAliasData?.metricVariable && (
|
||||
<div className="bg-indigo-500 h-8 w-8 min-w-8 rounded-lg flex items-center justify-center text-sm font-bold text-white shadow-sm">
|
||||
{props.data.metricAliasData.metricVariable}
|
||||
</div>
|
||||
)}
|
||||
{/* Summary info */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-semibold text-gray-900 truncate">
|
||||
{metricName}
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-md bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">
|
||||
{aggregationType}
|
||||
</span>
|
||||
{activeAttributeCount > 0 && (
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-indigo-50 border border-indigo-200 px-2 py-0.5 text-xs font-medium text-indigo-700">
|
||||
<Icon
|
||||
icon={IconProp.Filter}
|
||||
className="h-3 w-3 text-indigo-500"
|
||||
/>
|
||||
{activeAttributeCount}{" "}
|
||||
{activeAttributeCount === 1 ? "filter" : "filters"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{props.data?.metricAliasData?.title &&
|
||||
props.data.metricAliasData.title !== metricName && (
|
||||
<p className="text-xs text-gray-500 mt-0.5 truncate">
|
||||
{props.data.metricAliasData.title}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remove button */}
|
||||
{props.onRemove && (
|
||||
<div>
|
||||
<Button
|
||||
title={"Remove"}
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-1 ml-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center h-7 w-7 rounded-md text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
||||
onClick={() => {
|
||||
setIsExpanded(!isExpanded);
|
||||
}}
|
||||
title={isExpanded ? "Collapse" : "Expand"}
|
||||
>
|
||||
<Icon
|
||||
icon={isExpanded ? IconProp.ChevronUp : IconProp.ChevronDown}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
{props.onRemove && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center h-7 w-7 rounded-md text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500"
|
||||
onClick={() => {
|
||||
props.onBlur?.();
|
||||
props.onFocus?.();
|
||||
return props.onRemove?.();
|
||||
}}
|
||||
buttonSize={ButtonSize.Small}
|
||||
buttonStyle={ButtonStyleType.DANGER_OUTLINE}
|
||||
/>
|
||||
title="Remove query"
|
||||
>
|
||||
<Icon icon={IconProp.Trash} className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getAttributeChips: () => ReactElement | null = (): ReactElement | null => {
|
||||
if (!attributes || activeAttributeCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-3 pt-3 border-t border-gray-100">
|
||||
<span className="text-xs text-gray-400 font-medium mr-1">
|
||||
Filtered by:
|
||||
</span>
|
||||
{Object.entries(attributes).map(
|
||||
([key, value]: [string, string | number | boolean]) => {
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
className="inline-flex items-center gap-1 rounded-md border border-indigo-200 bg-indigo-50 py-0.5 pl-2 pr-1 text-xs text-indigo-700"
|
||||
>
|
||||
<span className="font-medium text-indigo-500">{key}:</span>
|
||||
<span>{String(value)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded text-indigo-400 transition-colors hover:bg-indigo-100 hover:text-indigo-600"
|
||||
onClick={() => {
|
||||
handleRemoveAttribute(key);
|
||||
}}
|
||||
title={`Remove ${key}: ${String(value)}`}
|
||||
>
|
||||
<Icon icon={IconProp.Close} className="h-2.5 w-2.5" />
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
)}
|
||||
{activeAttributeCount > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded px-1.5 py-0.5 text-[11px] font-medium text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
||||
onClick={handleClearAllAttributes}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getContent: () => ReactElement = (): ReactElement => {
|
||||
return (
|
||||
<div>
|
||||
{/* Header with summary */}
|
||||
{getHeader()}
|
||||
|
||||
{/* Attribute filter chips - always visible */}
|
||||
{!isExpanded && getAttributeChips()}
|
||||
|
||||
{/* Expandable content */}
|
||||
{isExpanded && (
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* Metric query selection */}
|
||||
{props.data?.metricQueryData && (
|
||||
<MetricQuery
|
||||
data={props.data?.metricQueryData || {}}
|
||||
onDataChanged={(data: MetricQueryData) => {
|
||||
props.onBlur?.();
|
||||
props.onFocus?.();
|
||||
if (props.onChange) {
|
||||
const selectedMetricName: string | undefined =
|
||||
data.filterData?.metricName?.toString();
|
||||
const previousMetricName: string | undefined =
|
||||
props.data?.metricQueryData?.filterData?.metricName?.toString();
|
||||
|
||||
// If metric changed, prefill all alias fields from MetricType
|
||||
if (
|
||||
selectedMetricName &&
|
||||
selectedMetricName !== previousMetricName
|
||||
) {
|
||||
const metricType: MetricType | undefined =
|
||||
props.metricTypes.find((m: MetricType) => {
|
||||
return m.name === selectedMetricName;
|
||||
});
|
||||
|
||||
if (metricType) {
|
||||
const currentAlias: MetricAliasData =
|
||||
props.data.metricAliasData || defaultAliasData;
|
||||
|
||||
props.onChange({
|
||||
...props.data,
|
||||
metricQueryData: data,
|
||||
metricAliasData: {
|
||||
...currentAlias,
|
||||
title: metricType.name || "",
|
||||
description: metricType.description || "",
|
||||
legend: metricType.name || "",
|
||||
legendUnit: metricType.unit || "",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
props.onChange({ ...props.data, metricQueryData: data });
|
||||
}
|
||||
}}
|
||||
metricTypes={props.metricTypes}
|
||||
telemetryAttributes={props.telemetryAttributes}
|
||||
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
|
||||
isAttributesLoading={props.attributesLoading}
|
||||
attributesError={props.attributesError}
|
||||
onAttributesRetry={props.onAttributesRetry}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Attribute filter chips */}
|
||||
{getAttributeChips()}
|
||||
|
||||
{/* Display Settings - collapsible */}
|
||||
<div className="border-t border-gray-200 pt-3">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-xs font-medium text-gray-500 uppercase tracking-wide hover:text-gray-700 transition-colors w-full"
|
||||
onClick={() => {
|
||||
setShowDisplaySettings(!showDisplaySettings);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
showDisplaySettings
|
||||
? IconProp.ChevronDown
|
||||
: IconProp.ChevronRight
|
||||
}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
<span>Display Settings</span>
|
||||
{(props.data?.metricAliasData?.title ||
|
||||
props.data?.warningThreshold !== undefined ||
|
||||
props.data?.criticalThreshold !== undefined) && (
|
||||
<span className="inline-flex h-1.5 w-1.5 rounded-full bg-indigo-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{showDisplaySettings && (
|
||||
<div className="mt-3 space-y-4">
|
||||
<MetricAlias
|
||||
data={props.data?.metricAliasData || defaultAliasData}
|
||||
onDataChanged={(data: MetricAliasData) => {
|
||||
props.onBlur?.();
|
||||
props.onFocus?.();
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
metricAliasData: data,
|
||||
});
|
||||
}
|
||||
}}
|
||||
isFormula={false}
|
||||
hideVariableBadge={true}
|
||||
/>
|
||||
|
||||
{/* Thresholds */}
|
||||
<div className="flex space-x-3">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
Warning Threshold
|
||||
</label>
|
||||
<Input
|
||||
value={
|
||||
props.data?.warningThreshold?.toString() || ""
|
||||
}
|
||||
type={InputType.NUMBER}
|
||||
onChange={(value: string) => {
|
||||
props.onBlur?.();
|
||||
props.onFocus?.();
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
warningThreshold: value
|
||||
? Number(value)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}}
|
||||
placeholder="e.g. 80"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
Critical Threshold
|
||||
</label>
|
||||
<Input
|
||||
value={
|
||||
props.data?.criticalThreshold?.toString() || ""
|
||||
}
|
||||
type={InputType.NUMBER}
|
||||
onChange={(value: string) => {
|
||||
props.onBlur?.();
|
||||
props.onFocus?.();
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
criticalThreshold: value
|
||||
? Number(value)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}}
|
||||
placeholder="e.g. 95"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.error && (
|
||||
<p data-testid="error-message" className="mt-1 text-sm text-red-400">
|
||||
<p data-testid="error-message" className="mt-3 text-sm text-red-400">
|
||||
{props.error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -12,13 +12,11 @@ import Button, {
|
||||
ButtonStyleType,
|
||||
} from "Common/UI/Components/Button/Button";
|
||||
import Text from "Common/Types/Text";
|
||||
import HorizontalRule from "Common/UI/Components/HorizontalRule/HorizontalRule";
|
||||
import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType";
|
||||
import StartAndEndDate, {
|
||||
StartAndEndDateType,
|
||||
} from "Common/UI/Components/Date/StartAndEndDate";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import FieldLabelElement from "Common/UI/Components/Forms/Fields/FieldLabel";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import AggregatedResult from "Common/Types/BaseDatabase/AggregatedResult";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
@@ -34,6 +32,7 @@ import MetricCharts from "./MetricCharts";
|
||||
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
|
||||
import JSONFunctions from "Common/Types/JSONFunctions";
|
||||
import MetricType from "Common/Models/DatabaseModels/MetricType";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
|
||||
const getFetchRelevantState: (data: MetricViewData) => unknown = (
|
||||
data: MetricViewData,
|
||||
@@ -109,9 +108,9 @@ const MetricView: FunctionComponent<ComponentProps> = (
|
||||
const [isPageLoading, setIsPageLoading] = useState<boolean>(false);
|
||||
const [pageError, setPageError] = useState<string>("");
|
||||
|
||||
const [telemetryAttributes, setTelemetryAttributes] = useState<Array<string>>(
|
||||
[],
|
||||
);
|
||||
const [telemetryAttributes, setTelemetryAttributes] = useState<
|
||||
Array<string>
|
||||
>([]);
|
||||
const [telemetryAttributesLoaded, setTelemetryAttributesLoaded] =
|
||||
useState<boolean>(false);
|
||||
const [telemetryAttributesLoading, setTelemetryAttributesLoading] =
|
||||
@@ -305,29 +304,33 @@ const MetricView: FunctionComponent<ComponentProps> = (
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
{/* Time range selector */}
|
||||
{!props.hideStartAndEndDate && (
|
||||
<div className="mb-5">
|
||||
<Card>
|
||||
<div className="-mt-5">
|
||||
<FieldLabelElement title="Start and End Time" required={true} />
|
||||
<StartAndEndDate
|
||||
type={StartAndEndDateType.DateTime}
|
||||
value={props.data.startAndEndDate || undefined}
|
||||
onValueChanged={(startAndEndDate: InBetween<Date> | null) => {
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
startAndEndDate: startAndEndDate,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Card>
|
||||
<div className="-mt-5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
||||
Time Range
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<StartAndEndDate
|
||||
type={StartAndEndDateType.DateTime}
|
||||
value={props.data.startAndEndDate || undefined}
|
||||
onValueChanged={(startAndEndDate: InBetween<Date> | null) => {
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
startAndEndDate: startAndEndDate,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Query configs */}
|
||||
{!props.hideQueryElements && (
|
||||
<div className="space-y-3">
|
||||
{props.data.queryConfigs.map(
|
||||
@@ -382,104 +385,91 @@ const MetricView: FunctionComponent<ComponentProps> = (
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!props.hideQueryElements && (
|
||||
<div className="space-y-3">
|
||||
{/* Formula configs and Add buttons */}
|
||||
{!props.hideQueryElements && (
|
||||
<div className="space-y-3">
|
||||
{props.data.formulaConfigs.map(
|
||||
(formulaConfig: MetricFormulaConfigData, index: number) => {
|
||||
return (
|
||||
<MetricGraphConfig
|
||||
key={index}
|
||||
onDataChanged={(data: MetricFormulaConfigData) => {
|
||||
const newGraphConfigs: Array<MetricFormulaConfigData> = [
|
||||
...props.data.formulaConfigs,
|
||||
];
|
||||
newGraphConfigs[index] = data;
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
formulaConfigs: newGraphConfigs,
|
||||
});
|
||||
}
|
||||
}}
|
||||
data={formulaConfig}
|
||||
onRemove={() => {
|
||||
const newGraphConfigs: Array<MetricFormulaConfigData> = [
|
||||
...props.data.formulaConfigs,
|
||||
];
|
||||
newGraphConfigs.splice(index, 1);
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
formulaConfigs: newGraphConfigs,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex -ml-3 mt-8 justify-between w-full">
|
||||
<div>
|
||||
<Button
|
||||
title="Add Metric"
|
||||
buttonSize={ButtonSize.Small}
|
||||
onClick={() => {
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
queryConfigs: [
|
||||
...props.data.queryConfigs,
|
||||
getEmptyQueryConfigData(),
|
||||
],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* <Button
|
||||
title="Add Formula"
|
||||
buttonSize={ButtonSize.Small}
|
||||
onClick={() => {
|
||||
setMetricViewData({
|
||||
...metricViewData,
|
||||
formulaConfigs: [
|
||||
...metricViewData.formulaConfigs,
|
||||
getEmptyFormulaConfigData(),
|
||||
],
|
||||
});
|
||||
}}
|
||||
/> */}
|
||||
{props.data.formulaConfigs.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{props.data.formulaConfigs.map(
|
||||
(formulaConfig: MetricFormulaConfigData, index: number) => {
|
||||
return (
|
||||
<MetricGraphConfig
|
||||
key={index}
|
||||
onDataChanged={(data: MetricFormulaConfigData) => {
|
||||
const newGraphConfigs: Array<MetricFormulaConfigData> =
|
||||
[...props.data.formulaConfigs];
|
||||
newGraphConfigs[index] = data;
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
formulaConfigs: newGraphConfigs,
|
||||
});
|
||||
}
|
||||
}}
|
||||
data={formulaConfig}
|
||||
onRemove={() => {
|
||||
const newGraphConfigs: Array<MetricFormulaConfigData> =
|
||||
[...props.data.formulaConfigs];
|
||||
newGraphConfigs.splice(index, 1);
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
formulaConfigs: newGraphConfigs,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add metric button */}
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
title="Add Metric"
|
||||
buttonSize={ButtonSize.Small}
|
||||
buttonStyle={ButtonStyleType.OUTLINE}
|
||||
icon={IconProp.Add}
|
||||
onClick={() => {
|
||||
if (props.onChange) {
|
||||
props.onChange({
|
||||
...props.data,
|
||||
queryConfigs: [
|
||||
...props.data.queryConfigs,
|
||||
getEmptyQueryConfigData(),
|
||||
],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalRule />
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{isMetricResultsLoading && <ComponentLoader />}
|
||||
{/* Chart results */}
|
||||
{isMetricResultsLoading && <ComponentLoader />}
|
||||
|
||||
{metricResultsError && <ErrorMessage message={metricResultsError} />}
|
||||
{metricResultsError && <ErrorMessage message={metricResultsError} />}
|
||||
|
||||
{!isMetricResultsLoading && !metricResultsError && (
|
||||
<div
|
||||
className={
|
||||
props.hideCardInCharts ? "" : "grid grid-cols-1 gap-4 mt-3"
|
||||
}
|
||||
>
|
||||
{/** charts */}
|
||||
<MetricCharts
|
||||
hideCard={props.hideCardInCharts}
|
||||
metricResults={metricResults}
|
||||
metricTypes={metricTypes}
|
||||
metricViewData={props.data}
|
||||
chartCssClass={props.chartCssClass}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isMetricResultsLoading && !metricResultsError && (
|
||||
<div
|
||||
className={
|
||||
props.hideCardInCharts ? "" : "grid grid-cols-1 gap-4"
|
||||
}
|
||||
>
|
||||
<MetricCharts
|
||||
hideCard={props.hideCardInCharts}
|
||||
metricResults={metricResults}
|
||||
metricTypes={metricTypes}
|
||||
metricViewData={props.data}
|
||||
chartCssClass={props.chartCssClass}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCannotRemoveOneRemainingQueryError ? (
|
||||
<ConfirmModal
|
||||
|
||||
@@ -11,10 +11,35 @@ import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import Label from "Common/Models/DatabaseModels/Label";
|
||||
import Dashboard from "Common/Models/DatabaseModels/Dashboard";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useState,
|
||||
} from "react";
|
||||
import DashboardElement from "../../Components/Dashboard/DashboardElement";
|
||||
import DashboardTemplateCard from "../../Components/Dashboard/DashboardTemplateCard";
|
||||
import {
|
||||
DashboardTemplates,
|
||||
DashboardTemplateType,
|
||||
getTemplateConfig,
|
||||
DashboardTemplate,
|
||||
} from "Common/Types/Dashboard/DashboardTemplates";
|
||||
import DashboardViewConfig from "Common/Types/Dashboard/DashboardViewConfig";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
|
||||
const Dashboards: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const [selectedTemplate, setSelectedTemplate] =
|
||||
useState<DashboardTemplateType | null>(null);
|
||||
const [showCreateForm, setShowCreateForm] = useState<boolean>(false);
|
||||
|
||||
const handleTemplateClick: (type: DashboardTemplateType) => void =
|
||||
useCallback((type: DashboardTemplateType): void => {
|
||||
setSelectedTemplate(type);
|
||||
setShowCreateForm(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={"Dashboards"}
|
||||
@@ -31,6 +56,29 @@ const Dashboards: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Card
|
||||
title="Create from Template"
|
||||
description="Choose a template to quickly get started with a pre-configured dashboard."
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{DashboardTemplates.map(
|
||||
(template: DashboardTemplate): ReactElement => {
|
||||
return (
|
||||
<DashboardTemplateCard
|
||||
key={template.type}
|
||||
title={template.name}
|
||||
description={template.description}
|
||||
icon={template.icon}
|
||||
onClick={() => {
|
||||
handleTemplateClick(template.type);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<ModelTable<Dashboard>
|
||||
modelType={Dashboard}
|
||||
id="dashboard-table"
|
||||
@@ -40,6 +88,7 @@ const Dashboards: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
isCreateable={true}
|
||||
name="Dashboards"
|
||||
isViewable={true}
|
||||
showCreateForm={showCreateForm}
|
||||
cardProps={{
|
||||
title: "Dashboards",
|
||||
description: "Here is a list of dashboards for this project.",
|
||||
@@ -69,6 +118,18 @@ const Dashboards: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
placeholder: "Description",
|
||||
},
|
||||
]}
|
||||
onBeforeCreate={async (item: Dashboard, _miscDataProps: JSONObject): Promise<Dashboard> => {
|
||||
if (selectedTemplate && selectedTemplate !== DashboardTemplateType.Blank) {
|
||||
const templateConfig: DashboardViewConfig | null =
|
||||
getTemplateConfig(selectedTemplate);
|
||||
if (templateConfig) {
|
||||
item.dashboardViewConfig = templateConfig;
|
||||
}
|
||||
}
|
||||
setSelectedTemplate(null);
|
||||
setShowCreateForm(false);
|
||||
return item;
|
||||
}}
|
||||
saveFilterProps={{
|
||||
tableId: "all-dashboards-table",
|
||||
}}
|
||||
|
||||
317
Common/Types/Dashboard/DashboardTemplates.ts
Normal file
317
Common/Types/Dashboard/DashboardTemplates.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import DashboardViewConfig from "./DashboardViewConfig";
|
||||
import { ObjectType } from "../JSON";
|
||||
import DashboardSize from "./DashboardSize";
|
||||
import DashboardComponentType from "./DashboardComponentType";
|
||||
import DashboardChartType from "./Chart/ChartType";
|
||||
import ObjectID from "../ObjectID";
|
||||
import DashboardBaseComponent from "./DashboardComponents/DashboardBaseComponent";
|
||||
import IconProp from "../Icon/IconProp";
|
||||
|
||||
export enum DashboardTemplateType {
|
||||
Blank = "Blank",
|
||||
Monitor = "Monitor",
|
||||
Incident = "Incident",
|
||||
Kubernetes = "Kubernetes",
|
||||
}
|
||||
|
||||
export interface DashboardTemplate {
|
||||
type: DashboardTemplateType;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: IconProp;
|
||||
}
|
||||
|
||||
export const DashboardTemplates: Array<DashboardTemplate> = [
|
||||
{
|
||||
type: DashboardTemplateType.Blank,
|
||||
name: "Blank Dashboard",
|
||||
description: "Start from scratch with an empty dashboard.",
|
||||
icon: IconProp.Add,
|
||||
},
|
||||
{
|
||||
type: DashboardTemplateType.Monitor,
|
||||
name: "Monitor Dashboard",
|
||||
description:
|
||||
"Pre-configured with response time, uptime, and throughput widgets.",
|
||||
icon: IconProp.Activity,
|
||||
},
|
||||
{
|
||||
type: DashboardTemplateType.Incident,
|
||||
name: "Incident Dashboard",
|
||||
description:
|
||||
"Track active incidents, MTTR, MTTA, and view recent logs.",
|
||||
icon: IconProp.Alert,
|
||||
},
|
||||
{
|
||||
type: DashboardTemplateType.Kubernetes,
|
||||
name: "Kubernetes Dashboard",
|
||||
description:
|
||||
"Monitor CPU, memory, pod count, and resource usage over time.",
|
||||
icon: IconProp.Kubernetes,
|
||||
},
|
||||
];
|
||||
|
||||
function createTextComponent(data: {
|
||||
text: string;
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
isBold?: boolean;
|
||||
}): DashboardBaseComponent {
|
||||
return {
|
||||
_type: ObjectType.DashboardComponent,
|
||||
componentType: DashboardComponentType.Text,
|
||||
componentId: ObjectID.generate(),
|
||||
topInDashboardUnits: data.top,
|
||||
leftInDashboardUnits: data.left,
|
||||
widthInDashboardUnits: data.width,
|
||||
heightInDashboardUnits: data.height,
|
||||
minHeightInDashboardUnits: 1,
|
||||
minWidthInDashboardUnits: 3,
|
||||
arguments: {
|
||||
text: data.text,
|
||||
isBold: data.isBold ?? false,
|
||||
isItalic: false,
|
||||
isUnderline: false,
|
||||
isMarkdown: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createValueComponent(data: {
|
||||
title: string;
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
}): DashboardBaseComponent {
|
||||
return {
|
||||
_type: ObjectType.DashboardComponent,
|
||||
componentType: DashboardComponentType.Value,
|
||||
componentId: ObjectID.generate(),
|
||||
topInDashboardUnits: data.top,
|
||||
leftInDashboardUnits: data.left,
|
||||
widthInDashboardUnits: data.width,
|
||||
heightInDashboardUnits: 1,
|
||||
minHeightInDashboardUnits: 1,
|
||||
minWidthInDashboardUnits: 1,
|
||||
arguments: {
|
||||
title: data.title,
|
||||
metricQueryConfig: {
|
||||
metricQueryData: {
|
||||
filterData: {},
|
||||
groupBy: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createChartComponent(data: {
|
||||
title: string;
|
||||
chartType: DashboardChartType;
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}): DashboardBaseComponent {
|
||||
return {
|
||||
_type: ObjectType.DashboardComponent,
|
||||
componentType: DashboardComponentType.Chart,
|
||||
componentId: ObjectID.generate(),
|
||||
topInDashboardUnits: data.top,
|
||||
leftInDashboardUnits: data.left,
|
||||
widthInDashboardUnits: data.width,
|
||||
heightInDashboardUnits: data.height,
|
||||
minHeightInDashboardUnits: 3,
|
||||
minWidthInDashboardUnits: 6,
|
||||
arguments: {
|
||||
chartTitle: data.title,
|
||||
chartType: data.chartType,
|
||||
metricQueryConfig: {
|
||||
metricAliasData: {
|
||||
metricVariable: "a",
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
legend: undefined,
|
||||
legendUnit: undefined,
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {},
|
||||
groupBy: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createLogStreamComponent(data: {
|
||||
title: string;
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}): DashboardBaseComponent {
|
||||
return {
|
||||
_type: ObjectType.DashboardComponent,
|
||||
componentType: DashboardComponentType.LogStream,
|
||||
componentId: ObjectID.generate(),
|
||||
topInDashboardUnits: data.top,
|
||||
leftInDashboardUnits: data.left,
|
||||
widthInDashboardUnits: data.width,
|
||||
heightInDashboardUnits: data.height,
|
||||
minHeightInDashboardUnits: 3,
|
||||
minWidthInDashboardUnits: 6,
|
||||
arguments: {
|
||||
title: data.title,
|
||||
maxRows: 50,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createMonitorDashboardConfig(): DashboardViewConfig {
|
||||
const components: Array<DashboardBaseComponent> = [
|
||||
createTextComponent({
|
||||
text: "Monitor Dashboard",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 12,
|
||||
height: 1,
|
||||
isBold: true,
|
||||
}),
|
||||
createValueComponent({ title: "Response Time", top: 1, left: 0, width: 4 }),
|
||||
createValueComponent({ title: "Uptime %", top: 1, left: 4, width: 4 }),
|
||||
createValueComponent({ title: "Error Rate", top: 1, left: 8, width: 4 }),
|
||||
createChartComponent({
|
||||
title: "Response Time Over Time",
|
||||
chartType: DashboardChartType.Line,
|
||||
top: 2,
|
||||
left: 0,
|
||||
width: 6,
|
||||
height: 3,
|
||||
}),
|
||||
createChartComponent({
|
||||
title: "Request Throughput",
|
||||
chartType: DashboardChartType.Area,
|
||||
top: 2,
|
||||
left: 6,
|
||||
width: 6,
|
||||
height: 3,
|
||||
}),
|
||||
];
|
||||
|
||||
return {
|
||||
_type: ObjectType.DashboardViewConfig,
|
||||
components,
|
||||
heightInDashboardUnits: Math.max(
|
||||
DashboardSize.heightInDashboardUnits,
|
||||
5,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function createIncidentDashboardConfig(): DashboardViewConfig {
|
||||
const components: Array<DashboardBaseComponent> = [
|
||||
createTextComponent({
|
||||
text: "Incident Dashboard",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 12,
|
||||
height: 1,
|
||||
isBold: true,
|
||||
}),
|
||||
createValueComponent({
|
||||
title: "Active Incidents",
|
||||
top: 1,
|
||||
left: 0,
|
||||
width: 4,
|
||||
}),
|
||||
createValueComponent({ title: "MTTR", top: 1, left: 4, width: 4 }),
|
||||
createValueComponent({ title: "MTTA", top: 1, left: 8, width: 4 }),
|
||||
createChartComponent({
|
||||
title: "Incidents Over Time",
|
||||
chartType: DashboardChartType.Line,
|
||||
top: 2,
|
||||
left: 0,
|
||||
width: 6,
|
||||
height: 3,
|
||||
}),
|
||||
createLogStreamComponent({
|
||||
title: "Recent Logs",
|
||||
top: 2,
|
||||
left: 6,
|
||||
width: 6,
|
||||
height: 3,
|
||||
}),
|
||||
];
|
||||
|
||||
return {
|
||||
_type: ObjectType.DashboardViewConfig,
|
||||
components,
|
||||
heightInDashboardUnits: Math.max(
|
||||
DashboardSize.heightInDashboardUnits,
|
||||
5,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function createKubernetesDashboardConfig(): DashboardViewConfig {
|
||||
const components: Array<DashboardBaseComponent> = [
|
||||
createTextComponent({
|
||||
text: "Kubernetes Dashboard",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 12,
|
||||
height: 1,
|
||||
isBold: true,
|
||||
}),
|
||||
createValueComponent({ title: "CPU Usage", top: 1, left: 0, width: 4 }),
|
||||
createValueComponent({
|
||||
title: "Memory Usage",
|
||||
top: 1,
|
||||
left: 4,
|
||||
width: 4,
|
||||
}),
|
||||
createValueComponent({ title: "Pod Count", top: 1, left: 8, width: 4 }),
|
||||
createChartComponent({
|
||||
title: "CPU Usage Over Time",
|
||||
chartType: DashboardChartType.Line,
|
||||
top: 2,
|
||||
left: 0,
|
||||
width: 6,
|
||||
height: 3,
|
||||
}),
|
||||
createChartComponent({
|
||||
title: "Memory Usage Over Time",
|
||||
chartType: DashboardChartType.Area,
|
||||
top: 2,
|
||||
left: 6,
|
||||
width: 6,
|
||||
height: 3,
|
||||
}),
|
||||
];
|
||||
|
||||
return {
|
||||
_type: ObjectType.DashboardViewConfig,
|
||||
components,
|
||||
heightInDashboardUnits: Math.max(
|
||||
DashboardSize.heightInDashboardUnits,
|
||||
5,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function getTemplateConfig(
|
||||
type: DashboardTemplateType,
|
||||
): DashboardViewConfig | null {
|
||||
switch (type) {
|
||||
case DashboardTemplateType.Monitor:
|
||||
return createMonitorDashboardConfig();
|
||||
case DashboardTemplateType.Incident:
|
||||
return createIncidentDashboardConfig();
|
||||
case DashboardTemplateType.Kubernetes:
|
||||
return createKubernetesDashboardConfig();
|
||||
case DashboardTemplateType.Blank:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user