Skip to content

Commit

Permalink
Added Query feature (#179)
Browse files Browse the repository at this point in the history
Adds a feature to create counts down and up from query results.
  • Loading branch information
Humbarrt authored May 21, 2024
1 parent e342be1 commit ac286cb
Show file tree
Hide file tree
Showing 10 changed files with 1,867 additions and 529 deletions.
1,328 changes: 893 additions & 435 deletions provisioning/dashboards/dashboard.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions provisioning/datasources/default.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apiVersion: 1

datasources:
- name: ClockTestData
type: testdata
405 changes: 401 additions & 4 deletions src/ClockPanel.test.tsx

Large diffs are not rendered by default.

27 changes: 19 additions & 8 deletions src/ClockPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { PanelProps } from '@grafana/data';
import { useTheme2 } from '@grafana/ui';
import React, { useEffect, useMemo, useState } from 'react';
import { ClockOptions, ClockRefresh } from './types';
import { ClockOptions, ClockRefresh, DescriptionSource } from './types';

// eslint-disable-next-line
import { RenderDate } from 'components/RenderDate';
Expand All @@ -11,11 +11,13 @@ import { RenderZone } from 'components/RenderZone';
import { Moment } from 'moment-timezone';
import { getMoment } from 'utils';
import './external/moment-duration-format';
import { CalculateClockOptions } from 'components/CalculateClockOptions';
import { RenderDescription } from 'components/RenderDescription';

interface Props extends PanelProps<ClockOptions> {}

export function ClockPanel(props: Props) {
const { options, width, height } = props;
const { options, width, height, data } = props;
const theme = useTheme2();
const { timezone: optionsTimezone, dateSettings, timezoneSettings } = options;
// notice the uppercase Z.
Expand Down Expand Up @@ -53,6 +55,17 @@ export function ClockPanel(props: Props) {
return;
}, [props.options.refresh, timezoneToUse]);

//refresh the time
let [targetTime, descriptionText, err]: [Moment, string, string | null] = useMemo(() => {
return CalculateClockOptions({
options: props.options,
timezone: timezoneToUse,
data,
replaceVariables: props.replaceVariables,
now,
});
}, [props.options, timezoneToUse, data, props.replaceVariables, now]);

return (
<div
className={className}
Expand All @@ -62,13 +75,11 @@ export function ClockPanel(props: Props) {
}}
>
{dateSettings.showDate ? <RenderDate now={now} options={props.options} /> : null}
<RenderTime
now={now}
replaceVariables={props.replaceVariables}
options={props.options}
timezone={timezoneToUse}
/>
<RenderTime options={props.options} targetTime={targetTime} err={err} now={now} />
{timezoneSettings.showTimezone ? <RenderZone now={now} options={props.options} timezone={timezoneToUse} /> : null}
{props.options.descriptionSettings.source !== DescriptionSource.none ? (
<RenderDescription options={props.options} descriptionText={descriptionText} />
) : null}
</div>
);
}
174 changes: 174 additions & 0 deletions src/components/CalculateClockOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { DataFrame, Field, PanelData, PanelProps } from '@grafana/data';
import moment, { Moment, MomentInput } from 'moment-timezone';
import {
ClockMode,
ClockOptions,
ClockSource,
CountdownQueryCalculation,
CountupQueryCalculation,
DescriptionSource,
QueryCalculation,
} from 'types';
import { getMoment } from 'utils';

type QueryRow = {
time: Moment;
description: string;
};

