diff --git a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts.tsx b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts.tsx index 194487bddb..608b0322fa 100644 --- a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts.tsx @@ -3,8 +3,10 @@ import OneUptimeDate from "Common/Types/Date"; import XAxisType from "Common/UI/Components/Charts/Types/XAxis/XAxisType"; import ChartGroup, { Chart, + ChartMetricInfo, ChartType, } from "Common/UI/Components/Charts/ChartGroup/ChartGroup"; +import Dictionary from "Common/Types/Dictionary"; import AggregatedResult from "Common/Types/BaseDatabase/AggregatedResult"; import { XAxisAggregateType } from "Common/UI/Components/Charts/Types/XAxis/XAxis"; import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType"; @@ -201,6 +203,35 @@ const MetricCharts: FunctionComponent = ( }); } + // Build metric info for the info icon modal + const metricAttributes: Dictionary = {}; + const filterAttributes: + | Dictionary + | undefined = queryConfig.metricQueryData.filterData.attributes as + | Dictionary + | undefined; + + if (filterAttributes) { + for (const key of Object.keys(filterAttributes)) { + metricAttributes[key] = String(filterAttributes[key]); + } + } + + const metricInfo: ChartMetricInfo = { + metricName: + queryConfig.metricQueryData.filterData.metricName?.toString() || "", + aggregationType: + queryConfig.metricQueryData.filterData.aggegationType?.toString() || + "", + attributes: + Object.keys(metricAttributes).length > 0 + ? metricAttributes + : undefined, + groupByAttribute: + queryConfig.metricQueryData.filterData.groupByAttribute, + unit, + }; + const chart: Chart = { id: index.toString(), type: chartType, @@ -209,6 +240,7 @@ const MetricCharts: FunctionComponent = ( queryConfig.metricQueryData.filterData.metricName?.toString() || "", description: queryConfig.metricAliasData?.description || "", + metricInfo, props: { data: chartSeries, xAxis: { diff --git a/Common/UI/Components/Charts/ChartGroup/ChartGroup.tsx b/Common/UI/Components/Charts/ChartGroup/ChartGroup.tsx index 74ab1692fc..970ccf93bb 100644 --- a/Common/UI/Components/Charts/ChartGroup/ChartGroup.tsx +++ b/Common/UI/Components/Charts/ChartGroup/ChartGroup.tsx @@ -1,4 +1,5 @@ import Text from "../../../../Types/Text"; +import Dictionary from "../../../../Types/Dictionary"; import LineChart, { ComponentProps as LineChartProps } from "../Line/LineChart"; import BarChartElement, { ComponentProps as BarChartProps, @@ -6,7 +7,10 @@ import BarChartElement, { import AreaChartElement, { ComponentProps as AreaChartProps, } from "../Area/AreaChart"; -import React, { FunctionComponent, ReactElement } from "react"; +import Icon, { SizeProp } from "../../Icon/Icon"; +import IconProp from "../../../../Types/Icon/IconProp"; +import Modal, { ModalWidth } from "../../Modal/Modal"; +import React, { FunctionComponent, ReactElement, useState } from "react"; export enum ChartType { LINE = "line", @@ -14,12 +18,21 @@ export enum ChartType { AREA = "area", } +export interface ChartMetricInfo { + metricName: string; + aggregationType: string; + attributes?: Dictionary | undefined; + groupByAttribute?: string | undefined; + unit?: string | undefined; +} + export interface Chart { id: string; title: string; description?: string | undefined; type: ChartType; props: LineChartProps | BarChartProps | AreaChartProps; + metricInfo?: ChartMetricInfo | undefined; } export interface ComponentProps { @@ -33,6 +46,8 @@ const ChartGroup: FunctionComponent = ( props: ComponentProps, ): ReactElement => { const syncId: string = Text.generateRandomText(10); + const [metricInfoModalChart, setMetricInfoModalChart] = + useState(null); const isLastChart: (index: number) => boolean = (index: number): boolean => { return index === props.charts.length - 1; @@ -77,33 +92,157 @@ const ChartGroup: FunctionComponent = ( } }; + type GetInfoIconFunction = (chart: Chart) => ReactElement; + + const getInfoIcon: GetInfoIconFunction = (chart: Chart): ReactElement => { + if (!chart.metricInfo) { + return <>; + } + + return ( + + ); + }; + + const renderMetricInfoModal: () => ReactElement = (): ReactElement => { + if (!metricInfoModalChart) { + return <>; + } + + const attributes: Dictionary = + metricInfoModalChart.attributes || {}; + const attributeKeys: Array = Object.keys(attributes); + + return ( + { + setMetricInfoModalChart(null); + }} + onSubmit={() => { + setMetricInfoModalChart(null); + }} + submitButtonText="Close" + modalWidth={ModalWidth.Normal} + > +
+
+ + + + + + + + + + + {metricInfoModalChart.unit && ( + + + + + )} + {metricInfoModalChart.groupByAttribute && ( + + + + + )} + {attributeKeys.length > 0 && ( + + + + + )} + +
+ Metric Name + + {metricInfoModalChart.metricName} +
+ Aggregation + + {metricInfoModalChart.aggregationType} +
+ Unit + + {metricInfoModalChart.unit} +
+ Grouped By + + {metricInfoModalChart.groupByAttribute} +
+ Attributes + +
+ {attributeKeys.map((key: string) => { + return ( +
+ + {key} + + = + + {attributes[key]} + +
+ ); + })} +
+
+
+
+
+ ); + }; + // When hideCard is true, render charts in a clean vertical stack with dividers if (props.hideCard) { return ( -
- {props.charts.map((chart: Chart, index: number) => { - return ( -
-
-
-

- {chart.title} -

- {chart.description && ( -

- {chart.description} -

- )} + <> + {renderMetricInfoModal()} +
+ {props.charts.map((chart: Chart, index: number) => { + return ( +
+
+
+
+

+ {chart.title} +

+ {getInfoIcon(chart)} +
+ {chart.description && ( +

+ {chart.description} +

+ )} +
+ {getChartContent(chart, index)}
- {getChartContent(chart, index)}
-
- ); - })} -
+ ); + })} +
+ ); } @@ -112,35 +251,41 @@ const ChartGroup: FunctionComponent = ( props.charts.length > 1 ? "lg:grid-cols-2" : "lg:grid-cols-1"; return ( -
- {props.charts.map((chart: Chart, index: number) => { - return ( -
-

+ {renderMetricInfoModal()} +
+ {props.charts.map((chart: Chart, index: number) => { + return ( +
- {chart.title} -

- {chart.description && ( -

- {chart.description} -

- )} - {getChartContent(chart, index)} -
- ); - })} -
+
+

+ {chart.title} +

+ {getInfoIcon(chart)} +
+ {chart.description && ( +

+ {chart.description} +

+ )} + {getChartContent(chart, index)} +
+ ); + })} +
+ ); }; diff --git a/Common/UI/Components/Graphs/UptimeBarTooltip.tsx b/Common/UI/Components/Graphs/UptimeBarTooltip.tsx index 93f1455f98..3120115cd0 100644 --- a/Common/UI/Components/Graphs/UptimeBarTooltip.tsx +++ b/Common/UI/Components/Graphs/UptimeBarTooltip.tsx @@ -26,50 +26,76 @@ const UptimeBarTooltip: FunctionComponent = ( const uptimeColor: string = props.uptimePercent >= 99.9 - ? "#22c55e" + ? "#16a34a" : props.uptimePercent >= 99 - ? "#eab308" - : "#ef4444"; + ? "#ca8a04" + : "#dc2626"; + + const uptimeBgColor: string = + props.uptimePercent >= 99.9 + ? "#f0fdf4" + : props.uptimePercent >= 99 + ? "#fefce8" + : "#fef2f2"; + + const uptimeTrackColor: string = + props.uptimePercent >= 99.9 + ? "#dcfce7" + : props.uptimePercent >= 99 + ? "#fef9c3" + : "#fee2e2"; return ( -
- {/* Header */} +
+ {/* Date header */}
- {dateStr} - +
- {/* Uptime bar */} + {/* Uptime card */} {props.hasEvents && ( -
+
- Uptime + + Uptime + {props.uptimePercent.toFixed(2)}% @@ -78,9 +104,9 @@ const UptimeBarTooltip: FunctionComponent = (
@@ -89,8 +115,7 @@ const UptimeBarTooltip: FunctionComponent = ( width: `${Math.min(props.uptimePercent, 100)}%`, height: "100%", backgroundColor: uptimeColor, - borderRadius: "2px", - transition: "width 0.3s ease", + borderRadius: "3px", }} />
@@ -100,19 +125,38 @@ const UptimeBarTooltip: FunctionComponent = ( {!props.hasEvents && (
- No data available for this day +
+ No data available for this day +
)} {/* Status breakdown */} {props.statusDurations.length > 0 && ( -
0 ? "8px" : "0" }}> +
0 ? "10px" : "0", + }} + > +
+ Status Breakdown +
{props.statusDurations.map( (status: StatusDuration, index: number) => { return ( @@ -122,11 +166,15 @@ const UptimeBarTooltip: FunctionComponent = ( display: "flex", alignItems: "center", justifyContent: "space-between", - padding: "3px 0", + padding: "4px 0", }} >
= ( backgroundColor: status.color.toString(), display: "inline-block", flexShrink: 0, + boxShadow: `0 0 0 2px ${status.color.toString()}30`, }} /> - + {status.label}
@@ -165,22 +220,42 @@ const UptimeBarTooltip: FunctionComponent = ( {props.incidents.length > 0 && (
- {props.incidents.length} Incident - {props.incidents.length !== 1 ? "s" : ""} +
+ Incidents +
+
+ {props.incidents.length} +
{props.incidents.slice(0, 3).map( (incident: UptimeBarTooltipIncident) => { @@ -188,19 +263,20 @@ const UptimeBarTooltip: FunctionComponent = (
{incident.title} @@ -217,13 +293,14 @@ const UptimeBarTooltip: FunctionComponent = ( {incident.incidentSeverity.name} @@ -233,15 +310,16 @@ const UptimeBarTooltip: FunctionComponent = ( {incident.currentIncidentState.name} @@ -256,23 +334,27 @@ const UptimeBarTooltip: FunctionComponent = (
- +{props.incidents.length - 3} more + +{props.incidents.length - 3} more incident + {props.incidents.length - 3 !== 1 ? "s" : ""}
)}
- Click bar for full details + Click bar to view details
)} diff --git a/Common/UI/Components/Tooltip/Tooltip.tsx b/Common/UI/Components/Tooltip/Tooltip.tsx index e7d0624b68..b7ac9c9346 100644 --- a/Common/UI/Components/Tooltip/Tooltip.tsx +++ b/Common/UI/Components/Tooltip/Tooltip.tsx @@ -1,6 +1,7 @@ import Tippy from "@tippyjs/react"; import React, { FunctionComponent, ReactElement } from "react"; import "tippy.js/dist/tippy.css"; +import "tippy.js/themes/light-border.css"; export interface ComponentProps { text?: string | undefined; @@ -21,6 +22,9 @@ const Tooltip: FunctionComponent = ( {props.text} ); + const themeProps: { theme: string } | Record = + props.richContent ? { theme: "light-border" } : {}; + return ( = ( trigger="mouseenter focus" hideOnClick={false} maxWidth={props.richContent ? 380 : 350} - delay={[100, 0]} + delay={[80, 0]} duration={[150, 100]} + {...themeProps} aria={{ content: "describedby", expanded: "auto",