diff --git a/provisioning/dashboards/panels.json b/provisioning/dashboards/panels.json index b0e8840..2d3033b 100644 --- a/provisioning/dashboards/panels.json +++ b/provisioning/dashboards/panels.json @@ -42,10 +42,6 @@ "color": "red", "value": null }, - { - "color": "red", - "value": 0 - }, { "color": "#EAB839", "value": 33 @@ -105,17 +101,13 @@ "color": "red", "value": null }, - { - "color": "red", - "value": 0 - }, { "color": "orange", - "value": 33 + "value": 0 }, { "color": "green", - "value": 66 + "value": 33 } ] } @@ -168,10 +160,6 @@ "color": "red", "value": null }, - { - "color": "red", - "value": 0 - }, { "color": "orange", "value": 33 @@ -227,13 +215,9 @@ "thresholds": { "mode": "absolute", "steps": [ - { - "color": "green", - "value": null - }, { "color": "red", - "value": 0 + "value": null }, { "color": "#EAB839", @@ -297,10 +281,6 @@ "color": "red", "value": null }, - { - "color": "red", - "value": 0 - }, { "color": "#EAB839", "value": 50 @@ -361,10 +341,6 @@ "color": "red", "value": null }, - { - "color": "red", - "value": 0 - }, { "color": "#EAB839", "value": 50 @@ -579,7 +555,8 @@ "mode": "percentage", "steps": [ { - "color": "red" + "color": "red", + "value": null }, { "color": "red", @@ -609,12 +586,9 @@ "value": { "mode": "percentage", "steps": [ - { - "color": "red" - }, { "color": "super-light-red", - "value": 0 + "value": null }, { "color": "red", @@ -640,12 +614,9 @@ "value": { "mode": "percentage", "steps": [ - { - "color": "red" - }, { "color": "super-light-orange", - "value": 0 + "value": null }, { "color": "orange", @@ -671,12 +642,9 @@ "value": { "mode": "percentage", "steps": [ - { - "color": "red" - }, { "color": "super-light-yellow", - "value": 0 + "value": null }, { "color": "yellow", @@ -702,12 +670,9 @@ "value": { "mode": "percentage", "steps": [ - { - "color": "red" - }, { "color": "super-light-green", - "value": 0 + "value": null }, { "color": "green", @@ -733,12 +698,9 @@ "value": { "mode": "percentage", "steps": [ - { - "color": "red" - }, { "color": "super-light-blue", - "value": 0 + "value": null }, { "color": "blue", @@ -764,12 +726,9 @@ "value": { "mode": "percentage", "steps": [ - { - "color": "red" - }, { "color": "super-light-purple", - "value": 0 + "value": null }, { "color": "purple", @@ -841,12 +800,9 @@ "thresholds": { "mode": "percentage", "steps": [ - { - "color": "red" - }, { "color": "red", - "value": 0 + "value": null }, { "color": "#EAB839", @@ -931,6 +887,6 @@ "timezone": "", "title": "Traffic Lights Example", "uid": "O4tc_E6Gz", - "version": 8, + "version": 4, "weekStart": "" } diff --git a/src/components/ThresholdsAssistant.tsx b/src/components/ThresholdsAssistant.tsx new file mode 100644 index 0000000..7ba13af --- /dev/null +++ b/src/components/ThresholdsAssistant.tsx @@ -0,0 +1,88 @@ +import { css, cx } from '@emotion/css'; +import { GrafanaTheme2, ThresholdsConfig } from '@grafana/data'; +import { Alert, Icon, useStyles2 } from '@grafana/ui'; +import React from 'react'; + +export function ThresholdsAssistant({ thresholds }: { thresholds?: ThresholdsConfig }) { + const validSteps = validateThresholds(thresholds!); + const styles = useStyles2(getStyles); + return ( + +

Please configure thresholds for all three lights:

+
+
+ {validSteps.map((step, i) => ( + + ))} +
+ + ); +} + +const BASE_LIGHT_GRID_AREA = [2, 1, 3, 2]; +const BASE_LABEL_GRID_AREA = [2, 2, 3, 3]; + +function ThresholdFeedback({ isValid, index }: { isValid: boolean; index: number }) { + const iconName = isValid ? 'check' : 'times'; + const label = isValid ? 'Threshold configured' : 'Threshold not configured'; + const styles = useStyles2(getStyles); + const lightGridArea = BASE_LIGHT_GRID_AREA.map((n, i) => (i % 2 === 0 ? n + index : n)).join(' / '); + const labelGridArea = BASE_LABEL_GRID_AREA.map((n, i) => (i % 2 === 0 ? n + index : n)).join(' / '); + return ( + <> +
+ +
+ + {label} + + + ); +} + +function validateThresholds(thresholds: ThresholdsConfig) { + const { steps } = thresholds; + const validNumberOfSteps = Array(3).fill(null); + const result = validNumberOfSteps.map((_, i) => { + return { + isValid: Boolean(steps[i]), + }; + }); + + return result; +} + +const getStyles = (theme: GrafanaTheme2) => ({ + grid: css({ + display: 'grid', + gridTemplateColumns: '40px 1fr', + gridTemplateRows: '4px repeat(3, 1fr) 4px', + gap: theme.spacing(), + }), + trafficLight: css({ + backgroundColor: theme.colors.background.canvas, + borderRadius: 50, + gridArea: '1 / 1 / 6 / 2', + }), + light: css({ + alignItems: 'center', + backgroundColor: theme.colors.background.secondary, + borderRadius: 100, + display: 'flex', + justifyContent: 'center', + height: 26, + placeSelf: 'center', + width: 26, + }), + label: css({ + display: 'flex', + alignItems: 'center', + fontSize: theme.typography.size.sm, + }), + valid: css({ + backgroundColor: theme.colors.success.main, + }), + invalid: css({ + backgroundColor: theme.colors.error.main, + }), +}); diff --git a/src/components/TrafficLightPanel.tsx b/src/components/TrafficLightPanel.tsx index be24680..cb28551 100644 --- a/src/components/TrafficLightPanel.tsx +++ b/src/components/TrafficLightPanel.tsx @@ -7,6 +7,7 @@ import { DataLinksContextMenu, useTheme2 } from '@grafana/ui'; import { LightsDataResultStatus, useLightsData } from 'hooks/useLightsData'; import { calculateRowsAndColumns } from 'utils'; import { TrafficLight } from './TrafficLight'; +import { ThresholdsAssistant } from './ThresholdsAssistant'; interface TrafficLightPanelProps extends PanelProps {} @@ -23,7 +24,7 @@ export function TrafficLightPanel({ const theme = useTheme2(); const { rows, cols } = calculateRowsAndColumns(width, minLightWidth, data.series.length); const styles = getStyles({ rows, cols, singleRow, minLightWidth, theme }); - const { values, status } = useLightsData({ + const { values, status, invalidThresholds } = useLightsData({ fieldConfig, replaceVariables, theme, @@ -51,7 +52,7 @@ export function TrafficLightPanel({ if (status === LightsDataResultStatus.incorrectThresholds) { return (
-

Thresholds are incorrectly set.

+
); } @@ -63,10 +64,10 @@ export function TrafficLightPanel({ height, }} > - {/* @ts-ignore TODO: fix up styles. */} + {/* @ts-ignore TODO: fix conditional styles errors. */}
{values.map((light) => ( - // @ts-ignore TODO: fix up styles. + // @ts-ignore TODO: fix conditional styles errors.
{light.hasLinks && light.getLinks !== undefined ? ( diff --git a/src/hooks/useLightsData.ts b/src/hooks/useLightsData.ts index 11e2387..65a5310 100644 --- a/src/hooks/useLightsData.ts +++ b/src/hooks/useLightsData.ts @@ -38,6 +38,7 @@ export type LightsDataValues = { export type LightsDataResult = { values: LightsDataValues[]; status: LightsDataResultStatus; + invalidThresholds?: ThresholdsConfig; }; type UseLightsData = Omit & { sortLights: SortOptions }; @@ -47,6 +48,8 @@ export function useLightsData(options: UseLightsData): LightsDataResult { return useMemo(() => { let status = LightsDataResultStatus.nodata; + let invalidThresholds = undefined; + if (noData(data)) { return { values: [ @@ -88,11 +91,10 @@ export function useLightsData(options: UseLightsData): LightsDataResult { const thresholdsValid = validateThresholds(displayValue.field.thresholds); const activeThreshold = getActiveThreshold(displayValue.display.numeric, displayValue.field.thresholds?.steps); const { title, text, suffix, prefix } = displayValue.display; - const colors = displayValue.field.thresholds?.steps.slice(1).map((threshold, i) => { - const isNegative = displayValue.display.numeric < 0 && i === 0; + const colors = displayValue.field.thresholds?.steps.map((threshold, i) => { return { color: theme.visualization.getColorByName(threshold.color), - active: isNegative ? true : threshold.value === activeThreshold.value, + active: threshold.value === activeThreshold.value, }; }); @@ -101,6 +103,7 @@ export function useLightsData(options: UseLightsData): LightsDataResult { if (!thresholdsValid) { status = LightsDataResultStatus.incorrectThresholds; + invalidThresholds = displayValue.field.thresholds; } else { status = LightsDataResultStatus.success; } @@ -119,9 +122,11 @@ export function useLightsData(options: UseLightsData): LightsDataResult { getLinks: displayValue.getLinks, }; }); + return { values: sortLights === SortOptions.None ? values : sortByValue(values, sortLights), - status: status, + invalidThresholds, + status, }; }, [theme, data, fieldConfig, replaceVariables, timeZone, sortLights]); } @@ -167,7 +172,7 @@ function getTrendColor(value: number) { function validateThresholds(thresholds?: ThresholdsConfig) { const numberOfSteps = thresholds?.steps.length; - if (!numberOfSteps || numberOfSteps < 4) { + if (!numberOfSteps || numberOfSteps < 3) { return false; }