Skip to content

Commit

Permalink
Merge pull request #4 from jackw/jackw/new-features
Browse files Browse the repository at this point in the history
Introduce reverse colors and show legend options
  • Loading branch information
jackw committed May 10, 2024
2 parents 36ad818 + 55a03d0 commit 3685ce2
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 190 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
if: steps.check-for-e2e.outputs.has-e2e == 'true'
run: docker-compose up -d

- name: Wait for grfana server
- name: Wait for grafana server
if: steps.check-for-e2e.outputs.has-e2e == 'true'
uses: nev7n/wait_for_response@v1
with:
Expand Down
223 changes: 109 additions & 114 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"devDependencies": {
"@babel/core": "^7.21.4",
"@grafana/eslint-config": "^6.0.0",
"@grafana/plugin-e2e": "^0.17.1",
"@grafana/plugin-e2e": "^1.0.0",
"@grafana/tsconfig": "^1.2.0-rc1",
"@playwright/test": "^1.41.2",
"@swc/core": "^1.3.90",
Expand Down
30 changes: 18 additions & 12 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Heywesty Traffic light panel plugin
# Heywesty Traffic light panel

[![CI](https://github.com/jackw/heywesty-trafficlight-panel/actions/workflows/ci.yml/badge.svg)](https://github.com/jackw/heywesty-trafficlight-panel/actions/workflows/ci.yml)
[![CI](https://github.com/jackw/heywesty-trafficlight-panel/actions/workflows/ci.yml/badge.svg)](https://github.com/jackw/heywesty-trafficlight-panel/actions/workflows/ci.yml)  ![Latest Version Badge](https://img.shields.io/badge/dynamic/json?logo=grafana&query=$.version&url=https://grafana.com/api/plugins/heywesty-trafficlight-panel&label=Version&prefix=v&color=F47A20)

![Latest Version Badge](https://img.shields.io/badge/dynamic/json?logo=grafana&query=$.version&url=https://grafana.com/api/plugins/heywesty-trafficlight-panel&label=Version&prefix=v&color=F47A20)
A traffic light to help you interpret complex information at a glance.

A traffic light for your data to help you interpret complex information at a glance.
<img width="500px" src="https://raw.githubusercontent.com/jackw/heywesty-trafficlight-panel/main/src/img/screenshots/traffic-lights-grid-layout.png" />

## Requirements

Expand All @@ -13,11 +13,14 @@ Grafana >=9.5.3
## Features

- **Customizable Traffic Light Width:** Set the minimum width for each traffic light.
- **Grid/Single Row Layout:** Ability to layout lights on a grid or in a single row.
- **Reversible light colors:** Reverse the order of traffic light colors.
- **Sorting Options:** Sort the traffic lights based on values in ascending, descending, or series (none) order.
- **Value Display:** Option to show or hide the values associated with each light.
- **Legend Display:** Option to show or hide the legend (query name) associated with each light.
- **Trend Display:** Show or hide the trend color to provide an additional layer of information.
- **Sorting Options:** Sort the traffic lights based on values in ascending, descending, or series (none) order.
- **Traffic Light Style:** Pick a style of traffic light.
- **Orientation Flexibility:** Choose between a vertical or horizontal layout for the traffic lights.
- **Grid/Single Row Layout:** Ability to layout lights on a grid or in a single row.

## Installation

Expand Down Expand Up @@ -47,14 +50,17 @@ The traffic light panel uses the built in Grafana thresholds to assign lights to
Getting started is as simple as adding the panel to your dashboard and tweaking a few settings:

1. **Minimum Light Width:** Decide how big your traffic lights should be for clear visibility. The default setting is 100 pixels.
2. **Show Value:** Choose whether to display the numerical values with each light. True by default.
3. **Show Trend:** Add an extra layer of insight with a trend color. True by default.
4. **Sort Lights:** Organize your traffic lights in the order that makes sense to you:
1. **Single Row View:** By default each light will flow in an auto grid layout. This can be controlled by adjusting the minimum light width. Enable this if you'd prefer to keep all lights on one row.
1. **Reverse Light Color:** Due to Grafana auto sorting thresholds, this option caters for situations where the "base" threshold should be considered "go" and the highest threshold should be considered "stop".
1. **Show Value:** Choose whether to display the numerical values with each light. True by default.
1. **Show Legend:** Choose whether to display the numerical values with each light. True by default.
1. **Show Trend:** Add an extra layer of insight with a trend color. True by default.
1. **Traffic Light Style:** Choose from one of: default, rounded, or side lights.
1. **Sort Lights:** Organize your traffic lights in the order that makes sense to you:
- None: Keep data series order.
- Ascending: Line them up from lowest to highest values.
- Descending: Line them up from highest to lowest values.
5. **Orientation:** Set the lights horizontally or stick to the default vertical layout.
6. **Single Row View:** By default each light will flow in an auto grid layout. This can be controlled by adjusting the minimum light width. Enable this if you'd prefer to keep all lights on one row.
1. **Orientation:** Set the lights horizontally or stick to the default vertical layout.

## Troubleshooting / Help

Expand All @@ -68,4 +74,4 @@ The plugin supports any data source that returns data frame(s) containing one nu

### Thresholds are incorrectly set.

Thresholds need to be set for the plugin to operate. Please see [usage](#usage) section above. Thresholds must contain `base` and three other values. One for each light.
Thresholds need to be set for the plugin to operate. Please see [usage](#usage) section above. Thresholds must contain `base` and two other values. One for each light.
7 changes: 4 additions & 3 deletions src/components/TrafficLightDefault.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { TEST_IDS } from '../constants';

type TrafficLightProps = {
colors: any;
Expand Down Expand Up @@ -34,7 +35,7 @@ export function TrafficLightDefault({
{colors[2]?.active && (
<g>
<path
data-testid="traffic-light-go"
data-testid={TEST_IDS.go}
fill={colors[2].color}
d="M85.714 389.095c0 27.762 22.524 50.286 50.286 50.286s50.286-22.524 50.286-50.286S163.762 338.81 136 338.81s-50.286 22.523-50.286 50.285Z"
style={{
Expand Down Expand Up @@ -62,7 +63,7 @@ export function TrafficLightDefault({
{colors[1]?.active && (
<g>
<path
data-testid="traffic-light-ready"
data-testid={TEST_IDS.ready}
fill={colors[1].color}
d="M85.714 255c0 27.762 22.524 50.286 50.286 50.286s50.286-22.524 50.286-50.286-22.524-50.286-50.286-50.286S85.714 227.238 85.714 255Z"
style={{
Expand Down Expand Up @@ -90,7 +91,7 @@ export function TrafficLightDefault({
{colors[0]?.active && (
<g>
<path
data-testid="traffic-light-stop"
data-testid={TEST_IDS.stop}
fill={colors[0].color}
d="M85.714 120.905c0 27.762 22.524 50.285 50.286 50.285s50.286-22.523 50.286-50.285c0-27.762-22.524-50.286-50.286-50.286s-50.286 22.524-50.286 50.286Z"
style={{
Expand Down
88 changes: 57 additions & 31 deletions src/components/TrafficLightPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React from 'react';
import { GrafanaTheme2, PanelProps } from '@grafana/data';

import { TrafficLightOptions } from 'types';
import React from 'react';
import { DataLinksContextMenu, useTheme2 } from '@grafana/ui';

import { LightsDataResultStatus, useLightsData } from 'hooks/useLightsData';
import { TrafficLightOptions } from 'types';
import { LightsDataResultStatus, LightsDataValues, useLightsData } from 'hooks/useLightsData';
import { calculateRowsAndColumns } from 'utils/utils';
import { TrafficLightRounded } from './TrafficLightRounded';
import { TEST_IDS } from '../constants';
import { ThresholdsAssistant } from './ThresholdsAssistant';
import { TrafficLightDefault } from './TrafficLightDefault';
import { TrafficLightRounded } from './TrafficLightRounded';
import { TrafficLightSideLights } from './TrafficLightSideLights';
import { ThresholdsAssistant } from './ThresholdsAssistant';

interface TrafficLightPanelProps extends PanelProps<TrafficLightOptions> {}

Expand All @@ -28,7 +27,7 @@ export function TrafficLightPanel({
fieldConfig,
timeZone,
}: TrafficLightPanelProps) {
const { minLightWidth, sortLights, showValue, showTrend, singleRow, style } = options;
const { minLightWidth, sortLights, showLegend, showValue, showTrend, singleRow, style, reverseColors } = options;
const theme = useTheme2();
const { values, status, invalidThresholds } = useLightsData({
fieldConfig,
Expand All @@ -37,22 +36,23 @@ export function TrafficLightPanel({
data: data.series,
timeZone,
sortLights,
reverseColors,
});
const Component = TrafficLightsComponentMap[style];
const { rows, cols } = calculateRowsAndColumns(width, minLightWidth, values.length);
const styles = getStyles({ rows, cols, singleRow, minLightWidth, theme });

if (status === LightsDataResultStatus.nodata) {
return (
<div data-testid="feedback-message-container" style={styles.centeredContent}>
<div data-testid={TEST_IDS.feedbackMsgContainer} style={styles.centeredContent}>
<h4>The query returned no data.</h4>
</div>
);
}

if (status === LightsDataResultStatus.unsupported) {
return (
<div data-testid="feedback-message-container" style={styles.centeredContent}>
<div data-testid={TEST_IDS.feedbackMsgContainer} style={styles.centeredContent}>
<h4>This data format is unsupported.</h4>
</div>
);
Expand All @@ -72,7 +72,7 @@ export function TrafficLightPanel({
width,
height,
}}
data-testid="heywesty-traffic-light"
data-testid={TEST_IDS.trafficLight}
>
{/* @ts-ignore TODO: fix conditional styles errors. */}
<div style={styles.containerStyle}>
Expand All @@ -99,33 +99,59 @@ export function TrafficLightPanel({
horizontal={options.horizontal}
/>
)}
{showValue && (
<div
style={{
alignItems: 'center',
backgroundColor: showTrend
? `color-mix(in srgb, ${light.trend.color} 20%, ${theme.colors.background.primary})`
: 'transparent',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
>
<span>{light.title}</span>
<strong>
{light.prefix}
{light.value}
{light.suffix}
</strong>
</div>
)}
<TrafficLightValue
showValue={showValue}
showLegend={showLegend}
showTrend={showTrend}
light={light}
theme={theme}
/>
</div>
))}
</div>
</div>
);
}

interface TrafficLightValueProps {
showValue: boolean;
showLegend: boolean;
showTrend: boolean;
light: LightsDataValues;
theme: GrafanaTheme2;
}

function TrafficLightValue({ showValue, showLegend, showTrend, light, theme }: TrafficLightValueProps) {
if (!showValue && !showLegend && !showTrend) {
return null;
}

return (
<div
data-testid={TEST_IDS.trafficLightValueContainer}
style={{
alignItems: 'center',
backgroundColor: showTrend
? `color-mix(in srgb, ${light.trend.color} 20%, ${theme.colors.background.primary})`
: 'transparent',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: theme.spacing(0.25),
}}
>
{showLegend && <span data-testid={TEST_IDS.trafficLightLegend}>{light.title}</span>}
{showValue && (
<strong data-testid={TEST_IDS.trafficLightValue}>
{light.prefix}
{light.value}
{light.suffix}
</strong>
)}
</div>
);
}

function getStyles({
rows,
cols,
Expand Down
7 changes: 4 additions & 3 deletions src/components/TrafficLightRounded.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { TEST_IDS } from '../constants';

type TrafficLightProps = {
colors: any;
Expand Down Expand Up @@ -34,7 +35,7 @@ export function TrafficLightRounded({
{colors[2]?.active && (
<g>
<path
data-testid="traffic-light-go"
data-testid={TEST_IDS.go}
fill={colors[2].color}
d="M85.714 389.095c0 27.762 22.524 50.286 50.286 50.286s50.286-22.524 50.286-50.286S163.762 338.81 136 338.81s-50.286 22.523-50.286 50.285Z"
style={{
Expand Down Expand Up @@ -62,7 +63,7 @@ export function TrafficLightRounded({
{colors[1]?.active && (
<g>
<path
data-testid="traffic-light-ready"
data-testid={TEST_IDS.ready}
fill={colors[1].color}
d="M85.714 255c0 27.762 22.524 50.286 50.286 50.286s50.286-22.524 50.286-50.286-22.524-50.286-50.286-50.286S85.714 227.238 85.714 255Z"
style={{
Expand Down Expand Up @@ -90,7 +91,7 @@ export function TrafficLightRounded({
{colors[0]?.active && (
<g>
<path
data-testid="traffic-light-stop"
data-testid={TEST_IDS.stop}
fill={colors[0].color}
d="M85.714 120.905c0 27.762 22.524 50.285 50.286 50.285s50.286-22.523 50.286-50.285c0-27.762-22.524-50.286-50.286-50.286s-50.286 22.524-50.286 50.286Z"
style={{
Expand Down
7 changes: 4 additions & 3 deletions src/components/TrafficLightSideLights.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { TEST_IDS } from '../constants';

type TrafficLightProps = {
colors: any;
Expand Down Expand Up @@ -88,7 +89,7 @@ export function TrafficLightSideLights({
{colors[2]?.active && (
<g>
<path
data-testid="traffic-light-go"
data-testid={TEST_IDS.go}
fill={colors[2].color}
d="M85.714 389.095c0 27.762 22.524 50.286 50.286 50.286s50.286-22.524 50.286-50.286S163.762 338.81 136 338.81s-50.286 22.523-50.286 50.285Z"
style={{
Expand Down Expand Up @@ -116,7 +117,7 @@ export function TrafficLightSideLights({
{colors[1]?.active && (
<g>
<path
data-testid="traffic-light-ready"
data-testid={TEST_IDS.ready}
fill={colors[1].color}
d="M85.714 255c0 27.762 22.524 50.286 50.286 50.286s50.286-22.524 50.286-50.286-22.524-50.286-50.286-50.286S85.714 227.238 85.714 255Z"
style={{
Expand Down Expand Up @@ -144,7 +145,7 @@ export function TrafficLightSideLights({
{colors[0]?.active && (
<g>
<path
data-testid="traffic-light-stop"
data-testid={TEST_IDS.stop}
fill={colors[0].color}
d="M85.714 120.905c0 27.762 22.524 50.285 50.286 50.285s50.286-22.523 50.286-50.285c0-27.762-22.524-50.286-50.286-50.286s-50.286 22.524-50.286 50.286Z"
style={{
Expand Down
10 changes: 10 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const TEST_IDS = {
go: 'traffic-light-go',
ready: 'traffic-light-ready',
stop: 'traffic-light-stop',
trafficLight: 'heywesty-traffic-light',
trafficLightValueContainer: 'heywesty-traffic-light-value-container',
trafficLightValue: 'heywesty-traffic-light-value',
trafficLightLegend: 'heywesty-traffic-light-legend',
feedbackMsgContainer: 'heywesty-feedback-msg-container',
};
13 changes: 9 additions & 4 deletions src/hooks/useLightsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ export type LightsDataResult = {
invalidThresholds?: ThresholdsConfig;
};

type UseLightsData = Omit<GetFieldDisplayValuesOptions, 'reduceOptions'> & { sortLights: SortOptions };
type UseLightsData = Omit<GetFieldDisplayValuesOptions, 'reduceOptions'> & {
sortLights: SortOptions;
reverseColors: boolean;
};

export function useLightsData(options: UseLightsData): LightsDataResult {
const { theme, data, fieldConfig, replaceVariables, timeZone, sortLights } = options;
const { theme, data, fieldConfig, replaceVariables, timeZone, sortLights, reverseColors } = options;

return useMemo(() => {
let status = LightsDataResultStatus.nodata;
Expand Down Expand Up @@ -91,7 +94,9 @@ 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.map((threshold, i) => {
const thresholdSteps = displayValue.field.thresholds?.steps ?? [];
const maybeReversedThresholdSteps = reverseColors ? thresholdSteps.slice().reverse() : thresholdSteps;
const colors = maybeReversedThresholdSteps.map((threshold) => {
return {
color: theme.visualization.getColorByName(threshold.color),
active: threshold.value === activeThreshold.value,
Expand Down Expand Up @@ -128,7 +133,7 @@ export function useLightsData(options: UseLightsData): LightsDataResult {
invalidThresholds,
status,
};
}, [theme, data, fieldConfig, replaceVariables, timeZone, sortLights]);
}, [theme, data, fieldConfig, replaceVariables, timeZone, sortLights, reverseColors]);
}

function sortByValue(arr: LightsDataValues[], sortOrder: SortOptions): LightsDataValues[] {
Expand Down
12 changes: 12 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export const plugin = new PanelPlugin<TrafficLightOptions>(TrafficLightPanel)
description: 'Place all lights in a single row',
defaultValue: false,
})
.addBooleanSwitch({
path: 'reverseColors',
name: 'Reverse light colors',
description: 'Reverse the order of the light colors',
defaultValue: false,
})
.addRadio({
path: 'sortLights',
name: 'Sort lights',
Expand All @@ -37,6 +43,12 @@ export const plugin = new PanelPlugin<TrafficLightOptions>(TrafficLightPanel)
description: 'Show or hide the value',
defaultValue: true,
})
.addBooleanSwitch({
path: 'showLegend',
name: 'Show legend',
description: 'Show or hide the legend (query name)',
defaultValue: true,
})
.addBooleanSwitch({
path: 'showTrend',
name: 'Show trend',
Expand Down

0 comments on commit 3685ce2

Please sign in to comment.