-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(DiffView): Add CTAs and comparison presets #231
Changes from all commits
6fbc0b4
1725337
ee7cd91
7ba7ad5
b6a5afb
5366824
331b2f7
2f895a3
66b711a
b3e3d02
4729ac0
2e98733
416cb53
b58f288
df9b745
7cb4fe2
0ba3815
a2b4a9f
783aceb
3c17ae6
5ea55f7
1659c0f
1844ff0
74d8eba
089e340
eee3deb
48a79df
c4b338b
4057904
f092ab7
39f6692
f93e77b
6582417
a34c41a
c529f0c
ae0421a
845acca
1dfe0c4
e872b51
a8f762e
91fd076
3f7611e
447fd4a
44bbced
a7fe893
347c3b2
7a09cab
8d3f4b2
cc29365
7fcb139
2ebf4ff
e3c3434
1bc8580
b82c048
73b7432
df4bb18
af51091
324a052
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,12 @@ | ||
import { css } from '@emotion/css'; | ||
import { dateTimeFormat, FieldMatcherID, getValueFormat, GrafanaTheme2, systemDateFormats } from '@grafana/data'; | ||
import { | ||
dateTime, | ||
dateTimeFormat, | ||
FieldMatcherID, | ||
getValueFormat, | ||
GrafanaTheme2, | ||
systemDateFormats, | ||
} from '@grafana/data'; | ||
import { | ||
SceneComponentProps, | ||
SceneDataTransformer, | ||
|
@@ -17,7 +24,7 @@ import { getProfileMetric, ProfileMetricId } from '@shared/infrastructure/profil | |
import { omit } from 'lodash'; | ||
import React from 'react'; | ||
|
||
import { getDefaultTimeRange } from '../../../../domain/getDefaultTimeRange'; | ||
import { buildTimeRange } from '../../../../domain/buildTimeRange'; | ||
import { FiltersVariable } from '../../../../domain/variables/FiltersVariable/FiltersVariable'; | ||
import { getSceneVariableValue } from '../../../../helpers/getSceneVariableValue'; | ||
import { getSeriesStatsValue } from '../../../../infrastructure/helpers/getSeriesStatsValue'; | ||
|
@@ -26,6 +33,7 @@ import { PanelType } from '../../../SceneByVariableRepeaterGrid/components/Scene | |
import { addRefId, addStats } from '../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; | ||
import { CompareTarget } from '../../../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; | ||
import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries'; | ||
import { Preset } from '../ScenePresetsPicker/ScenePresetsPicker'; | ||
import { | ||
SceneTimeRangeWithAnnotations, | ||
TimeRangeWithAnnotationsMode, | ||
|
@@ -76,7 +84,7 @@ export class SceneComparePanel extends SceneObjectBase<SceneComparePanelState> { | |
filterKey, | ||
title, | ||
color, | ||
$timeRange: new SceneTimeRange({ key: `${target}-panel-timerange`, ...getDefaultTimeRange() }), | ||
$timeRange: new SceneTimeRange({ key: `${target}-panel-timerange`, ...buildTimeRange('now-1h', 'now') }), | ||
timePicker: new SceneTimePicker({ isOnCanvas: true }), | ||
refreshPicker: new SceneRefreshPicker({ isOnCanvas: true }), | ||
timeseriesPanel: SceneComparePanel.buildTimeSeriesPanel({ target, filterKey, title, color }), | ||
|
@@ -120,18 +128,18 @@ export class SceneComparePanel extends SceneObjectBase<SceneComparePanelState> { | |
const allValuesSum = getSeriesStatsValue(s, 'allValuesSum') || 0; | ||
const formattedValue = getValueFormat(metricField.config.unit)(allValuesSum); | ||
const total = `${formattedValue.text}${formattedValue.suffix}`; | ||
const [diffFrom, diffTo, timeZone] = SceneComparePanel.getFlameGraphRange(timeseriesPanel); | ||
const [diffFrom, diffTo, timeZone] = SceneComparePanel.getDiffRange(timeseriesPanel); | ||
|
||
const displayName = | ||
diffFrom && diffTo | ||
? `${title} total = ${total} / Flame graph range = ${dateTimeFormat(diffFrom, { | ||
? `Total = ${total} / Flame graph range = ${dateTimeFormat(diffFrom, { | ||
format: systemDateFormats.fullDate, | ||
timeZone, | ||
})} → ${dateTimeFormat(diffTo, { | ||
format: systemDateFormats.fullDate, | ||
timeZone, | ||
})}` | ||
: `${title} total = ${total}`; | ||
: `Total = ${total}`; | ||
|
||
return { | ||
matcher: { id: FieldMatcherID.byFrameRefID, options: s.refId }, | ||
|
@@ -163,7 +171,7 @@ export class SceneComparePanel extends SceneObjectBase<SceneComparePanelState> { | |
return timeseriesPanel; | ||
} | ||
|
||
static getFlameGraphRange( | ||
static getDiffRange( | ||
timeseriesPanel: SceneLabelValuesTimeseries | ||
): [number | undefined, number | undefined, string | undefined] { | ||
let diffFrom: number | undefined; | ||
|
@@ -189,7 +197,7 @@ export class SceneComparePanel extends SceneObjectBase<SceneComparePanelState> { | |
} | ||
|
||
subscribeToEvents() { | ||
return this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { | ||
const switchSub = this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { | ||
// this triggers a timeseries request to the API | ||
// TODO: caching? | ||
(this.state.timeseriesPanel.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).setState({ | ||
|
@@ -199,6 +207,19 @@ export class SceneComparePanel extends SceneObjectBase<SceneComparePanelState> { | |
: TimeRangeWithAnnotationsMode.DEFAULT, | ||
}); | ||
}); | ||
|
||
const timeRangeSub = this.state.$timeRange.subscribeToState((newState, prevState) => { | ||
if (newState.from !== prevState.from || newState.to !== prevState.to) { | ||
this.updateTitle(''); | ||
} | ||
}); | ||
|
||
return { | ||
unsubscribe() { | ||
timeRangeSub.unsubscribe(); | ||
switchSub.unsubscribe(); | ||
}, | ||
}; | ||
} | ||
|
||
buildTimeseriesTitle() { | ||
|
@@ -211,16 +232,76 @@ export class SceneComparePanel extends SceneObjectBase<SceneComparePanelState> { | |
return (this.state.timeseriesPanel.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).useState(); | ||
} | ||
|
||
applyPreset({ from, to, diffFrom, diffTo, label }: Preset) { | ||
this.setDiffRange(diffFrom, diffTo); | ||
|
||
this.state.$timeRange.setState(buildTimeRange(from, to)); | ||
|
||
this.updateTitle(label); | ||
} | ||
|
||
setDiffRange(diffFrom: string, diffTo: string) { | ||
const $diffTimeRange = this.state.timeseriesPanel.state.body.state.$timeRange as SceneTimeRangeWithAnnotations; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: Should pull the state up? Drilling down the state looks a bit cumbersome and makes testing harder do to mocking. Do you think it would make sense / be feasible to pull There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As we discussed face to face: this time range is actually a timerange-like object (a hack) that we use to enable annotations on the time series (see SceneTimeRangeWithAnnotations). So it makes sense that we keep it as close as possible as where it's used (it's also responsible to change the data received from the API on-the-fly, in order to add the annotation on the time series). Also, if in the future the platform provides a way to add range selections out-of-the-box (via a new API or so), it's likely that it's going to be configured on the timeseries panel. Finally, for testing, I wouldn't mock anything ; I'd use end-to-end testing as we do now because these user interactions are quite complex (because they involve many different components). |
||
|
||
$diffTimeRange.setAnnotationTimeRange($diffTimeRange.buildAnnotationTimeRange(diffFrom, diffTo), true); | ||
} | ||
|
||
/** | ||
* This function is responsible for automatically selecting half of the time range (from the time picker) that will be used to build the diff flame graph | ||
* For the baseline panel, the leftmost part, for the comparison one, the rightmost part. | ||
* In the future, we might want to be smarter and provides a way to select (e.g.) the region with the lowest resource consumption on the baseline panel vs | ||
* the region with the highest consumption on the comparison panel. | ||
*/ | ||
autoSelectDiffRange(selectWholeRange: boolean) { | ||
const { $timeRange, target } = this.state; | ||
const { from, to } = $timeRange.state.value; | ||
|
||
if (selectWholeRange) { | ||
this.setDiffRange(from.toISOString(), to.toISOString()); | ||
return; | ||
} | ||
|
||
const diff = to.diff(from); | ||
const half = Math.round(diff / 2); // TODO: cap the max value? | ||
|
||
// we have to create a new instance because add() mutates the original one | ||
const middle = dateTime(from).add(half).toISOString(); | ||
|
||
if (target === CompareTarget.BASELINE) { | ||
this.setDiffRange(from.toISOString(), middle); | ||
} else { | ||
this.setDiffRange(middle, to.toISOString()); | ||
} | ||
} | ||
|
||
updateTitle(label = '') { | ||
const title = this.state.target === CompareTarget.BASELINE ? 'Baseline' : 'Comparison'; | ||
const newTitle = label ? `${title} (${label})` : title; | ||
|
||
this.setState({ title: newTitle }); | ||
} | ||
|
||
public static Component = ({ model }: SceneComponentProps<SceneComparePanel>) => { | ||
const styles = useStyles2(getStyles); | ||
const { target, title, timeseriesPanel: timeseries, timePicker, refreshPicker, filterKey } = model.useState(); | ||
const { | ||
target, | ||
color, | ||
title, | ||
timeseriesPanel: timeseries, | ||
timePicker, | ||
refreshPicker, | ||
filterKey, | ||
} = model.useState(); | ||
const styles = useStyles2(getStyles, color); | ||
|
||
const filtersVariable = sceneGraph.findByKey(model, filterKey) as FiltersVariable; | ||
|
||
return ( | ||
<div className={styles.panel} data-testid={`panel-${target}`}> | ||
<div className={styles.panelHeader}> | ||
<h6>{title}</h6> | ||
<h6> | ||
<div className={styles.colorCircle} /> | ||
{title} | ||
</h6> | ||
|
||
<div className={styles.timePicker}> | ||
<timePicker.Component model={timePicker} /> | ||
|
@@ -238,7 +319,7 @@ export class SceneComparePanel extends SceneObjectBase<SceneComparePanelState> { | |
}; | ||
} | ||
|
||
const getStyles = (theme: GrafanaTheme2) => ({ | ||
const getStyles = (theme: GrafanaTheme2, color: string) => ({ | ||
panel: css` | ||
background-color: ${theme.colors.background.primary}; | ||
padding: ${theme.spacing(1)} ${theme.spacing(1)} 0 ${theme.spacing(1)}; | ||
|
@@ -252,11 +333,20 @@ const getStyles = (theme: GrafanaTheme2) => ({ | |
margin-bottom: ${theme.spacing(2)}; | ||
|
||
& > h6 { | ||
font-size: 15px; | ||
height: 32px; | ||
line-height: 32px; | ||
margin: 0 ${theme.spacing(1)} 0 0; | ||
} | ||
`, | ||
colorCircle: css` | ||
display: inline-block; | ||
background-color: ${color}; | ||
border-radius: 50%; | ||
width: 9px; | ||
height: 9px; | ||
margin-right: 6px; | ||
`, | ||
timePicker: css` | ||
display: flex; | ||
justify-content: flex-end; | ||
|
@@ -272,5 +362,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ | |
& [data-viz-panel-key] > * { | ||
border: 0 none; | ||
} | ||
|
||
& [data-viz-panel-key] [data-testid='uplot-main-div'] { | ||
cursor: crosshair; | ||
} | ||
`, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (non-blocking): Automatically clean up subscriptions
I think in scenes when you use
this._subs.add(...)
it will automatically clean up all subscriptions when a scene is deactivated. Anything added withthis.subscribeToEvent(...)
should also be removed automatically:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this! I always wanted to refactor it ; I'll do it in a future PR.