export function CalculateClockOptions({
options,
timezone,
data,
replaceVariables,
now,
}: {
options: ClockOptions;
timezone: ClockOptions['timezone'];
data: PanelData;
replaceVariables: PanelProps['replaceVariables'];
now: Moment;
}): [Moment, string, string | null] {
let descriptionNoValueText = options.descriptionSettings.noValueText ?? '';
let descriptionText =
options.descriptionSettings.source === DescriptionSource.query
? descriptionNoValueText
: options.descriptionSettings.descriptionText;
if (options.mode !== ClockMode.countdown && options.mode !== ClockMode.countup) {
return [now, descriptionText, null];
}

let userInputTime: string | undefined = '';
let clockSettings: ClockOptions['countdownSettings'] | ClockOptions['countupSettings'];
switch (options.mode) {
case ClockMode.countdown:
userInputTime = options.countdownSettings.endCountdownTime || '';
clockSettings = options.countdownSettings;
break;
case ClockMode.countup:
userInputTime = options.countupSettings.beginCountupTime || '';
clockSettings = options.countupSettings;
break;
}

let clockNoValueText = clockSettings.noValueText ?? '';
let clockInvalidValueText = clockSettings.invalidValueText ?? '';
let targetTime: Moment | undefined | null = undefined;
switch (clockSettings.source) {
case ClockSource.input:
if (!userInputTime) {
return [now, descriptionText, clockNoValueText];
}
targetTime = moment(replaceVariables(userInputTime)).utcOffset(getMoment(timezone).format('Z'), true);
break;
case ClockSource.query:
const isDataReady = data.state === 'Done' && data.series.length !== 0;
const isQueryFieldSelected = clockSettings.queryField && clockSettings.queryField.length !== 0;
if (!isDataReady || !isQueryFieldSelected) {
return [now, descriptionText, clockNoValueText];
}

let timeField: Field | undefined = data.series.reduce((foundField: Field | undefined, series: DataFrame) => {
if (foundField) {
return foundField;
}
return series.fields.find((field: Field) => field.name === clockSettings.queryField) ?? undefined;
}, undefined);
let descriptionField: Field | undefined = data.series.reduce(
(foundField: Field | undefined, series: DataFrame) => {
if (foundField) {
return foundField;
}
return (
series.fields.find((field: Field) => field.name === options.descriptionSettings.queryField) ?? undefined
);
},
undefined
);

if (!timeField || !timeField.values || timeField.values.length === 0) {
return [now, descriptionText, clockNoValueText];
}

let descriptionFieldValues = descriptionField?.values
? [...descriptionField?.values]
: new Array(timeField.values.length);
let fieldValues: Array<{ time: MomentInput; description: string }> = timeField.values.map((value, index) => {
return { time: value, description: descriptionFieldValues[index] };
});

let values: QueryRow[] = fieldValues.reduce((acc: QueryRow[], row) => {
if (row.time !== null && row.time !== undefined && !Number.isNaN(row.time)) {
acc.push({ time: moment(row.time), description: row.description });
}
return acc;
}, []);

let sortedValues = (v: QueryRow[]): QueryRow[] => {
return v.sort((a, b) => a.time.diff(b.time));
};

let finalValue:
| {
time: moment.Moment | null;
description: string;
}
| undefined = undefined;
switch (clockSettings.queryCalculation) {
case QueryCalculation.lastNotNull:
finalValue = values.length > 0 ? values.at(-1) : undefined;
break;
case QueryCalculation.last:
finalValue =
fieldValues.length > 0
? {
...fieldValues[fieldValues.length - 1],
time: fieldValues[fieldValues.length - 1]?.time
? moment(fieldValues[fieldValues.length - 1]?.time)
: null,
}
: undefined;
break;
case QueryCalculation.firstNotNull:
finalValue = values.length > 0 ? values[0] : undefined;
break;
case QueryCalculation.first:
finalValue =
fieldValues.length > 0
? { ...fieldValues[0], time: fieldValues[0].time ? moment(fieldValues[0].time) : null }
: undefined;
break;
case QueryCalculation.min:
finalValue = values.length > 0 ? sortedValues(values)[0] : undefined;
break;
case CountdownQueryCalculation.minFuture:
values = values.filter((v: QueryRow) => v.time.isAfter(now));
finalValue = values.length > 0 ? sortedValues(values)[0] : undefined;
break;
case QueryCalculation.max:
finalValue = values.length > 0 ? sortedValues(values).at(-1) : undefined;
break;
case CountupQueryCalculation.maxPast:
values = values.filter((v: QueryRow) => v.time.isBefore(now));
finalValue = values.length > 0 ? sortedValues(values).at(-1) : undefined;
break;
default:
console.error('Invalid query calculation', clockSettings.queryCalculation);
return [now, descriptionText, clockNoValueText];
}

targetTime = finalValue?.time;
if (options.descriptionSettings.source === DescriptionSource.query) {
descriptionText = finalValue?.description ?? descriptionNoValueText;
}
break;
}

if (targetTime === undefined) {
return [now, descriptionText, clockNoValueText];
}
if (targetTime === null || !targetTime.isValid()) {
return [now, descriptionText, clockInvalidValueText];
}
return [targetTime, descriptionText, null];
}
22 changes: 22 additions & 0 deletions src/components/RenderDescription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import { ClockOptions } from 'types';

export function RenderDescription({ options, descriptionText }: { options: ClockOptions; descriptionText: string }) {
const { descriptionSettings } = options;

const className = useMemo(() => {
return css`
font-size: ${descriptionSettings.fontSize};
font-weight: ${descriptionSettings.fontWeight};
font-family: ${options.fontMono ? 'monospace' : ''};
margin: 0;
`;
}, [descriptionSettings.fontSize, descriptionSettings.fontWeight, options.fontMono]);

return (
<span>
<h3 className={className}>{descriptionText}</h3>
</span>
);
}
Loading

0 comments on commit ac286cb

Please sign in to comment.