TimeSeries: Use exported time shift and fix time comparison tooltip (#109947)
* TimeSeries: Use exported time comparison function * Add alignTimeRangeCompareData to grafana/data * Simplify tooltip time text formatting * Bump scenes version * Add tests for alignTimeRangeCompareData * Add backwards compatibility for older scenes * Update shouldAlignTimeCompare for typical query * Fix tooltip for older versions of scenes * support for multiple shifts --------- Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
co-authored by
Leon Sorokin
parent
76f7836419
commit
23fa9a1484
@@ -1,7 +1,8 @@
|
||||
import { FieldType } from '../types/dataFrame';
|
||||
import { TimeRange } from '../types/time';
|
||||
|
||||
import { createDataFrame, toDataFrame } from './processDataFrame';
|
||||
import { anySeriesWithTimeField, addRow } from './utils';
|
||||
import { anySeriesWithTimeField, addRow, alignTimeRangeCompareData, shouldAlignTimeCompare } from './utils';
|
||||
|
||||
describe('anySeriesWithTimeField', () => {
|
||||
describe('single frame', () => {
|
||||
@@ -104,3 +105,287 @@ describe('addRow', () => {
|
||||
expect(frame.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alignTimeRangeCompareData', () => {
|
||||
const ONE_DAY_MS = 24 * 60 * 60 * 1000; // 86400000ms
|
||||
const ONE_WEEK_MS = 7 * ONE_DAY_MS; // 604800000ms
|
||||
|
||||
it('should align time field values with positive diff (1 day)', () => {
|
||||
const frame = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000] },
|
||||
{ name: 'value', type: FieldType.number, values: [10, 20, 30] },
|
||||
],
|
||||
});
|
||||
|
||||
alignTimeRangeCompareData(frame, ONE_DAY_MS);
|
||||
|
||||
expect(frame.fields[0].values).toEqual([ONE_DAY_MS + 1000, ONE_DAY_MS + 2000, ONE_DAY_MS + 3000]);
|
||||
expect(frame.fields[1].values).toEqual([10, 20, 30]); // non-time fields unchanged
|
||||
});
|
||||
|
||||
it('should align time field values with negative diff (1 week)', () => {
|
||||
const frame = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000, 3000] },
|
||||
{ name: 'value', type: FieldType.number, values: [10, 20, 30] },
|
||||
],
|
||||
});
|
||||
|
||||
alignTimeRangeCompareData(frame, -ONE_WEEK_MS);
|
||||
|
||||
// When diff is negative, function does v - diff, so v - (-ONE_WEEK_MS) = v + ONE_WEEK_MS
|
||||
expect(frame.fields[0].values).toEqual([ONE_WEEK_MS + 1000, ONE_WEEK_MS + 2000, ONE_WEEK_MS + 3000]);
|
||||
});
|
||||
|
||||
it('should apply default gray color and timeCompare config', () => {
|
||||
const frame = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000] },
|
||||
{ name: 'value', type: FieldType.number, values: [10, 20] },
|
||||
],
|
||||
});
|
||||
|
||||
alignTimeRangeCompareData(frame, ONE_DAY_MS);
|
||||
|
||||
frame.fields.forEach((field) => {
|
||||
expect(field.config.color?.fixedColor).toBe('gray');
|
||||
expect(field.config.custom?.timeCompare).toEqual({
|
||||
diffMs: ONE_DAY_MS,
|
||||
isTimeShiftQuery: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply custom color when provided', () => {
|
||||
const frame = toDataFrame({
|
||||
fields: [{ name: 'value', type: FieldType.number, values: [10, 20] }],
|
||||
});
|
||||
|
||||
alignTimeRangeCompareData(frame, ONE_DAY_MS, 'red');
|
||||
|
||||
expect(frame.fields[0].config.color?.fixedColor).toBe('red');
|
||||
});
|
||||
|
||||
it('should preserve existing config when merging', () => {
|
||||
const frame = toDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'value',
|
||||
type: FieldType.number,
|
||||
values: [10, 20],
|
||||
config: {
|
||||
displayName: 'My Display Name',
|
||||
custom: { existingProperty: 'existingValue' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
alignTimeRangeCompareData(frame, ONE_WEEK_MS);
|
||||
|
||||
expect(frame.fields[0].config.displayName).toBe('My Display Name');
|
||||
expect(frame.fields[0].config.custom?.existingProperty).toBe('existingValue');
|
||||
expect(frame.fields[0].config.custom?.timeCompare?.diffMs).toBe(ONE_WEEK_MS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldAlignTimeCompare', () => {
|
||||
const TIME_VALUES_A = [1000, 2000, 3000];
|
||||
const TIME_VALUES_B = [5000, 6000, 7000];
|
||||
const ORIGINAL_VALUES = [10, 20, 30];
|
||||
const COMPARE_VALUES = [15, 25, 35];
|
||||
|
||||
const mockTimeRange: TimeRange = {
|
||||
from: { valueOf: () => 4000 },
|
||||
to: { valueOf: () => 8000 },
|
||||
raw: { from: 'now-1h', to: 'now' },
|
||||
} as TimeRange;
|
||||
|
||||
it('should return true when compare first time is before time range', () => {
|
||||
const originalFrame = toDataFrame({
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: TIME_VALUES_A },
|
||||
{ name: 'value', type: FieldType.number, values: ORIGINAL_VALUES },
|
||||
],
|
||||
});
|
||||
|
||||
const compareFrame = toDataFrame({
|
||||
refId: 'A-compare',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: TIME_VALUES_A },
|
||||
{ name: 'value', type: FieldType.number, values: COMPARE_VALUES },
|
||||
],
|
||||
meta: {
|
||||
timeCompare: {
|
||||
isTimeShiftQuery: true,
|
||||
diffMs: 86400000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const allFrames = [originalFrame, compareFrame];
|
||||
expect(shouldAlignTimeCompare(compareFrame, allFrames, mockTimeRange)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when compare first time is after time range', () => {
|
||||
const originalFrame = toDataFrame({
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: TIME_VALUES_A },
|
||||
{ name: 'value', type: FieldType.number, values: ORIGINAL_VALUES },
|
||||
],
|
||||
});
|
||||
|
||||
const compareFrame = toDataFrame({
|
||||
refId: 'A-compare',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: TIME_VALUES_B },
|
||||
{ name: 'value', type: FieldType.number, values: COMPARE_VALUES },
|
||||
],
|
||||
meta: {
|
||||
timeCompare: {
|
||||
isTimeShiftQuery: true,
|
||||
diffMs: 86400000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const allFrames = [originalFrame, compareFrame];
|
||||
expect(shouldAlignTimeCompare(compareFrame, allFrames, mockTimeRange)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when compare frame refId does not end with -compare', () => {
|
||||
const compareFrame = toDataFrame({
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: TIME_VALUES_A },
|
||||
{ name: 'value', type: FieldType.number, values: ORIGINAL_VALUES },
|
||||
],
|
||||
});
|
||||
|
||||
const allFrames = [compareFrame];
|
||||
expect(shouldAlignTimeCompare(compareFrame, allFrames, mockTimeRange)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when original frame is not found', () => {
|
||||
const compareFrame = toDataFrame({
|
||||
refId: 'A-compare',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: TIME_VALUES_A },
|
||||
{ name: 'value', type: FieldType.number, values: ORIGINAL_VALUES },
|
||||
],
|
||||
});
|
||||
|
||||
const allFrames = [compareFrame]; // No original frame with refId 'A'
|
||||
expect(shouldAlignTimeCompare(compareFrame, allFrames, mockTimeRange)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when compare frame has no time field', () => {
|
||||
const originalFrame = toDataFrame({
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: TIME_VALUES_A },
|
||||
{ name: 'value', type: FieldType.number, values: ORIGINAL_VALUES },
|
||||
],
|
||||
});
|
||||
|
||||
const compareFrame = toDataFrame({
|
||||
refId: 'A-compare',
|
||||
fields: [{ name: 'value', type: FieldType.number, values: COMPARE_VALUES }],
|
||||
});
|
||||
|
||||
const allFrames = [originalFrame, compareFrame];
|
||||
expect(shouldAlignTimeCompare(compareFrame, allFrames, mockTimeRange)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when original frame has no time field', () => {
|
||||
const originalFrame = toDataFrame({
|
||||
refId: 'A',
|
||||
fields: [{ name: 'value', type: FieldType.number, values: ORIGINAL_VALUES }],
|
||||
});
|
||||
|
||||
const compareFrame = toDataFrame({
|
||||
refId: 'A-compare',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: TIME_VALUES_A },
|
||||
{ name: 'value', type: FieldType.number, values: COMPARE_VALUES },
|
||||
],
|
||||
});
|
||||
|
||||
const allFrames = [originalFrame, compareFrame];
|
||||
expect(shouldAlignTimeCompare(compareFrame, allFrames, mockTimeRange)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when time fields have empty values', () => {
|
||||
const EMPTY_VALUES: number[] = [];
|
||||
|
||||
const originalFrame = toDataFrame({
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: EMPTY_VALUES },
|
||||
{ name: 'value', type: FieldType.number, values: EMPTY_VALUES },
|
||||
],
|
||||
});
|
||||
|
||||
const compareFrame = toDataFrame({
|
||||
refId: 'A-compare',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: EMPTY_VALUES },
|
||||
{ name: 'value', type: FieldType.number, values: EMPTY_VALUES },
|
||||
],
|
||||
});
|
||||
|
||||
const allFrames = [originalFrame, compareFrame];
|
||||
expect(shouldAlignTimeCompare(compareFrame, allFrames, mockTimeRange)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null values and return true when first non-null time is before range', () => {
|
||||
const TIME_WITH_NULLS = [null, ...TIME_VALUES_A];
|
||||
const ORIGINAL_WITH_NULLS = [null, ...ORIGINAL_VALUES];
|
||||
const COMPARE_WITH_NULLS = [null, ...COMPARE_VALUES];
|
||||
|
||||
const originalFrame = toDataFrame({
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: TIME_WITH_NULLS },
|
||||
{ name: 'value', type: FieldType.number, values: ORIGINAL_WITH_NULLS },
|
||||
],
|
||||
});
|
||||
|
||||
const compareFrame = toDataFrame({
|
||||
refId: 'A-compare',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: TIME_WITH_NULLS },
|
||||
{ name: 'value', type: FieldType.number, values: COMPARE_WITH_NULLS },
|
||||
],
|
||||
});
|
||||
|
||||
const allFrames = [originalFrame, compareFrame];
|
||||
expect(shouldAlignTimeCompare(compareFrame, allFrames, mockTimeRange)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when all time values are null', () => {
|
||||
const ALL_NULL_TIMES = [null, null, null];
|
||||
|
||||
const originalFrame = toDataFrame({
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: ALL_NULL_TIMES },
|
||||
{ name: 'value', type: FieldType.number, values: ORIGINAL_VALUES },
|
||||
],
|
||||
});
|
||||
|
||||
const compareFrame = toDataFrame({
|
||||
refId: 'A-compare',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: ALL_NULL_TIMES },
|
||||
{ name: 'value', type: FieldType.number, values: COMPARE_VALUES },
|
||||
],
|
||||
});
|
||||
|
||||
const allFrames = [originalFrame, compareFrame];
|
||||
expect(shouldAlignTimeCompare(compareFrame, allFrames, mockTimeRange)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user