mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore Metrics: Update history breadcrumb tooltips (#90825)
* add history handler * move them into functions * handle adding new history steps * handle time history by respecting the timezone * remove commented code * no type casting * add unit tests * add colons and a new type metric_page * remove console * fix unit tests
This commit is contained in:
parent
2fe506d502
commit
8dd6bfef3c
@ -52,8 +52,8 @@ describe('DataTrail', () => {
|
|||||||
expect(trail.state.topScene).toBeInstanceOf(MetricSelectScene);
|
expect(trail.state.topScene).toBeInstanceOf(MetricSelectScene);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history current step to 0', () => {
|
it('Should set history current step to 1', () => {
|
||||||
expect(trail.state.history.state.currentStep).toBe(0);
|
expect(trail.state.history.state.currentStep).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history step 0 parentIndex to -1', () => {
|
it('Should set history step 0 parentIndex to -1', () => {
|
||||||
@ -75,11 +75,11 @@ describe('DataTrail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add history step', () => {
|
it('should add history step', () => {
|
||||||
expect(trail.state.history.state.steps[1].type).toBe('metric');
|
expect(trail.state.history.state.steps[1].type).toBe('metric_page');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history currentStep to 1', () => {
|
it('Should set history currentStep to 2', () => {
|
||||||
expect(trail.state.history.state.currentStep).toBe(1);
|
expect(trail.state.history.state.currentStep).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history step 1 parentIndex to 0', () => {
|
it('Should set history step 1 parentIndex to 0', () => {
|
||||||
@ -105,11 +105,11 @@ describe('DataTrail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add history step', () => {
|
it('should add history step', () => {
|
||||||
expect(trail.state.history.state.steps[2].type).toBe('time');
|
expect(trail.state.history.state.steps[3].type).toBe('time');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history currentStep to 2', () => {
|
it('Should set history currentStep to 3', () => {
|
||||||
expect(trail.state.history.state.currentStep).toBe(2);
|
expect(trail.state.history.state.currentStep).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history step 2 parentIndex to 1', () => {
|
it('Should set history step 2 parentIndex to 1', () => {
|
||||||
@ -125,7 +125,7 @@ describe('DataTrail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Current history step should have new `from` of "now-1h"', () => {
|
it('Current history step should have new `from` of "now-1h"', () => {
|
||||||
expect(trail.state.history.state.steps[2].trailState.$timeRange?.state.from).toBe('now-1h');
|
expect(trail.state.history.state.steps[3].trailState.$timeRange?.state.from).toBe('now-1h');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('And when traversing back to step 1', () => {
|
describe('And when traversing back to step 1', () => {
|
||||||
@ -154,12 +154,12 @@ describe('DataTrail', () => {
|
|||||||
expect(trail.state.history.state.steps[3].type).toBe('time');
|
expect(trail.state.history.state.steps[3].type).toBe('time');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history currentStep to 3', () => {
|
it('Should set history currentStep to 4', () => {
|
||||||
expect(trail.state.history.state.currentStep).toBe(3);
|
expect(trail.state.history.state.currentStep).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history step 3 parentIndex to 1', () => {
|
it('Should set history step 4 parentIndex to 1', () => {
|
||||||
expect(trail.state.history.state.steps[3].parentIndex).toBe(1);
|
expect(trail.state.history.state.steps[4].parentIndex).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should have time range `from` be updated "now-15m"', () => {
|
it('Should have time range `from` be updated "now-15m"', () => {
|
||||||
@ -171,7 +171,7 @@ describe('DataTrail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('History step 2 should still have `from` of "now-1h"', () => {
|
it('History step 2 should still have `from` of "now-1h"', () => {
|
||||||
expect(trail.state.history.state.steps[2].trailState.$timeRange?.state.from).toBe('now-1h');
|
expect(trail.state.history.state.steps[3].trailState.$timeRange?.state.from).toBe('now-1h');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('And then when returning again to step 1', () => {
|
describe('And then when returning again to step 1', () => {
|
||||||
@ -191,12 +191,12 @@ describe('DataTrail', () => {
|
|||||||
expect(trail.state.history.state.steps[1].trailState.$timeRange?.state.from).toBe('now-6h');
|
expect(trail.state.history.state.steps[1].trailState.$timeRange?.state.from).toBe('now-6h');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('History step 2 should still have `from` of "now-1h"', () => {
|
it('History step 3 should still have `from` of "now-1h"', () => {
|
||||||
expect(trail.state.history.state.steps[2].trailState.$timeRange?.state.from).toBe('now-1h');
|
expect(trail.state.history.state.steps[3].trailState.$timeRange?.state.from).toBe('now-1h');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('History step 3 should still have `from` of "now-15m"', () => {
|
it('History step 4 should still have `from` of "now-15m"', () => {
|
||||||
expect(trail.state.history.state.steps[3].trailState.$timeRange?.state.from).toBe('now-15m');
|
expect(trail.state.history.state.steps[4].trailState.$timeRange?.state.from).toBe('now-15m');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should have time range `from` be set back to "now-6h"', () => {
|
it('Should have time range `from` be set back to "now-6h"', () => {
|
||||||
@ -217,11 +217,11 @@ describe('DataTrail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add history step', () => {
|
it('should add history step', () => {
|
||||||
expect(trail.state.history.state.steps[2].type).toBe('filters');
|
expect(trail.state.history.state.steps[3].type).toBe('filters');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history currentStep to 2', () => {
|
it('Should set history currentStep to 3', () => {
|
||||||
expect(trail.state.history.state.currentStep).toBe(2);
|
expect(trail.state.history.state.currentStep).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history step 2 parentIndex to 1', () => {
|
it('Should set history step 2 parentIndex to 1', () => {
|
||||||
@ -238,8 +238,8 @@ describe('DataTrail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Current history step should have new filter zone=a', () => {
|
it('Current history step should have new filter zone=a', () => {
|
||||||
expect(getStepFilterVar(2).state.filters[0].key).toBe('zone');
|
expect(getStepFilterVar(3).state.filters[0].key).toBe('zone');
|
||||||
expect(getStepFilterVar(2).state.filters[0].value).toBe('a');
|
expect(getStepFilterVar(3).state.filters[0].value).toBe('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('And when traversing back to step 1', () => {
|
describe('And when traversing back to step 1', () => {
|
||||||
@ -268,12 +268,12 @@ describe('DataTrail', () => {
|
|||||||
expect(trail.state.history.state.steps[3].type).toBe('filters');
|
expect(trail.state.history.state.steps[3].type).toBe('filters');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history currentStep to 3', () => {
|
it('Should set history currentStep to 4', () => {
|
||||||
expect(trail.state.history.state.currentStep).toBe(3);
|
expect(trail.state.history.state.currentStep).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history step 3 parentIndex to 1', () => {
|
it('Should set history step 4 parentIndex to 1', () => {
|
||||||
expect(trail.state.history.state.steps[3].parentIndex).toBe(1);
|
expect(trail.state.history.state.steps[4].parentIndex).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should have filter be updated to "zone=b"', () => {
|
it('Should have filter be updated to "zone=b"', () => {
|
||||||
@ -285,14 +285,14 @@ describe('DataTrail', () => {
|
|||||||
expect(getStepFilterVar(1).state.filters.length).toBe(0);
|
expect(getStepFilterVar(1).state.filters.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('History step 2 should still have old filter zone=a', () => {
|
it('History step 3 should still have old filter zone=a', () => {
|
||||||
expect(getStepFilterVar(2).state.filters[0].key).toBe('zone');
|
expect(getStepFilterVar(3).state.filters[0].key).toBe('zone');
|
||||||
expect(getStepFilterVar(2).state.filters[0].value).toBe('a');
|
expect(getStepFilterVar(3).state.filters[0].value).toBe('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Current history step 3 should have new filter zone=b', () => {
|
it('Current history step 4 should have new filter zone=b', () => {
|
||||||
expect(getStepFilterVar(3).state.filters[0].key).toBe('zone');
|
expect(getStepFilterVar(4).state.filters[0].key).toBe('zone');
|
||||||
expect(getStepFilterVar(3).state.filters[0].value).toBe('b');
|
expect(getStepFilterVar(4).state.filters[0].value).toBe('b');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('And then when returning again to step 1', () => {
|
describe('And then when returning again to step 1', () => {
|
||||||
@ -316,14 +316,14 @@ describe('DataTrail', () => {
|
|||||||
expect(getStepFilterVar(1).state.filters.length).toBe(0);
|
expect(getStepFilterVar(1).state.filters.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('History step 2 should still have old filter zone=a', () => {
|
it('History step 3 should still have old filter zone=a', () => {
|
||||||
expect(getStepFilterVar(2).state.filters[0].key).toBe('zone');
|
|
||||||
expect(getStepFilterVar(2).state.filters[0].value).toBe('a');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('History step 3 should have new filter zone=b', () => {
|
|
||||||
expect(getStepFilterVar(3).state.filters[0].key).toBe('zone');
|
expect(getStepFilterVar(3).state.filters[0].key).toBe('zone');
|
||||||
expect(getStepFilterVar(3).state.filters[0].value).toBe('b');
|
expect(getStepFilterVar(3).state.filters[0].value).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('History step 4 should have new filter zone=b', () => {
|
||||||
|
expect(getStepFilterVar(4).state.filters[0].key).toBe('zone');
|
||||||
|
expect(getStepFilterVar(4).state.filters[0].value).toBe('b');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -331,11 +331,11 @@ describe('DataTrail', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When going back to history step 1', () => {
|
describe('When going back to history step 2', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
trail.publishEvent(new MetricSelectedEvent('first_metric'));
|
trail.publishEvent(new MetricSelectedEvent('first_metric'));
|
||||||
trail.publishEvent(new MetricSelectedEvent('second_metric'));
|
trail.publishEvent(new MetricSelectedEvent('second_metric'));
|
||||||
trail.state.history.goBackToStep(1);
|
trail.state.history.goBackToStep(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should restore state and url', () => {
|
it('Should restore state and url', () => {
|
||||||
@ -343,12 +343,12 @@ describe('DataTrail', () => {
|
|||||||
expect(locationService.getSearchObject().metric).toBe('first_metric');
|
expect(locationService.getSearchObject().metric).toBe('first_metric');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history currentStep to 1', () => {
|
it('Should set history currentStep to 2', () => {
|
||||||
expect(trail.state.history.state.currentStep).toBe(1);
|
expect(trail.state.history.state.currentStep).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not create another history step', () => {
|
it('Should not create another history step', () => {
|
||||||
expect(trail.state.history.state.steps.length).toBe(3);
|
expect(trail.state.history.state.steps.length).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('But then selecting a new metric', () => {
|
describe('But then selecting a new metric', () => {
|
||||||
@ -357,15 +357,15 @@ describe('DataTrail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should create another history step', () => {
|
it('Should create another history step', () => {
|
||||||
expect(trail.state.history.state.steps.length).toBe(4);
|
expect(trail.state.history.state.steps.length).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history current step to 3', () => {
|
it('Should set history current step to 4', () => {
|
||||||
expect(trail.state.history.state.currentStep).toBe(3);
|
expect(trail.state.history.state.currentStep).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should set history step 3 parent index to 1', () => {
|
it('Should set history step 4 parent index to 2', () => {
|
||||||
expect(trail.state.history.state.steps[3].parentIndex).toBe(1);
|
expect(trail.state.history.state.steps[4].parentIndex).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('And browser back button is pressed', () => {
|
describe('And browser back button is pressed', () => {
|
||||||
@ -407,9 +407,9 @@ describe('DataTrail', () => {
|
|||||||
expect(getFilterVar().state.filters[0].value).toBe('a');
|
expect(getFilterVar().state.filters[0].value).toBe('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Filter of step 1 should be zone=a', () => {
|
it('Filter of step 2 should be zone=a', () => {
|
||||||
expect(getStepFilterVar(1).state.filters[0].key).toBe('zone');
|
expect(getStepFilterVar(2).state.filters[0].key).toBe('zone');
|
||||||
expect(getStepFilterVar(1).state.filters[0].value).toBe('a');
|
expect(getStepFilterVar(2).state.filters[0].value).toBe('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Filter of step 0 should empty', () => {
|
it('Filter of step 0 should empty', () => {
|
||||||
@ -440,12 +440,12 @@ describe('DataTrail', () => {
|
|||||||
expect(trail.state.$timeRange?.state.from).toBe('now-15m');
|
expect(trail.state.$timeRange?.state.from).toBe('now-15m');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Time range `from` of step 1 should be now-15m', () => {
|
it('Time range `from` of step 2 should be now-15m', () => {
|
||||||
expect(trail.state.history.state.steps[1].trailState.$timeRange?.state.from).toBe('now-15m');
|
expect(trail.state.history.state.steps[2].trailState.$timeRange?.state.from).toBe('now-15m');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Time range `from` of step 0 should be now-6h', () => {
|
it('Time range `from` of step 1 should be now-6h', () => {
|
||||||
expect(trail.state.history.state.steps[0].trailState.$timeRange?.state.from).toBe('now-6h');
|
expect(trail.state.history.state.steps[1].trailState.$timeRange?.state.from).toBe('now-6h');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When returning to step 0', () => {
|
describe('When returning to step 0', () => {
|
||||||
|
65
public/app/features/trails/DataTrailsHistory.test.tsx
Normal file
65
public/app/features/trails/DataTrailsHistory.test.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { SceneObjectUrlValues } from '@grafana/scenes';
|
||||||
|
|
||||||
|
import { parseFilterTooltip, parseTimeTooltip } from './DataTrailsHistory';
|
||||||
|
|
||||||
|
type ParseTimeTestCase = {
|
||||||
|
name: string;
|
||||||
|
input: SceneObjectUrlValues;
|
||||||
|
expected: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ParseFilterTestCase = {
|
||||||
|
name: string;
|
||||||
|
input: { urlValues: SceneObjectUrlValues; filtersApplied: string[] };
|
||||||
|
expected: string;
|
||||||
|
expectedFiltersApplied: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('DataTrailsHistory', () => {
|
||||||
|
describe('parseTimeTooltip', () => {
|
||||||
|
// global timezone is set to Pacific/Easter, see jest-config.js file
|
||||||
|
test.each<ParseTimeTestCase>([
|
||||||
|
{
|
||||||
|
name: 'from history',
|
||||||
|
input: { from: '2024-07-22T18:30:00.000Z', to: '2024-07-22T19:30:00.000Z' },
|
||||||
|
expected: '2024-07-22 12:30:00 - 2024-07-22 13:30:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'time change event with timezone',
|
||||||
|
input: { from: '2024-07-22T18:30:00.000Z', to: '2024-07-22T19:30:00.000Z', timeZone: 'Europe/Berlin' },
|
||||||
|
expected: '2024-07-22 20:30:00 - 2024-07-22 21:30:00',
|
||||||
|
},
|
||||||
|
])('$name', ({ input, expected }) => {
|
||||||
|
const result = parseTimeTooltip(input);
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseFilterTooltip', () => {
|
||||||
|
test.each<ParseFilterTestCase>([
|
||||||
|
{
|
||||||
|
name: 'from history initial load',
|
||||||
|
input: {
|
||||||
|
urlValues: { 'var-filters': ['job|=|grafana'] },
|
||||||
|
filtersApplied: [],
|
||||||
|
},
|
||||||
|
expected: 'job = grafana',
|
||||||
|
expectedFiltersApplied: ['job|=|grafana'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'from history initial load',
|
||||||
|
input: {
|
||||||
|
urlValues: { 'var-filters': ['job|=|grafana', 'instance|=|host.docker.internal:3000'] },
|
||||||
|
filtersApplied: ['job|=|grafana'],
|
||||||
|
},
|
||||||
|
expected: 'instance = host.docker.internal:3000',
|
||||||
|
expectedFiltersApplied: ['job|=|grafana', 'instance|=|host.docker.internal:3000'],
|
||||||
|
},
|
||||||
|
])('$name', ({ input, expected, expectedFiltersApplied }) => {
|
||||||
|
const filtersApplied = input.filtersApplied;
|
||||||
|
const result = parseFilterTooltip(input.urlValues, filtersApplied);
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
expect(filtersApplied).toEqual(expectedFiltersApplied);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,19 +1,24 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { getTimeZoneInfo, GrafanaTheme2, InternalTimeZones, TIME_FORMAT } from '@grafana/data';
|
||||||
|
import { convertRawToRange } from '@grafana/data/src/datetime/rangeutil';
|
||||||
import {
|
import {
|
||||||
SceneObjectState,
|
getUrlSyncManager,
|
||||||
SceneObjectBase,
|
|
||||||
SceneComponentProps,
|
SceneComponentProps,
|
||||||
SceneVariableValueChangedEvent,
|
SceneObjectBase,
|
||||||
|
SceneObjectState,
|
||||||
SceneObjectStateChangedEvent,
|
SceneObjectStateChangedEvent,
|
||||||
|
SceneObjectUrlValue,
|
||||||
|
SceneObjectUrlValues,
|
||||||
SceneTimeRange,
|
SceneTimeRange,
|
||||||
sceneUtils,
|
sceneUtils,
|
||||||
|
SceneVariableValueChangedEvent,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { useStyles2, Tooltip, Stack } from '@grafana/ui';
|
import { Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { DataTrail, DataTrailState, getTopSceneFor } from './DataTrail';
|
import { DataTrail, DataTrailState, getTopSceneFor } from './DataTrail';
|
||||||
|
import { SerializedTrailHistory } from './TrailStore/TrailStore';
|
||||||
import { reportExploreMetrics } from './interactions';
|
import { reportExploreMetrics } from './interactions';
|
||||||
import { VAR_FILTERS } from './shared';
|
import { VAR_FILTERS } from './shared';
|
||||||
import { getTrailFor, isSceneTimeRangeState } from './utils';
|
import { getTrailFor, isSceneTimeRangeState } from './utils';
|
||||||
@ -21,23 +26,42 @@ import { getTrailFor, isSceneTimeRangeState } from './utils';
|
|||||||
export interface DataTrailsHistoryState extends SceneObjectState {
|
export interface DataTrailsHistoryState extends SceneObjectState {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
steps: DataTrailHistoryStep[];
|
steps: DataTrailHistoryStep[];
|
||||||
|
filtersApplied: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDataTrailsHistoryState(state: SceneObjectState): state is DataTrailsHistoryState {
|
export function isDataTrailsHistoryState(state: SceneObjectState): state is DataTrailsHistoryState {
|
||||||
return 'currentStep' in state && 'steps' in state;
|
return 'currentStep' in state && 'steps' in state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDataTrailHistoryFilter(filter?: SceneObjectUrlValue): filter is string[] {
|
||||||
|
return !!filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isString = (value: unknown): value is string => typeof value === 'string';
|
||||||
|
|
||||||
export interface DataTrailHistoryStep {
|
export interface DataTrailHistoryStep {
|
||||||
description: string;
|
description: string;
|
||||||
|
detail: string;
|
||||||
type: TrailStepType;
|
type: TrailStepType;
|
||||||
trailState: DataTrailState;
|
trailState: DataTrailState;
|
||||||
parentIndex: number;
|
parentIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TrailStepType = 'filters' | 'time' | 'metric' | 'start';
|
export type TrailStepType = 'filters' | 'time' | 'metric' | 'start' | 'metric_page';
|
||||||
|
|
||||||
|
const filterSubst = ` $2 `;
|
||||||
|
const filterPipeRegex = /(\|)(=|=~|!=|>|<|!~)(\|)/g;
|
||||||
|
const stepDescriptionMap: Record<TrailStepType, string> = {
|
||||||
|
start: 'Start of history',
|
||||||
|
metric: 'Metric selected:',
|
||||||
|
metric_page: 'Metric select page',
|
||||||
|
filters: 'Filter applied:',
|
||||||
|
time: 'Time range changed:',
|
||||||
|
};
|
||||||
|
|
||||||
export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
||||||
public constructor(state: Partial<DataTrailsHistoryState>) {
|
public constructor(state: Partial<DataTrailsHistoryState>) {
|
||||||
super({ steps: state.steps ?? [], currentStep: state.currentStep ?? 0 });
|
super({ steps: state.steps ?? [], currentStep: state.currentStep ?? 0, filtersApplied: [] });
|
||||||
|
|
||||||
this.addActivationHandler(this._onActivate.bind(this));
|
this.addActivationHandler(this._onActivate.bind(this));
|
||||||
}
|
}
|
||||||
@ -62,7 +86,9 @@ export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
|||||||
|
|
||||||
// But must add a secondary step to represent the selection of the metric
|
// But must add a secondary step to represent the selection of the metric
|
||||||
// for this restored trail state
|
// for this restored trail state
|
||||||
this.addTrailStep(trail, 'metric');
|
this.addTrailStep(trail, 'metric', trail.state.metric);
|
||||||
|
} else {
|
||||||
|
this.addTrailStep(trail, 'metric_page');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,15 +99,20 @@ export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
|||||||
this.state.steps[0].trailState = sceneUtils.cloneSceneObjectState(oldState, { history: this });
|
this.state.steps[0].trailState = sceneUtils.cloneSceneObjectState(oldState, { history: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newState.metric || oldState.metric) {
|
if (!newState.metric) {
|
||||||
this.addTrailStep(trail, 'metric');
|
this.addTrailStep(trail, 'metric_page');
|
||||||
|
} else {
|
||||||
|
this.addTrailStep(trail, 'metric', newState.metric);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
trail.subscribeToEvent(SceneVariableValueChangedEvent, (evt) => {
|
trail.subscribeToEvent(SceneVariableValueChangedEvent, (evt) => {
|
||||||
if (evt.payload.state.name === VAR_FILTERS) {
|
if (evt.payload.state.name === VAR_FILTERS) {
|
||||||
this.addTrailStep(trail, 'filters');
|
const filtersApplied = this.state.filtersApplied;
|
||||||
|
const urlState = getUrlSyncManager().getUrlState(trail);
|
||||||
|
this.addTrailStep(trail, 'filters', parseFilterTooltip(urlState, filtersApplied));
|
||||||
|
this.setState({ filtersApplied });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -93,14 +124,22 @@ export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
|||||||
if (prevState.from === newState.from && prevState.to === newState.to) {
|
if (prevState.from === newState.from && prevState.to === newState.to) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.addTrailStep(trail, 'time');
|
this.addTrailStep(
|
||||||
|
trail,
|
||||||
|
'time',
|
||||||
|
parseTimeTooltip({
|
||||||
|
from: newState.from,
|
||||||
|
to: newState.to,
|
||||||
|
timeZone: newState.timeZone,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public addTrailStep(trail: DataTrail, type: TrailStepType) {
|
public addTrailStep(trail: DataTrail, type: TrailStepType, detail = '') {
|
||||||
if (this.stepTransitionInProgress) {
|
if (this.stepTransitionInProgress) {
|
||||||
// Do not add trail steps when step transition is in progress
|
// Do not add trail steps when step transition is in progress
|
||||||
return;
|
return;
|
||||||
@ -114,8 +153,49 @@ export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
|||||||
steps: [
|
steps: [
|
||||||
...this.state.steps,
|
...this.state.steps,
|
||||||
{
|
{
|
||||||
description: 'Test',
|
|
||||||
type,
|
type,
|
||||||
|
detail,
|
||||||
|
description: stepDescriptionMap[type],
|
||||||
|
trailState: sceneUtils.cloneSceneObjectState(trail.state, { history: this }),
|
||||||
|
parentIndex,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public addTrailStepFromStorage(trail: DataTrail, step: SerializedTrailHistory) {
|
||||||
|
if (this.stepTransitionInProgress) {
|
||||||
|
// Do not add trail steps when step transition is in progress
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = step.type;
|
||||||
|
const stepIndex = this.state.steps.length;
|
||||||
|
const parentIndex = type === 'start' ? -1 : this.state.currentStep;
|
||||||
|
const filtersApplied = this.state.filtersApplied;
|
||||||
|
let detail = '';
|
||||||
|
|
||||||
|
switch (step.type) {
|
||||||
|
case 'metric':
|
||||||
|
detail = step.urlValues.metric?.toString() ?? '';
|
||||||
|
break;
|
||||||
|
case 'filters':
|
||||||
|
detail = parseFilterTooltip(step.urlValues, filtersApplied);
|
||||||
|
break;
|
||||||
|
case 'time':
|
||||||
|
detail = parseTimeTooltip(step.urlValues);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
filtersApplied,
|
||||||
|
currentStep: stepIndex,
|
||||||
|
steps: [
|
||||||
|
...this.state.steps,
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
detail,
|
||||||
|
description: stepDescriptionMap[type],
|
||||||
trailState: sceneUtils.cloneSceneObjectState(trail.state, { history: this }),
|
trailState: sceneUtils.cloneSceneObjectState(trail.state, { history: this }),
|
||||||
parentIndex,
|
parentIndex,
|
||||||
},
|
},
|
||||||
@ -145,8 +225,8 @@ export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
|||||||
renderStepTooltip(step: DataTrailHistoryStep) {
|
renderStepTooltip(step: DataTrailHistoryStep) {
|
||||||
return (
|
return (
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
<div>{step.type}</div>
|
<div>{step.description}</div>
|
||||||
{step.type === 'metric' && <div>{step.trailState.metric || 'Select new metric'}</div>}
|
{step.detail !== '' && <div>{step.detail}</div>}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -220,6 +300,43 @@ export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseTimeTooltip(urlValues: SceneObjectUrlValues): string {
|
||||||
|
if (!isSceneTimeRangeState(urlValues)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = convertRawToRange({
|
||||||
|
from: urlValues.from,
|
||||||
|
to: urlValues.to,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zone = isString(urlValues.timeZone) ? urlValues.timeZone : InternalTimeZones.localBrowserTime;
|
||||||
|
const tzInfo = getTimeZoneInfo(zone, Date.now());
|
||||||
|
|
||||||
|
const from = range.from.subtract(tzInfo?.offsetInMins ?? 0, 'minute').format(TIME_FORMAT);
|
||||||
|
const to = range.to.subtract(tzInfo?.offsetInMins ?? 0, 'minute').format(TIME_FORMAT);
|
||||||
|
|
||||||
|
return `${from} - ${to}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFilterTooltip(urlValues: SceneObjectUrlValues, filtersApplied: string[]): string {
|
||||||
|
let detail = '';
|
||||||
|
const varFilters = urlValues['var-filters'];
|
||||||
|
if (isDataTrailHistoryFilter(varFilters)) {
|
||||||
|
detail =
|
||||||
|
varFilters.filter((f) => {
|
||||||
|
if (f !== '' && !filtersApplied.includes(f)) {
|
||||||
|
filtersApplied.push(f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})[0] ?? '';
|
||||||
|
}
|
||||||
|
// filters saved as key|operator|value
|
||||||
|
// we need to remove pipes (|)
|
||||||
|
return detail.replace(filterPipeRegex, filterSubst);
|
||||||
|
}
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2) {
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
const visTheme = theme.visualization;
|
const visTheme = theme.visualization;
|
||||||
|
|
||||||
@ -290,6 +407,7 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
start: generateStepTypeStyle(visTheme.getColorByName('green')),
|
start: generateStepTypeStyle(visTheme.getColorByName('green')),
|
||||||
filters: generateStepTypeStyle(visTheme.getColorByName('purple')),
|
filters: generateStepTypeStyle(visTheme.getColorByName('purple')),
|
||||||
metric: generateStepTypeStyle(visTheme.getColorByName('orange')),
|
metric: generateStepTypeStyle(visTheme.getColorByName('orange')),
|
||||||
|
metric_page: generateStepTypeStyle(visTheme.getColorByName('orange')),
|
||||||
time: generateStepTypeStyle(theme.colors.primary.main),
|
time: generateStepTypeStyle(theme.colors.primary.main),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -14,13 +14,15 @@ import { createBookmarkSavedNotification } from './utils';
|
|||||||
|
|
||||||
const MAX_RECENT_TRAILS = 20;
|
const MAX_RECENT_TRAILS = 20;
|
||||||
|
|
||||||
export interface SerializedTrail {
|
export interface SerializedTrailHistory {
|
||||||
history: Array<{
|
|
||||||
urlValues: SceneObjectUrlValues;
|
urlValues: SceneObjectUrlValues;
|
||||||
type: TrailStepType;
|
type: TrailStepType;
|
||||||
description: string;
|
description: string;
|
||||||
parentIndex: number;
|
parentIndex: number;
|
||||||
}>;
|
}
|
||||||
|
|
||||||
|
export interface SerializedTrail {
|
||||||
|
history: SerializedTrailHistory[];
|
||||||
currentStep?: number; // Assume last step in history if not specified
|
currentStep?: number; // Assume last step in history if not specified
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
}
|
}
|
||||||
@ -98,7 +100,7 @@ export class TrailStore {
|
|||||||
const parentIndex = step.parentIndex ?? trail.state.history.state.steps.length - 1;
|
const parentIndex = step.parentIndex ?? trail.state.history.state.steps.length - 1;
|
||||||
// Set the parent of the next trail step by setting the current step in history.
|
// Set the parent of the next trail step by setting the current step in history.
|
||||||
trail.state.history.setState({ currentStep: parentIndex });
|
trail.state.history.setState({ currentStep: parentIndex });
|
||||||
trail.state.history.addTrailStep(trail, step.type);
|
trail.state.history.addTrailStepFromStorage(trail, step);
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentStep = t.currentStep ?? trail.state.history.state.steps.length - 1;
|
const currentStep = t.currentStep ?? trail.state.history.state.steps.length - 1;
|
||||||
|
@ -100,7 +100,9 @@ export function getColorByIndex(index: number) {
|
|||||||
export type SceneTimeRangeState = SceneObjectState & {
|
export type SceneTimeRangeState = SceneObjectState & {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
|
timeZone?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isSceneTimeRangeState(state: SceneObjectState): state is SceneTimeRangeState {
|
export function isSceneTimeRangeState(state: SceneObjectState): state is SceneTimeRangeState {
|
||||||
const keys = Object.keys(state);
|
const keys = Object.keys(state);
|
||||||
return keys.includes('from') && keys.includes('to');
|
return keys.includes('from') && keys.includes('to');
|
||||||
|
Loading…
Reference in New Issue
Block a user