mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
data-trails: hightlight current node and its ancestry (#78660)
* feat: data-trails: show current node and ancestry
This commit is contained in:
parent
01ad2918d6
commit
a5377f85ce
@ -40,6 +40,14 @@ describe('DataTrail', () => {
|
|||||||
expect(trail.state.stepIndex).toBe(0);
|
expect(trail.state.stepIndex).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should set parentIndex to -1', () => {
|
||||||
|
expect(trail.state.parentIndex).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should set history currentStep to 0', () => {
|
||||||
|
expect(trail.state.history.state.currentStep).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
describe('And metric is selected', () => {
|
describe('And metric is selected', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
trail.publishEvent(new MetricSelectedEvent('metric_bucket'));
|
trail.publishEvent(new MetricSelectedEvent('metric_bucket'));
|
||||||
@ -61,9 +69,17 @@ describe('DataTrail', () => {
|
|||||||
it('Should set stepIndex to 1', () => {
|
it('Should set stepIndex to 1', () => {
|
||||||
expect(trail.state.stepIndex).toBe(1);
|
expect(trail.state.stepIndex).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should set history currentStep to 1', () => {
|
||||||
|
expect(trail.state.history.state.currentStep).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should set parentIndex to 0', () => {
|
||||||
|
expect(trail.state.parentIndex).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When going back to history step', () => {
|
describe('When going back to history step 1', () => {
|
||||||
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'));
|
||||||
@ -79,13 +95,34 @@ describe('DataTrail', () => {
|
|||||||
expect(trail.state.stepIndex).toBe(1);
|
expect(trail.state.stepIndex).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should set history currentStep to 1', () => {
|
||||||
|
expect(trail.state.history.state.currentStep).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
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(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('But selecting a new metric should create another history step', () => {
|
describe('But then selecting a new metric', () => {
|
||||||
trail.publishEvent(new MetricSelectedEvent('third_metric'));
|
beforeEach(() => {
|
||||||
expect(trail.state.history.state.steps.length).toBe(4);
|
trail.publishEvent(new MetricSelectedEvent('third_metric'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should create another history step', () => {
|
||||||
|
expect(trail.state.history.state.steps.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should set stepIndex to 3', () => {
|
||||||
|
expect(trail.state.stepIndex).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should set history currentStep to 1', () => {
|
||||||
|
expect(trail.state.history.state.currentStep).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should set parentIndex to 1', () => {
|
||||||
|
expect(trail.state.parentIndex).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -46,6 +46,7 @@ export interface DataTrailState extends SceneObjectState {
|
|||||||
|
|
||||||
// Indicates which step in the data trail this is
|
// Indicates which step in the data trail this is
|
||||||
stepIndex: number;
|
stepIndex: number;
|
||||||
|
parentIndex: number; // If there is no parent, this will be -1
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataTrail extends SceneObjectBase<DataTrailState> {
|
export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||||
@ -64,6 +65,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
|||||||
history: state.history ?? new DataTrailHistory({}),
|
history: state.history ?? new DataTrailHistory({}),
|
||||||
settings: state.settings ?? new DataTrailSettings({}),
|
settings: state.settings ?? new DataTrailSettings({}),
|
||||||
stepIndex: state.stepIndex ?? 0,
|
stepIndex: state.stepIndex ?? 0,
|
||||||
|
parentIndex: state.parentIndex ?? -1,
|
||||||
...state,
|
...state,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
@ -18,6 +18,7 @@ import { VAR_FILTERS } from './shared';
|
|||||||
import { getTrailFor } from './utils';
|
import { getTrailFor } from './utils';
|
||||||
|
|
||||||
export interface DataTrailsHistoryState extends SceneObjectState {
|
export interface DataTrailsHistoryState extends SceneObjectState {
|
||||||
|
currentStep: number;
|
||||||
steps: DataTrailHistoryStep[];
|
steps: DataTrailHistoryStep[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ export type TrailStepType = 'filters' | 'time' | 'metric' | 'start';
|
|||||||
|
|
||||||
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 ?? [] });
|
super({ steps: state.steps ?? [], currentStep: state.currentStep ?? 0 });
|
||||||
|
|
||||||
this.addActivationHandler(this._onActivate.bind(this));
|
this.addActivationHandler(this._onActivate.bind(this));
|
||||||
}
|
}
|
||||||
@ -44,17 +45,26 @@ export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trail.subscribeToState((newState, oldState) => {
|
trail.subscribeToState((newState, oldState) => {
|
||||||
|
// Check if new and old state are at the same step index
|
||||||
|
// Then we know this is a history transition
|
||||||
|
const isMovingThroughHistory = newState.stepIndex !== oldState.stepIndex;
|
||||||
|
|
||||||
|
if (isMovingThroughHistory) {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
currentStep: newState.stepIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (newState.metric !== oldState.metric) {
|
if (newState.metric !== oldState.metric) {
|
||||||
if (this.state.steps.length === 1) {
|
if (this.state.steps.length === 1) {
|
||||||
// For the first step we want to update the starting state so that it contains data
|
// For the first step we want to update the starting state so that it contains data
|
||||||
this.state.steps[0].trailState = sceneUtils.cloneSceneObjectState(oldState, { history: this });
|
this.state.steps[0].trailState = sceneUtils.cloneSceneObjectState(oldState, { history: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if new and old state are at the same step index
|
if (newState.metric) {
|
||||||
// Then we know this isn't a history transition
|
|
||||||
const isMovingThroughHistory = newState.stepIndex !== oldState.stepIndex;
|
|
||||||
|
|
||||||
if (newState.metric && !isMovingThroughHistory) {
|
|
||||||
this.addTrailStep(trail, 'metric');
|
this.addTrailStep(trail, 'metric');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,11 +84,13 @@ export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addTrailStep(trail: DataTrail, type: TrailStepType) {
|
public addTrailStep(trail: DataTrail, type: TrailStepType) {
|
||||||
|
// Update the trail's new current step state, and note its parent step.
|
||||||
const stepIndex = this.state.steps.length;
|
const stepIndex = this.state.steps.length;
|
||||||
// Update the trail's current step state. It is being given a step index.
|
const parentIndex = type === 'start' ? -1 : trail.state.stepIndex;
|
||||||
trail.setState({ ...trail.state, stepIndex });
|
trail.setState({ ...trail.state, stepIndex, parentIndex });
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
currentStep: stepIndex,
|
||||||
steps: [
|
steps: [
|
||||||
...this.state.steps,
|
...this.state.steps,
|
||||||
{
|
{
|
||||||
@ -100,17 +112,52 @@ export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Component = ({ model }: SceneComponentProps<DataTrailHistory>) => {
|
public static Component = ({ model }: SceneComponentProps<DataTrailHistory>) => {
|
||||||
const { steps } = model.useState();
|
const { steps, currentStep } = model.useState();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const trail = getTrailFor(model);
|
const trail = getTrailFor(model);
|
||||||
|
|
||||||
|
const { ancestry, alternatePredecessorStyle } = useMemo(() => {
|
||||||
|
const ancestry = new Set<number>();
|
||||||
|
let cursor = currentStep;
|
||||||
|
while (cursor >= 0) {
|
||||||
|
ancestry.add(cursor);
|
||||||
|
cursor = steps[cursor].trailState.parentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alternatePredecessorStyle = new Map<number, string>();
|
||||||
|
|
||||||
|
ancestry.forEach((index) => {
|
||||||
|
const parent = steps[index].trailState.parentIndex;
|
||||||
|
if (parent + 1 !== index) {
|
||||||
|
alternatePredecessorStyle.set(index, createAlternatePredecessorStyle(index, parent));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ancestry, alternatePredecessorStyle };
|
||||||
|
}, [currentStep, steps]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.heading}>Trail</div>
|
<div className={styles.heading}>Trail</div>
|
||||||
{steps.map((step, index) => (
|
{steps.map((step, index) => (
|
||||||
<Tooltip content={() => model.renderStepTooltip(step)} key={index}>
|
<Tooltip content={() => model.renderStepTooltip(step)} key={index}>
|
||||||
<button
|
<button
|
||||||
className={cx(styles.step, styles.stepTypes[step.type])}
|
className={cx(
|
||||||
|
// Base for all steps
|
||||||
|
styles.step,
|
||||||
|
// Specifics per step type
|
||||||
|
styles.stepTypes[step.type],
|
||||||
|
// To highlight selected step
|
||||||
|
model.state.currentStep === index ? styles.stepSelected : '',
|
||||||
|
// To alter the look of steps with distant non-directly preceding parent
|
||||||
|
alternatePredecessorStyle.get(index) ?? '',
|
||||||
|
// To remove direct link for steps that don't have a direct parent
|
||||||
|
index !== step.trailState.parentIndex + 1 ? styles.stepOmitsDirectLeftLink : '',
|
||||||
|
// To remove the direct parent link on the start node as well
|
||||||
|
index === 0 ? styles.stepOmitsDirectLeftLink : '',
|
||||||
|
// To darken steps that aren't the current step's ancesters
|
||||||
|
!ancestry.has(index) ? styles.stepIsNotAncestorOfCurrent : ''
|
||||||
|
)}
|
||||||
onClick={() => trail.goBackToStep(step)}
|
onClick={() => trail.goBackToStep(step)}
|
||||||
></button>
|
></button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -144,48 +191,87 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
background: theme.colors.primary.main,
|
background: theme.colors.primary.main,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
transform: 'scale(1.1)',
|
opacity: 1,
|
||||||
},
|
},
|
||||||
'&:after': {
|
'&:hover:before': {
|
||||||
|
// We only want the node to hover, not its connection to its parent
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
'&:before': {
|
||||||
content: '""',
|
content: '""',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 2,
|
height: 2,
|
||||||
left: 8,
|
left: -10,
|
||||||
top: 3,
|
top: 3,
|
||||||
background: theme.colors.primary.border,
|
background: theme.colors.primary.border,
|
||||||
|
pointerEvents: 'none',
|
||||||
},
|
},
|
||||||
'&:last-child': {
|
}),
|
||||||
'&:after': {
|
stepSelected: css({
|
||||||
display: 'none',
|
'&:after': {
|
||||||
},
|
content: '""',
|
||||||
|
borderStyle: `solid`,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: '50%',
|
||||||
|
position: 'absolute',
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
left: -4,
|
||||||
|
top: -4,
|
||||||
|
boxShadow: `0px 0px 0px 2px inset ${theme.colors.background.canvas}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
stepOmitsDirectLeftLink: css({
|
||||||
|
'&:before': {
|
||||||
|
background: 'none',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
stepIsNotAncestorOfCurrent: css({
|
||||||
|
opacity: 0.2,
|
||||||
|
'&:hover:before': {
|
||||||
|
opacity: 0.2,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
stepTypes: {
|
stepTypes: {
|
||||||
start: css({
|
start: generateStepTypeStyle(visTheme.getColorByName('green')),
|
||||||
background: visTheme.getColorByName('green'),
|
filters: generateStepTypeStyle(visTheme.getColorByName('purple')),
|
||||||
'&:after': {
|
metric: generateStepTypeStyle(visTheme.getColorByName('orange')),
|
||||||
background: visTheme.getColorByName('green'),
|
time: generateStepTypeStyle(theme.colors.primary.main),
|
||||||
},
|
|
||||||
}),
|
|
||||||
filters: css({
|
|
||||||
background: visTheme.getColorByName('purple'),
|
|
||||||
'&:after': {
|
|
||||||
background: visTheme.getColorByName('purple'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
metric: css({
|
|
||||||
background: visTheme.getColorByName('orange'),
|
|
||||||
'&:after': {
|
|
||||||
background: visTheme.getColorByName('orange'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
time: css({
|
|
||||||
background: theme.colors.primary.main,
|
|
||||||
'&:after': {
|
|
||||||
background: theme.colors.primary.main,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateStepTypeStyle(color: string) {
|
||||||
|
return css({
|
||||||
|
background: color,
|
||||||
|
'&:before': {
|
||||||
|
background: color,
|
||||||
|
borderColor: color,
|
||||||
|
},
|
||||||
|
'&:after': {
|
||||||
|
borderColor: color,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAlternatePredecessorStyle(index: number, parent: number) {
|
||||||
|
const difference = index - parent;
|
||||||
|
|
||||||
|
const NODE_DISTANCE = 18;
|
||||||
|
const distanceToParent = difference * NODE_DISTANCE;
|
||||||
|
|
||||||
|
return css({
|
||||||
|
'&:before': {
|
||||||
|
content: '""',
|
||||||
|
width: distanceToParent + 2,
|
||||||
|
height: 10,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderBottom: 'none',
|
||||||
|
top: -10,
|
||||||
|
left: 3 - distanceToParent,
|
||||||
|
background: 'none',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user