From 41994a8d55ca50004d41cf9a0dbc93d2c8bc805c Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 6 Aug 2024 18:19:35 +0200 Subject: [PATCH 01/76] feat(Labels): Create new comparison flow --- .../SceneByVariableRepeaterGrid.tsx | 21 +- .../components/SceneLayoutSwitcher.tsx | 2 +- .../components/ScenePanelTypeSwitcher.tsx | 3 +- .../components/SceneQuickFilter.tsx | 2 +- .../SceneExploreServiceFlameGraph.tsx | 2 - .../SceneExploreServiceLabels.tsx | 4 +- .../components/SceneGroupByLabels.tsx | 326 ------------ .../SceneGroupByLabels/SceneGroupByLabels.tsx | 495 ++++++++++++++++++ .../SceneGroupByLabels/ui/CompareActions.tsx | 63 +++ .../components/SceneLabelValuesGrid.tsx | 297 ++++------- .../SceneLabelValuesStatAndTimeseries.tsx | 84 +++ .../ui/ComparePanel.tsx | 92 ++++ .../components/SceneLabelValueStat.tsx | 48 -- .../components/SceneLabelValuesTimeseries.tsx | 43 +- .../SceneProfilesExplorer.tsx | 2 +- .../domain/actions/CompareAction.tsx | 205 -------- .../domain/events/EventSelectForCompare.tsx | 13 + .../GroupByVariable/GroupBySelector.tsx | 8 +- .../infrastructure/labels/labelsRepository.ts | 4 +- 19 files changed, 886 insertions(+), 828 deletions(-) delete mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels.tsx create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries.tsx create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx delete mode 100644 src/pages/ProfilesExplorerView/components/SceneLabelValueStat.tsx delete mode 100644 src/pages/ProfilesExplorerView/domain/actions/CompareAction.tsx create mode 100644 src/pages/ProfilesExplorerView/domain/events/EventSelectForCompare.tsx diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx index db9711ea..4344ceca 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx @@ -22,14 +22,13 @@ import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../helpers/getSceneVariableValue'; import { FavoritesDataSource } from '../../infrastructure/favorites/FavoritesDataSource'; import { SceneLabelValuesBarGauge } from '../SceneLabelValuesBarGauge'; -import { SceneLabelValueStat } from '../SceneLabelValueStat'; import { SceneLabelValuesTimeseries } from '../SceneLabelValuesTimeseries'; import { SceneEmptyState } from './components/SceneEmptyState/SceneEmptyState'; import { SceneErrorState } from './components/SceneErrorState/SceneErrorState'; -import { LayoutType, SceneLayoutSwitcher } from './components/SceneLayoutSwitcher'; -import { SceneNoDataSwitcher } from './components/SceneNoDataSwitcher'; +import { LayoutType, SceneLayoutSwitcher, SceneLayoutSwitcherState } from './components/SceneLayoutSwitcher'; +import { SceneNoDataSwitcher, SceneNoDataSwitcherState } from './components/SceneNoDataSwitcher'; import { PanelType, ScenePanelTypeSwitcher } from './components/ScenePanelTypeSwitcher'; -import { SceneQuickFilter } from './components/SceneQuickFilter'; +import { SceneQuickFilter, SceneQuickFilterState } from './components/SceneQuickFilter'; import { GridItemData } from './types/GridItemData'; interface SceneByVariableRepeaterGridState extends EmbeddedSceneState { @@ -181,7 +180,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { + const onChangeState = (newState: SceneQuickFilterState, prevState?: SceneQuickFilterState) => { if (newState.searchText !== prevState?.searchText) { this.renderGridItems(); } @@ -195,7 +194,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { + const onChangeState = (newState: SceneLayoutSwitcherState, prevState?: SceneLayoutSwitcherState) => { if (newState.layout !== prevState?.layout) { body.setState({ templateColumns: SceneByVariableRepeaterGrid.getGridColumnsTemplate(newState.layout), @@ -219,7 +218,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { + const onChangeState = (newState: SceneNoDataSwitcherState, prevState?: SceneNoDataSwitcherState) => { if (newState.hideNoData !== prevState?.hideNoData) { this.setState({ hideNoData: newState.hideNoData === 'on' }); @@ -311,12 +310,6 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { if (state.data?.state !== LoadingState.Done || state.data.series.length > 0) { return; diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher.tsx index 40d4f1ea..851dac3c 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher.tsx @@ -14,7 +14,7 @@ export enum LayoutType { ROWS = 'rows', } -interface SceneLayoutSwitcherState extends SceneObjectState { +export interface SceneLayoutSwitcherState extends SceneObjectState { layout: LayoutType; onChange?: (layout: LayoutType) => void; } diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher.tsx index 9d626252..4c6eba26 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher.tsx @@ -12,10 +12,9 @@ import React from 'react'; export enum PanelType { TIMESERIES = 'time-series', BARGAUGE = 'bar-gauge', - STATS = 'stats', } -interface ScenePanelTypeSwitcherState extends SceneObjectState { +export interface ScenePanelTypeSwitcherState extends SceneObjectState { panelType: PanelType; onChange?: (panelType: PanelType) => void; } diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneQuickFilter.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneQuickFilter.tsx index b140b9ba..f37fd24f 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneQuickFilter.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneQuickFilter.tsx @@ -10,7 +10,7 @@ import { Icon, IconButton, Input, useStyles2 } from '@grafana/ui'; import { reportInteraction } from '@shared/domain/reportInteraction'; import React from 'react'; -interface SceneQuickFilterState extends SceneObjectState { +export interface SceneQuickFilterState extends SceneObjectState { placeholder: string; searchText: string; onChange?: (searchText: string) => void; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx index 30602272..e0a5e990 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { FavAction } from '../../domain/actions/FavAction'; import { SelectAction } from '../../domain/actions/SelectAction'; import { EventViewServiceLabels } from '../../domain/events/EventViewServiceLabels'; -import { EventViewServiceProfiles } from '../../domain/events/EventViewServiceProfiles'; import { FiltersVariable } from '../../domain/variables/FiltersVariable/FiltersVariable'; import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; @@ -27,7 +26,6 @@ export class SceneExploreServiceFlameGraph extends SceneObjectBase [ - new SelectAction({ EventClass: EventViewServiceProfiles, item }), new SelectAction({ EventClass: EventViewServiceLabels, item }), new FavAction({ item }), ], diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx index 263a20e8..05650044 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx @@ -12,7 +12,6 @@ import React from 'react'; import { SceneMainServiceTimeseries } from '../../components/SceneMainServiceTimeseries'; import { FavAction } from '../../domain/actions/FavAction'; import { SelectAction } from '../../domain/actions/SelectAction'; -import { EventViewServiceFlameGraph } from '../../domain/events/EventViewServiceFlameGraph'; import { EventViewServiceProfiles } from '../../domain/events/EventViewServiceProfiles'; import { FiltersVariable } from '../../domain/variables/FiltersVariable/FiltersVariable'; import { GroupByVariable } from '../../domain/variables/GroupByVariable/GroupByVariable'; @@ -21,7 +20,7 @@ import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable' import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { ScenePanelTypeSwitcher } from '../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; import { GridItemData } from '../SceneByVariableRepeaterGrid/types/GridItemData'; -import { SceneGroupByLabels } from './components/SceneGroupByLabels'; +import { SceneGroupByLabels } from './components/SceneGroupByLabels/SceneGroupByLabels'; interface SceneExploreServiceLabelsState extends EmbeddedSceneState {} @@ -43,7 +42,6 @@ export class SceneExploreServiceLabels extends SceneObjectBase [ new SelectAction({ EventClass: EventViewServiceProfiles, item }), - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), new FavAction({ item }), ], }), diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels.tsx deleted file mode 100644 index 1479e944..00000000 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import { css } from '@emotion/css'; -import { AdHocVariableFilter, GrafanaTheme2 } from '@grafana/data'; -import { SceneComponentProps, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; -import { Stack, useStyles2 } from '@grafana/ui'; -import React from 'react'; - -import { CompareAction } from '../../../domain/actions/CompareAction'; -import { FavAction } from '../../../domain/actions/FavAction'; -import { SelectAction } from '../../../domain/actions/SelectAction'; -import { EventAddLabelToFilters } from '../../../domain/events/EventAddLabelToFilters'; -import { EventExpandPanel } from '../../../domain/events/EventExpandPanel'; -import { EventSelectLabel } from '../../../domain/events/EventSelectLabel'; -import { EventViewServiceFlameGraph } from '../../../domain/events/EventViewServiceFlameGraph'; -import { addFilter } from '../../../domain/variables/FiltersVariable/filters-ops'; -import { FiltersVariable } from '../../../domain/variables/FiltersVariable/FiltersVariable'; -import { GroupByVariable } from '../../../domain/variables/GroupByVariable/GroupByVariable'; -import { findSceneObjectByClass } from '../../../helpers/findSceneObjectByClass'; -import { getSceneVariableValue } from '../../../helpers/getSceneVariableValue'; -import { getProfileMetricLabel } from '../../../infrastructure/series/helpers/getProfileMetricLabel'; -import { SceneNoDataSwitcher } from '../../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; -import { PanelType, ScenePanelTypeSwitcher } from '../../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; -import { SceneQuickFilter } from '../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; -import { SceneByVariableRepeaterGrid } from '../../SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid'; -import { GridItemData } from '../../SceneByVariableRepeaterGrid/types/GridItemData'; -import { SceneDrawer } from '../../SceneDrawer'; -import { SceneLabelValuesBarGauge } from '../../SceneLabelValuesBarGauge'; -import { SceneLabelValuesTimeseries } from '../../SceneLabelValuesTimeseries'; -import { SceneProfilesExplorer } from '../../SceneProfilesExplorer/SceneProfilesExplorer'; -import { SceneLabelValuesGrid } from './SceneLabelValuesGrid'; - -interface SceneGroupByLabelsState extends SceneObjectState { - body?: SceneObject; - drawer: SceneDrawer; -} - -export class SceneGroupByLabels extends SceneObjectBase { - constructor() { - super({ - key: 'group-by-labels', - body: undefined, - drawer: new SceneDrawer(), - }); - - this.addActivationHandler(this.onActivate.bind(this)); - } - - onActivate() { - const groupBySub = this.subscribeToGroupByChange(); - const panelTypeChangeSub = this.subscribeToPanelTypeChange(); - const filtersSub = this.subscribeToFiltersChange(); - const panelEventsSub = this.subscribeToPanelEvents(); - - return () => { - panelTypeChangeSub.unsubscribe(); - filtersSub.unsubscribe(); - panelEventsSub.unsubscribe(); - groupBySub.unsubscribe(); - }; - } - - subscribeToGroupByChange() { - const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; - - const onChangeState = (newState: typeof groupByVariable.state, prevState?: typeof groupByVariable.state) => { - if (newState.value !== prevState?.value) { - const quickFilter = findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter; - quickFilter.clear(); - - if (newState.value === 'all') { - quickFilter.setPlaceholder('Search labels (comma-separated regexes are supported)'); - - this.setState({ - body: this.buildSceneByVariableRepeaterGrid(), - }); - } else { - quickFilter.setPlaceholder('Search label values (comma-separated regexes are supported)'); - - this.setState({ - body: this.buildSceneLabelValuesGrid(), - }); - } - } - }; - - onChangeState(groupByVariable.state); - - return groupByVariable.subscribeToState(onChangeState); - } - - buildSceneByVariableRepeaterGrid() { - return new SceneByVariableRepeaterGrid({ - key: 'service-labels-grid', - variableName: 'groupBy', - mapOptionToItem: (option, index, { serviceName, profileMetricId, panelType }) => { - if (option.value === 'all') { - return null; - } - - // see LabelsDataSource.ts - const { value, groupBy } = JSON.parse(option.value as string); - - return { - index, - value, - // remove the count in parenthesis that exists in option.label - // it'll be set by SceneLabelValuesTimeseries or SceneLabelValuesBarGauge - label: value, - queryRunnerParams: { - serviceName, - profileMetricId, - groupBy, - filters: [], - }, - panelType: panelType as PanelType, - }; - }, - headerActions: (item, items) => { - const { queryRunnerParams } = item; - - if (!queryRunnerParams.groupBy) { - if (items.length > 1) { - return [ - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), - new SelectAction({ EventClass: EventAddLabelToFilters, item }), - new CompareAction({ item }), - new FavAction({ item }), - ]; - } - - return [ - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), - new SelectAction({ EventClass: EventAddLabelToFilters, item }), - new FavAction({ item }), - ]; - } - - // FIXME: should be based on how many series are displayed -> re-render header actions after each data load - if (queryRunnerParams.groupBy.values.length > 1) { - return [ - new SelectAction({ EventClass: EventSelectLabel, item }), - new SelectAction({ EventClass: EventExpandPanel, item }), - new FavAction({ item }), - ]; - } - - return [new FavAction({ item })]; - }, - }); - } - - buildSceneLabelValuesGrid() { - return new SceneLabelValuesGrid({ - key: 'service-label-values-grid', - headerActions: (item, items) => { - const { queryRunnerParams } = item; - - if (!queryRunnerParams.groupBy) { - if (items.length > 1) { - return [ - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), - new SelectAction({ EventClass: EventAddLabelToFilters, item }), - new CompareAction({ item }), - new FavAction({ item }), - ]; - } - - return [ - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), - new SelectAction({ EventClass: EventAddLabelToFilters, item }), - new FavAction({ item }), - ]; - } - - if (queryRunnerParams.groupBy.values.length > 1) { - return [ - new SelectAction({ EventClass: EventSelectLabel, item }), - new SelectAction({ EventClass: EventExpandPanel, item }), - new FavAction({ item }), - ]; - } - - return [new FavAction({ item })]; - }, - }); - } - - subscribeToPanelTypeChange() { - const panelTypeSwitcher = findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher; - - return panelTypeSwitcher.subscribeToState( - (newState: typeof panelTypeSwitcher.state, prevState?: typeof panelTypeSwitcher.state) => { - if (newState.panelType !== prevState?.panelType) { - (this.state.body as SceneByVariableRepeaterGrid | SceneLabelValuesGrid)?.renderGridItems(); - } - } - ); - } - - subscribeToFiltersChange() { - const filtersVariable = findSceneObjectByClass(this, FiltersVariable) as FiltersVariable; - const noDataSwitcher = findSceneObjectByClass(this, SceneNoDataSwitcher) as SceneNoDataSwitcher; - - // the handler will be called each time a filter is added/removed/modified - return filtersVariable.subscribeToState(() => { - if (noDataSwitcher.state.hideNoData === 'on') { - // we force render because the filters only influence the query made in each panel, not the list of items to render (which come from the groupBy options) - (this.state.body as SceneByVariableRepeaterGrid | SceneLabelValuesGrid)?.renderGridItems(true); - } - }); - } - - subscribeToPanelEvents() { - const selectLabelSub = this.subscribeToEvent(EventSelectLabel, (event) => { - this.selectLabel(event.payload.item); - }); - - const addToFiltersSub = this.subscribeToEvent(EventAddLabelToFilters, (event) => { - this.addLabelValueToFilters(event.payload.item); - }); - - const expandPanelSub = this.subscribeToEvent(EventExpandPanel, async (event) => { - this.openExpandedPanelDrawer(event.payload.item); - }); - - return { - unsubscribe() { - expandPanelSub.unsubscribe(); - addToFiltersSub.unsubscribe(); - selectLabelSub.unsubscribe(); - }, - }; - } - - selectLabel({ queryRunnerParams }: GridItemData) { - const labelValue = queryRunnerParams!.groupBy!.label; - const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; - - groupByVariable.changeValueTo(labelValue); - - // the event may be published from an expanded panel in the drawer - this.state.drawer.close(); - } - - addLabelValueToFilters(item: GridItemData) { - const filterByVariable = findSceneObjectByClass(this, FiltersVariable) as FiltersVariable; - - let filterToAdd: AdHocVariableFilter; - const { filters, groupBy } = item.queryRunnerParams; - - if (filters?.[0]) { - filterToAdd = filters?.[0]; - } else if (groupBy?.values.length === 1) { - filterToAdd = { key: groupBy.label, operator: '=', value: groupBy.values[0] }; - } else { - const error = new Error('Cannot build filter! Missing "filters" and "groupBy" value.'); - console.error(error); - console.info(item); - throw error; - } - - addFilter(filterByVariable, filterToAdd); - - const goupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; - goupByVariable.changeValueTo(GroupByVariable.DEFAULT_VALUE); - } - - openExpandedPanelDrawer(item: GridItemData) { - this.state.drawer.open({ - title: this.buildtimeSeriesPanelTitle(item), - body: - item.panelType === PanelType.BARGAUGE - ? new SceneLabelValuesBarGauge({ - item, - headerActions: () => [new SelectAction({ EventClass: EventSelectLabel, item }), new FavAction({ item })], - }) - : new SceneLabelValuesTimeseries({ - displayAllValues: true, - item, - headerActions: () => [new SelectAction({ EventClass: EventSelectLabel, item }), new FavAction({ item })], - }), - }); - } - - buildtimeSeriesPanelTitle(item: GridItemData) { - const serviceName = getSceneVariableValue(this, 'serviceName'); - const profileMetricId = getSceneVariableValue(this, 'profileMetricId'); - return `${serviceName} · ${getProfileMetricLabel(profileMetricId)} · ${item.label}`; - } - - static Component = ({ model }: SceneComponentProps) => { - const styles = useStyles2(getStyles); - - const { body, drawer } = model.useState(); - - const groupByVariable = findSceneObjectByClass(model, GroupByVariable); - const { gridControls } = (findSceneObjectByClass(model, SceneProfilesExplorer) as SceneProfilesExplorer).state; - - return ( -
- - -
- {gridControls.length ? ( - - {gridControls.map((control) => ( - - ))} - - ) : null} -
- - {body && } - {} -
- ); - }; -} - -const getStyles = (theme: GrafanaTheme2) => ({ - container: css` - margin-top: ${theme.spacing(1)}; - `, - sceneControls: css` - margin-bottom: ${theme.spacing(1)}; - `, -}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx new file mode 100644 index 00000000..410559eb --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -0,0 +1,495 @@ +import { css } from '@emotion/css'; +import { AdHocVariableFilter, GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { + MultiValueVariableState, + SceneComponentProps, + sceneGraph, + SceneObject, + SceneObjectBase, + SceneObjectState, + SceneReactObject, +} from '@grafana/scenes'; +import { Stack, useStyles2 } from '@grafana/ui'; +import { reportInteraction } from '@shared/domain/reportInteraction'; +import { buildQuery } from '@shared/domain/url-params/parseQuery'; +import React, { useMemo } from 'react'; +import { Unsubscribable } from 'rxjs'; + +import { computeRoundedTimeRange } from '../../../..//helpers/computeRoundedTimeRange'; +import { interpolateQueryRunnerVariables } from '../../../..//infrastructure/helpers/interpolateQueryRunnerVariables'; +import { FavAction } from '../../../../domain/actions/FavAction'; +import { SelectAction } from '../../../../domain/actions/SelectAction'; +import { EventAddLabelToFilters } from '../../../../domain/events/EventAddLabelToFilters'; +import { EventExpandPanel } from '../../../../domain/events/EventExpandPanel'; +import { EventSelectForCompare } from '../../../../domain/events/EventSelectForCompare'; +import { EventSelectLabel } from '../../../../domain/events/EventSelectLabel'; +import { EventViewServiceFlameGraph } from '../../../../domain/events/EventViewServiceFlameGraph'; +import { addFilter } from '../../../../domain/variables/FiltersVariable/filters-ops'; +import { FiltersVariable } from '../../../../domain/variables/FiltersVariable/FiltersVariable'; +import { GroupByVariable } from '../../../../domain/variables/GroupByVariable/GroupByVariable'; +import { findSceneObjectByClass } from '../../../../helpers/findSceneObjectByClass'; +import { getSceneVariableValue } from '../../../../helpers/getSceneVariableValue'; +import { getProfileMetricLabel } from '../../../../infrastructure/series/helpers/getProfileMetricLabel'; +import { SceneLayoutSwitcher } from '../../../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; +import { SceneNoDataSwitcher } from '../../../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; +import { + PanelType, + ScenePanelTypeSwitcher, + ScenePanelTypeSwitcherState, +} from '../../../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; +import { SceneQuickFilter } from '../../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; +import { SceneByVariableRepeaterGrid } from '../../../SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid'; +import { GridItemData } from '../../../SceneByVariableRepeaterGrid/types/GridItemData'; +import { SceneDrawer } from '../../../SceneDrawer'; +import { SceneLabelValuesBarGauge } from '../../../SceneLabelValuesBarGauge'; +import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries'; +import { SceneProfilesExplorer } from '../../../SceneProfilesExplorer/SceneProfilesExplorer'; +import { GridItemDataWithStats, SceneLabelValuesGrid } from '../SceneLabelValuesGrid'; +import { SceneLabelValuesStatAndTimeseries } from '../SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries'; +import { CompareTarget } from '../SceneLabelValuesStatAndTimeseries/ui/ComparePanel'; +import { CompareActions } from './ui/CompareActions'; + +export interface SceneGroupByLabelsState extends SceneObjectState { + body?: SceneObject; + drawer: SceneDrawer; + compare: Map; + panelTypeChangeSub?: Unsubscribable; +} + +export class SceneGroupByLabels extends SceneObjectBase { + constructor() { + super({ + key: 'group-by-labels', + body: undefined, + drawer: new SceneDrawer(), + compare: new Map(), + panelTypeChangeSub: undefined, + }); + + this.addActivationHandler(this.onActivate.bind(this)); + } + + onActivate() { + const groupBySub = this.subscribeToGroupByChange(); + const filtersSub = this.subscribeToFiltersChange(); + const panelEventsSub = this.subscribeToPanelEvents(); + + return () => { + filtersSub.unsubscribe(); + panelEventsSub.unsubscribe(); + groupBySub.unsubscribe(); + + this.state.panelTypeChangeSub?.unsubscribe(); + }; + } + + subscribeToGroupByChange() { + const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; + let clearQuickFilter = false; // do not clear the filter when the user lands on the page + + const onChangeState = (newState: MultiValueVariableState, prevState?: MultiValueVariableState) => { + if (newState.value === prevState?.value) { + return; + } + + const quickFilter = findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter; + + if (clearQuickFilter) { + quickFilter.clear(); + } + + this.state.panelTypeChangeSub?.unsubscribe(); + + if (newState.value === 'all') { + // we have to resubscribe every time because the subscription is removed every time the ScenePanelTypeSwitcher UI component is unmounted + this.setState({ panelTypeChangeSub: this.subscribeToPanelTypeChange() }); + + this.switchToLabelNamesGrid(); + } else { + this.switchToLabelValuesGrid(newState); + } + }; + + onChangeState(groupByVariable.state); + clearQuickFilter = true; + + return groupByVariable.subscribeToState(onChangeState); + } + + switchToLabelNamesGrid() { + (findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter).setPlaceholder( + 'Search labels (comma-separated regexes are supported)' + ); + + this.setState({ + body: this.buildSceneLabelNamesGrid(), + }); + } + + buildSceneLabelNamesGrid() { + return new SceneByVariableRepeaterGrid({ + key: 'service-labels-grid', + variableName: 'groupBy', + mapOptionToItem: (option, index, { serviceName, profileMetricId, panelType }) => { + if (option.value === 'all') { + return null; + } + + // see LabelsDataSource.ts + const { value, groupBy } = JSON.parse(option.value as string); + + return { + index: index - 1, // the 'all' option has been removed ;) + value, + // remove the count in parenthesis that exists in option.label + // it'll be set by SceneLabelValuesTimeseries or SceneLabelValuesBarGauge + label: value, + queryRunnerParams: { + serviceName, + profileMetricId, + groupBy, + filters: [], + }, + panelType: panelType as PanelType, + }; + }, + headerActions: (item) => { + const { queryRunnerParams } = item; + + if (!queryRunnerParams.groupBy) { + return [ + new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), + new SelectAction({ EventClass: EventAddLabelToFilters, item }), + new FavAction({ item }), + ]; + } + + return [ + new SelectAction({ EventClass: EventSelectLabel, item }), + new SelectAction({ EventClass: EventExpandPanel, item }), + new FavAction({ item }), + ]; + }, + }); + } + + subscribeToPanelTypeChange() { + const panelTypeSwitcher = findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher; + + return panelTypeSwitcher.subscribeToState( + (newState: ScenePanelTypeSwitcherState, prevState?: ScenePanelTypeSwitcherState) => { + if (newState.panelType !== prevState?.panelType) { + (this.state.body as SceneByVariableRepeaterGrid)?.renderGridItems(); + } + } + ); + } + + switchToLabelValuesGrid(newState: MultiValueVariableState) { + (findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter).setPlaceholder( + 'Search label values (comma-separated regexes are supported)' + ); + + const { value, options } = newState; + + const index = options + .filter((o) => o.value !== 'all') + // See LabelsDataSource.ts + .findIndex((o) => JSON.parse(o.value as string).value === value); + const startColorIndex = index > -1 ? index : 0; + + this.setState({ + body: this.buildSceneLabelValuesGrid(newState.value as string, startColorIndex), + }); + + this.clearCompare(); + } + + buildSceneLabelValuesGrid(label: string, startColorIndex: number) { + return new SceneLabelValuesGrid({ + key: 'service-label-values-grid', + startColorIndex, + label, + headerActions: (item) => { + const { queryRunnerParams } = item; + + if (!queryRunnerParams.groupBy) { + return [ + new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), + new SelectAction({ EventClass: EventAddLabelToFilters, item }), + new FavAction({ item }), + ]; + } + + return [ + new SelectAction({ EventClass: EventSelectLabel, item }), + new SelectAction({ EventClass: EventExpandPanel, item }), + new FavAction({ item }), + ]; + }, + }); + } + + subscribeToFiltersChange() { + const filtersVariable = findSceneObjectByClass(this, FiltersVariable) as FiltersVariable; + const noDataSwitcher = findSceneObjectByClass(this, SceneNoDataSwitcher) as SceneNoDataSwitcher; + + // the handler will be called each time a filter is added/removed/modified + return filtersVariable.subscribeToState(() => { + if (noDataSwitcher.state.hideNoData === 'on') { + // we force render because the filters only influence the query made in each panel, not the list of items to render (which come from the groupBy options) + (this.state.body as SceneByVariableRepeaterGrid | SceneLabelValuesGrid)?.renderGridItems(true); + } + }); + } + + subscribeToPanelEvents() { + const selectLabelSub = this.subscribeToEvent(EventSelectLabel, (event) => { + this.selectLabel(event.payload.item); + }); + + const addToFiltersSub = this.subscribeToEvent(EventAddLabelToFilters, (event) => { + this.addLabelValueToFilters(event.payload.item); + }); + + const expandPanelSub = this.subscribeToEvent(EventExpandPanel, async (event) => { + this.openExpandedPanelDrawer(event.payload.item); + }); + + const selectForCompareSub = this.subscribeToEvent(EventSelectForCompare, (event) => { + const { compareTarget, item } = event.payload; + this.selectForCompare(compareTarget, item); + }); + + return { + unsubscribe() { + selectForCompareSub.unsubscribe(); + expandPanelSub.unsubscribe(); + addToFiltersSub.unsubscribe(); + selectLabelSub.unsubscribe(); + }, + }; + } + + selectLabel({ queryRunnerParams }: GridItemData) { + const labelValue = queryRunnerParams!.groupBy!.label; + const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; + + groupByVariable.changeValueTo(labelValue); + + // the event may be published from an expanded panel in the drawer + this.state.drawer.close(); + } + + addLabelValueToFilters(item: GridItemData) { + const filterByVariable = findSceneObjectByClass(this, FiltersVariable) as FiltersVariable; + + let filterToAdd: AdHocVariableFilter; + const { filters, groupBy } = item.queryRunnerParams; + + if (filters?.[0]) { + filterToAdd = filters?.[0]; + } else if (groupBy?.values.length === 1) { + filterToAdd = { key: groupBy.label, operator: '=', value: groupBy.values[0] }; + } else { + const error = new Error('Cannot build filter! Missing "filters" and "groupBy" value.'); + console.error(error); + console.info(item); + throw error; + } + + addFilter(filterByVariable, filterToAdd); + } + + openExpandedPanelDrawer(item: GridItemData) { + const serviceName = getSceneVariableValue(this, 'serviceName'); + const profileMetricId = getSceneVariableValue(this, 'profileMetricId'); + + this.state.drawer.open({ + title: `${serviceName} · ${getProfileMetricLabel(profileMetricId)} · ${item.label}`, + body: + item.panelType === PanelType.BARGAUGE + ? new SceneLabelValuesBarGauge({ + item, + headerActions: () => [new SelectAction({ EventClass: EventSelectLabel, item }), new FavAction({ item })], + }) + : new SceneLabelValuesTimeseries({ + displayAllValues: true, + item, + headerActions: () => [new SelectAction({ EventClass: EventSelectLabel, item }), new FavAction({ item })], + }), + }); + } + + selectForCompare(compareTarget: CompareTarget, item: GridItemDataWithStats) { + const compare = new Map(this.state.compare); + + const otherTarget = compareTarget === CompareTarget.BASELINE ? CompareTarget.COMPARISON : CompareTarget.BASELINE; + + if (compare.get(otherTarget)?.value === item.value) { + compare.delete(otherTarget); + } + + compare.set(compareTarget, item); + + this.setState({ compare }); + + this.updateComparePanels(); + } + + updateComparePanels() { + const { compare } = this.state; + const baselineItem = compare.get(CompareTarget.BASELINE); + const comparisonItem = compare.get(CompareTarget.COMPARISON); + + const comparePanels = sceneGraph.findAllObjects(this, (o) => o instanceof SceneReactObject) as SceneReactObject[]; + + let baselineItemFound = false; + let comparisonItemFound = false; + + for (const panel of comparePanels) { + let compareTargetValue = undefined; + const { key, props } = panel.state; + + if ( + !baselineItemFound && + baselineItem && + key === SceneLabelValuesStatAndTimeseries.buildComparePanelKey(baselineItem) + ) { + compareTargetValue = CompareTarget.BASELINE; + baselineItemFound = true; + } else if ( + !comparisonItemFound && + comparisonItem && + key === SceneLabelValuesStatAndTimeseries.buildComparePanelKey(comparisonItem) + ) { + compareTargetValue = CompareTarget.COMPARISON; + comparisonItemFound = true; + } + + panel.setState({ props: { ...props, compareTargetValue } }); + } + } + + getCompare() { + return this.state.compare; + } + + clearCompare() { + this.setState({ compare: new Map() }); + } + + buildDiffUrl(): string { + const { compare } = this.state; + const baselineItem = compare.get(CompareTarget.BASELINE); + const comparisonItem = compare.get(CompareTarget.COMPARISON); + + let { appUrl } = config; + if (appUrl.at(-1) !== '/') { + // ensures that the API pathname is appended correctly (appUrl seems to always have it but better to be extra careful) + appUrl += '/'; + } + + const diffUrl = new URL('a/grafana-pyroscope-app/comparison-diff', appUrl); + + // data source + diffUrl.searchParams.set('var-dataSource', getSceneVariableValue(this, 'dataSource')); + + // time range + const { from, to } = computeRoundedTimeRange(sceneGraph.getTimeRange(this).state.value); + diffUrl.searchParams.set('from', from.toString()); + diffUrl.searchParams.set('to', to.toString()); + + const baselineQueryRunnerParams = interpolateQueryRunnerVariables(this, baselineItem as GridItemData); + const comparisonQueryRunnerParams = interpolateQueryRunnerVariables(this, comparisonItem as GridItemData); + + // // query - just in case + const query = buildQuery({ + serviceId: baselineQueryRunnerParams.serviceName, + profileMetricId: baselineQueryRunnerParams.profileMetricId, + labels: baselineQueryRunnerParams.filters.map(({ key, operator, value }) => `${key}${operator}"${value}"`), + }); + diffUrl.searchParams.set('query', query); + + // left & right queries + const [leftQuery, rightQuery] = [baselineQueryRunnerParams, comparisonQueryRunnerParams].map( + ({ serviceName: serviceId, profileMetricId, filters }) => + buildQuery({ + serviceId, + profileMetricId, + labels: filters.map(({ key, operator, value }) => `${key}${operator}"${value}"`), + }) + ); + + diffUrl.searchParams.set('leftQuery', leftQuery); + diffUrl.searchParams.set('rightQuery', rightQuery); + + return diffUrl.toString(); + } + + onClickCompareButton = () => { + window.open(this.buildDiffUrl(), '_blank'); + + reportInteraction('g_pyroscope_app_compare_link_clicked'); + }; + + onClickClearCompareButton = () => { + this.clearCompare(); + this.updateComparePanels(); + }; + + static Component = ({ model }: SceneComponentProps) => { + const styles = useStyles2(getStyles); + + const { body, drawer, compare } = model.useState(); + + const groupByVariable = findSceneObjectByClass(model, GroupByVariable) as GroupByVariable; + const { value: groupByVariableValue } = groupByVariable.useState(); + + const gridControls = useMemo( + () => + groupByVariableValue === 'all' + ? (findSceneObjectByClass(model, SceneProfilesExplorer) as SceneProfilesExplorer).state.gridControls + : ([ + findSceneObjectByClass(model, SceneQuickFilter), + findSceneObjectByClass(model, SceneLayoutSwitcher), + ] as SceneObject[]), + [groupByVariableValue, model] + ); + + return ( +
+ + +
+ + {groupByVariableValue !== 'all' && ( + + )} + + {gridControls.map((control) => ( + + ))} + +
+ + {body && } + {} +
+ ); + }; +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css` + margin-top: ${theme.spacing(1)}; + `, + sceneControls: css` + margin-bottom: ${theme.spacing(1)}; + `, +}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx new file mode 100644 index 00000000..280edd2f --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx @@ -0,0 +1,63 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Button, IconButton, useStyles2 } from '@grafana/ui'; +import { noOp } from '@shared/domain/noOp'; +import React from 'react'; + +import { WIDTH_COMPARE_PANEL } from '../../SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries'; +import { CompareTarget } from '../../SceneLabelValuesStatAndTimeseries/ui/ComparePanel'; +import { SceneGroupByLabelsState } from '../SceneGroupByLabels'; + +type CompareButtonProps = { + compare: SceneGroupByLabelsState['compare']; + onClickCompare: () => void; + onClickClear: () => void; +}; + +export function CompareActions({ compare, onClickCompare, onClickClear }: CompareButtonProps) { + const styles = useStyles2(getStyles); + const disabled = compare.size < 2; + const hasSelection = compare.size > 0; + + return ( +
+ + + +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css` + display: flex; + align-items: center; + justify-content: space-between; + width: ${WIDTH_COMPARE_PANEL}; + gap: ${theme.spacing(1)}; + `, + compareButton: css` + flex-grow: 1; + `, + clearButton: css``, +}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx index ce79764c..c0b25c59 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx @@ -1,48 +1,51 @@ -import { DashboardCursorSync, LoadingState, VariableRefresh } from '@grafana/data'; +import { DashboardCursorSync, DataFrame, LoadingState } from '@grafana/data'; import { behaviors, EmbeddedSceneState, - QueryVariable, SceneComponentProps, SceneCSSGridItem, SceneCSSGridLayout, - sceneGraph, + SceneDataProvider, + SceneDataTransformer, SceneObjectBase, - SceneQueryRunner, VizPanelState, } from '@grafana/scenes'; import { Spinner } from '@grafana/ui'; -import { noOp } from '@shared/domain/noOp'; import { debounce, isEqual } from 'lodash'; import React from 'react'; import { FavAction } from '../../../domain/actions/FavAction'; import { findSceneObjectByClass } from '../../../helpers/findSceneObjectByClass'; -import { getSceneVariableValue } from '../../../helpers/getSceneVariableValue'; import { FavoritesDataSource } from '../../../infrastructure/favorites/FavoritesDataSource'; +import { buildTimeSeriesQueryRunner } from '../../../infrastructure/timeseries/buildTimeSeriesQueryRunner'; import { SceneEmptyState } from '../../SceneByVariableRepeaterGrid/components/SceneEmptyState/SceneEmptyState'; import { SceneErrorState } from '../../SceneByVariableRepeaterGrid/components/SceneErrorState/SceneErrorState'; -import { LayoutType, SceneLayoutSwitcher } from '../../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; -import { SceneNoDataSwitcher } from '../../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; -import { PanelType, ScenePanelTypeSwitcher } from '../../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; -import { SceneQuickFilter } from '../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; +import { + LayoutType, + SceneLayoutSwitcher, + SceneLayoutSwitcherState, +} from '../../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; +import { SceneQuickFilter, SceneQuickFilterState } from '../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; +import { addRefId, addStats, sortSeries } from '../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; import { GridItemData } from '../../SceneByVariableRepeaterGrid/types/GridItemData'; -import { SceneLabelValuesBarGauge } from '../../SceneLabelValuesBarGauge'; -import { SceneLabelValueStat } from '../../SceneLabelValueStat'; -import { SceneLabelValuesTimeseries } from '../../SceneLabelValuesTimeseries'; +import { SceneGroupByLabels } from './SceneGroupByLabels/SceneGroupByLabels'; +import { SceneLabelValuesStatAndTimeseries } from './SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries'; +import { CompareTarget } from './SceneLabelValuesStatAndTimeseries/ui/ComparePanel'; + +export type GridItemDataWithStats = GridItemData & { stats: Record }; interface SceneLabelValuesGridState extends EmbeddedSceneState { - items: GridItemData[]; + $data: SceneDataProvider; + items: GridItemDataWithStats[]; + label: string; + startColorIndex: number; headerActions: (item: GridItemData, items: GridItemData[]) => VizPanelState['headerActions']; sortItemsFn: (a: GridItemData, b: GridItemData) => number; - hideNoData: boolean; } -const GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(400px, 1fr))'; - +const GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(800px, 1fr))'; const GRID_TEMPLATE_ROWS = '1fr'; -const GRID_AUTO_ROWS = '240px'; -const GRID_AUTO_ROWS_SMALL = '76px'; +export const GRID_AUTO_ROWS = '160px'; const DEFAULT_SORT_ITEMS_FN: SceneLabelValuesGridState['sortItemsFn'] = function (a, b) { const aIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(a)); @@ -64,31 +67,30 @@ const DEFAULT_SORT_ITEMS_FN: SceneLabelValuesGridState['sortItemsFn'] = function }; export class SceneLabelValuesGrid extends SceneObjectBase { - static buildGridItemKey(item: GridItemData) { - return `grid-item-${item.index}-${item.value}`; - } - - static getGridColumnsTemplate(layout: LayoutType) { - return layout === LayoutType.ROWS ? GRID_TEMPLATE_ROWS : GRID_TEMPLATE_COLUMNS; - } - constructor({ key, + label, + startColorIndex, headerActions, - sortItemsFn, }: { key: string; + label: SceneLabelValuesGridState['label']; + startColorIndex: SceneLabelValuesGridState['startColorIndex']; headerActions: SceneLabelValuesGridState['headerActions']; - sortItemsFn?: SceneLabelValuesGridState['sortItemsFn']; }) { super({ key, + label, + startColorIndex, items: [], + $data: new SceneDataTransformer({ + $data: buildTimeSeriesQueryRunner({ groupBy: { label } }), + transformations: [addRefId, addStats, sortSeries], + }), headerActions, - sortItemsFn: sortItemsFn || DEFAULT_SORT_ITEMS_FN, - hideNoData: false, + sortItemsFn: DEFAULT_SORT_ITEMS_FN, body: new SceneCSSGridLayout({ - templateColumns: SceneLabelValuesGrid.getGridColumnsTemplate(SceneLayoutSwitcher.DEFAULT_LAYOUT), + templateColumns: GRID_TEMPLATE_ROWS, autoRows: GRID_AUTO_ROWS, isLazy: true, $behaviors: [ @@ -104,75 +106,30 @@ export class SceneLabelValuesGrid extends SceneObjectBase void }; - - const variableSub = variable.subscribeToState((newState, prevState) => { - if (!newState.loading && prevState.loading) { - this.renderGridItems(); - } - }); - - // the "groupBy" variable data source will not fetch values if the variable is inactive - // (see src/pages/ProfilesExplorerView/data/labels/LabelsDataSource.ts) - // so we force an update here to be sure we have the latest values - variable.update(); - - const refreshSub = this.subscribeToRefreshClick(); + const dataSub = this.subscribeToDataChange(); const quickFilterSub = this.subscribeToQuickFilterChange(); const layoutChangeSub = this.subscribeToLayoutChange(); - const hideNoDataSub = this.subscribeToHideNoDataChange(); return () => { - hideNoDataSub.unsubscribe(); layoutChangeSub.unsubscribe(); quickFilterSub.unsubscribe(); - refreshSub.unsubscribe(); - - variableSub.unsubscribe(); + dataSub.unsubscribe(); }; } - subscribeToRefreshClick() { - const variable = sceneGraph.lookupVariable('groupBy', this) as QueryVariable & { update: () => void }; - const originalRefresh = variable.state.refresh; - - variable.setState({ refresh: VariableRefresh.never }); - - const onClickRefresh = () => { - variable.update(); - }; - - // start of hack, for a better UX: we disable the variable "refresh" option and we allow the user to reload the list only by clicking on the "Refresh" button - // if we don't do this, every time the time range changes (even with auto-refresh on), - // all the timeseries present on the screen would be re-created, resulting in blinking and a poor UX - const refreshButton = document.querySelector( - '[data-testid="data-testid RefreshPicker run button"]' - ) as HTMLButtonElement; - - if (!refreshButton) { - console.error('SceneLabelValuesGrid: Refresh button not found! The list of items will never be updated.'); - } - - refreshButton?.addEventListener('click', onClickRefresh); - refreshButton?.setAttribute('title', 'Click to completely refresh all the panels present on the screen'); - // end of hack - - return { - unsubscribe() { - refreshButton?.removeAttribute('title'); - refreshButton?.removeEventListener('click', onClickRefresh); - variable.setState({ refresh: originalRefresh }); - }, - }; + subscribeToDataChange() { + return this.state.$data.subscribeToState((newState) => { + if (newState.data?.state !== LoadingState.Loading) { + this.renderGridItems(); + } + }); } subscribeToQuickFilterChange() { const quickFilter = findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter; - const onChangeState = (newState: typeof quickFilter.state, prevState?: typeof quickFilter.state) => { + const onChangeState = (newState: SceneQuickFilterState, prevState?: SceneQuickFilterState) => { if (newState.searchText !== prevState?.searchText) { this.renderGridItems(); } @@ -183,13 +140,12 @@ export class SceneLabelValuesGrid extends SceneObjectBase { + const onChangeState = (newState: SceneLayoutSwitcherState, prevState?: SceneLayoutSwitcherState) => { if (newState.layout !== prevState?.layout) { body.setState({ - templateColumns: SceneLabelValuesGrid.getGridColumnsTemplate(newState.layout), + templateColumns: newState.layout === LayoutType.ROWS ? GRID_TEMPLATE_ROWS : GRID_TEMPLATE_COLUMNS, }); } }; @@ -199,90 +155,58 @@ export class SceneLabelValuesGrid extends SceneObjectBase { - if (newState.hideNoData !== prevState?.hideNoData) { - this.setState({ hideNoData: newState.hideNoData === 'on' }); - - // we force render because this.state.items certainly have not changed but we want to update the UI panels anyway - this.renderGridItems(true); - } - }; - - onChangeState(noDataSwitcher.state); - - return noDataSwitcher.subscribeToState(onChangeState); + return !isEqual(items, newItems); } - buildItemsData(variable: QueryVariable) { - const { value: variableValue, options } = variable.state; - - const currentOption = options - .filter((o) => o.value !== 'all') - .find((o) => variableValue === JSON.parse(o.value as string).value); + buildItemsData(series: DataFrame[]) { + const { label, startColorIndex } = this.state; - if (!currentOption) { - console.error('Cannot find the "%s" groupBy value among all the variable option!', variableValue); - return []; - } - - const serviceName = getSceneVariableValue(this, 'serviceName'); - const profileMetricId = getSceneVariableValue(this, 'profileMetricId'); - const panelTypeFromSwitcher = (findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher).state - .panelType; + const items = series.map(({ fields, meta }, index) => { + const labelFromSerieLabels = fields[1].labels?.[label]; + const labelFromSerieName = fields[1].name; + const labelValue = labelFromSerieLabels || labelFromSerieName; - const panelType = panelTypeFromSwitcher === PanelType.BARGAUGE ? PanelType.STATS : panelTypeFromSwitcher; + const allValuesSum = meta?.stats?.find(({ displayName }) => displayName === 'allValuesSum')?.value || 0; + const { unit } = fields[1].config; - const items = JSON.parse(currentOption.value as string).groupBy.values.map((value: string, index: number) => { return { - index, - value: value, - label: value, + index: startColorIndex + index, + value: labelValue, + label: labelValue, queryRunnerParams: { - serviceName, - profileMetricId, - filters: [{ key: variableValue, operator: '=', value }], + filters: [{ key: label, operator: '=', value: labelFromSerieLabels || '' }], + }, + stats: { + allValuesSum, + unit, }, - panelType, }; }); return this.filterItems(items).sort(this.state.sortItemsFn); } - shouldRenderItems(newItems: SceneLabelValuesGridState['items']) { - const { items } = this.state; - - if (!newItems.length || items.length !== newItems.length) { - return true; - } - - return !isEqual(items, newItems); - } - renderGridItems(forceRender = false) { - const variable = sceneGraph.lookupVariable('groupBy', this) as QueryVariable; + const { state: loadingState, series, errors } = this.state.$data.state.data!; - if (variable.state.loading) { + if (loadingState === LoadingState.Loading) { return; } - if (variable.state.error) { - this.renderErrorState(variable.state.error); + if (loadingState === LoadingState.Error) { + // TODO: check + this.renderErrorState(errors?.[0] as Error); return; } - const newItems = this.buildItemsData(variable); + const newItems = this.buildItemsData(series); if (!forceRender && !this.shouldRenderItems(newItems)) { return; @@ -295,77 +219,36 @@ export class SceneLabelValuesGrid extends SceneObjectBase { - const vizPanel = this.buildVizPanel(item); + const compare = (findSceneObjectByClass(this, SceneGroupByLabels) as SceneGroupByLabels).getCompare(); - if (this.state.hideNoData) { - this.setupHideNoData(vizPanel); - } + const gridItems = newItems.map((item) => { + const vizPanel = new SceneLabelValuesStatAndTimeseries({ + item, + headerActions: this.state.headerActions.bind(null, item, this.state.items), + compareTargetValue: this.getItemCompareTargetValue(item, compare), + }); return new SceneCSSGridItem({ - key: SceneLabelValuesGrid.buildGridItemKey(item), body: vizPanel, }); }); (this.state.body as SceneCSSGridLayout).setState({ - autoRows: this.getAutoRows(), // required to have the correct grid items height + autoRows: GRID_AUTO_ROWS, // required to have the correct grid items height children: gridItems, }); } - buildVizPanel(item: GridItemData) { - switch (item.panelType) { - case PanelType.BARGAUGE: - return new SceneLabelValuesBarGauge({ - item, - headerActions: this.state.headerActions.bind(null, item, this.state.items), - }); - - case PanelType.STATS: - return new SceneLabelValueStat({ - item, - headerActions: this.state.headerActions.bind(null, item, this.state.items), - }); - - case PanelType.TIMESERIES: - default: - return new SceneLabelValuesTimeseries({ - item, - headerActions: this.state.headerActions.bind(null, item, this.state.items), - }); + getItemCompareTargetValue(item: GridItemDataWithStats, compare: Map) { + if (compare.get(CompareTarget.BASELINE)?.value === item.value) { + return CompareTarget.BASELINE; } - } - - setupHideNoData(vizPanel: SceneLabelValuesTimeseries | SceneLabelValuesBarGauge | SceneLabelValueStat) { - const sub = (vizPanel.state.body.state.$data as SceneQueryRunner)!.subscribeToState((state) => { - if (state.data?.state !== LoadingState.Done || state.data.series.length > 0) { - return; - } - const gridItem = sceneGraph.getAncestor(vizPanel, SceneCSSGridItem); - const { key: gridItemKey } = gridItem.state; - const grid = sceneGraph.getAncestor(gridItem, SceneCSSGridLayout); - - const filteredChildren = grid.state.children.filter((c) => c.state.key !== gridItemKey); - - if (!filteredChildren.length) { - this.renderEmptyState(); - } else { - grid.setState({ children: filteredChildren }); - } - }); - - vizPanel.addActivationHandler(() => { - return () => { - sub.unsubscribe(); - }; - }); - } + if (compare.get(CompareTarget.COMPARISON)?.value === item.value) { + return CompareTarget.COMPARISON; + } - getAutoRows() { - const { panelType } = (findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher).state; - return panelType === PanelType.BARGAUGE ? GRID_AUTO_ROWS_SMALL : GRID_AUTO_ROWS; + return undefined; } filterItems(items: SceneLabelValuesGridState['items']) { @@ -423,9 +306,9 @@ export class SceneLabelValuesGrid extends SceneObjectBase) { - const { body } = model.useState(); - const { loading } = (sceneGraph.lookupVariable('groupBy', model) as QueryVariable)?.useState(); + const { body, $data } = model.useState(); + const $dataState = $data?.useState(); - return loading ? : ; + return $dataState.data?.state === LoadingState.Loading ? : ; } } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries.tsx new file mode 100644 index 00000000..367c8dfd --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries.tsx @@ -0,0 +1,84 @@ +import { + SceneComponentProps, + SceneFlexItem, + SceneFlexLayout, + SceneObjectBase, + SceneObjectState, + SceneReactObject, + VizPanelState, +} from '@grafana/scenes'; +import React from 'react'; + +import { EventSelectForCompare } from '../../../../domain/events/EventSelectForCompare'; +import { GridItemData } from '../../../SceneByVariableRepeaterGrid/types/GridItemData'; +import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries'; +import { GRID_AUTO_ROWS, GridItemDataWithStats } from '../SceneLabelValuesGrid'; +import { ComparePanel, CompareTarget } from './ui/ComparePanel'; + +interface SceneLabelValuesStatAndTimeseriesState extends SceneObjectState { + body: SceneFlexLayout; +} + +export const WIDTH_COMPARE_PANEL = '180px'; + +export class SceneLabelValuesStatAndTimeseries extends SceneObjectBase { + static buildComparePanelKey(item: GridItemDataWithStats) { + return `compare-panel-${item.value}`; + } + + constructor(options: { + item: GridItemDataWithStats; + headerActions: (item: GridItemData) => VizPanelState['headerActions']; + compareTargetValue?: CompareTarget; + }) { + super({ + key: 'stat-and-timeseries-label-values', + body: new SceneFlexLayout({ + direction: 'row', + minHeight: GRID_AUTO_ROWS, + children: [], + }), + }); + + this.addActivationHandler(this.onActivate.bind(this, options)); + } + + onActivate({ + item, + headerActions, + compareTargetValue, + }: { + item: GridItemDataWithStats; + headerActions: (item: GridItemData) => VizPanelState['headerActions']; + compareTargetValue?: CompareTarget; + }) { + this.state.body.setState({ + minHeight: GRID_AUTO_ROWS, + children: [ + new SceneFlexItem({ + width: WIDTH_COMPARE_PANEL, + body: new SceneReactObject({ + key: SceneLabelValuesStatAndTimeseries.buildComparePanelKey(item), + component: ComparePanel, + props: { + item, + onChangeCompareTarget: (compareTarget: CompareTarget, item: GridItemDataWithStats) => { + this.publishEvent(new EventSelectForCompare({ compareTarget, item }), true); + }, + compareTargetValue, + }, + }), + }), + new SceneFlexItem({ + body: new SceneLabelValuesTimeseries({ item, headerActions }), + }), + ], + }); + } + + static Component({ model }: SceneComponentProps) { + const { body } = model.useState(); + + return ; + } +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx new file mode 100644 index 00000000..1a93594e --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx @@ -0,0 +1,92 @@ +import { css } from '@emotion/css'; +import { getValueFormat, GrafanaTheme2 } from '@grafana/data'; +import { RadioButtonGroup, useStyles2 } from '@grafana/ui'; +import React, { useMemo } from 'react'; + +import { getColorByIndex } from '../../../../../helpers/getColorByIndex'; +import { GridItemDataWithStats } from '../../SceneLabelValuesGrid'; + +export type ComparePanelProps = { + item: GridItemDataWithStats; + onChangeCompareTarget: (compareTarget: CompareTarget, item: GridItemDataWithStats) => void; + compareTargetValue?: CompareTarget; +}; + +export enum CompareTarget { + BASELINE = 'baseline', + COMPARISON = 'comparison', +} + +const OPTIONS = [ + { + label: 'Baseline', + value: CompareTarget.BASELINE, + description: 'Click to select this timeseries as baseline for comparison', + }, + { + label: 'Comparison', + value: CompareTarget.COMPARISON, + description: 'Click to select this timeseries as target for comparison', + }, +]; + +export function ComparePanel({ item, onChangeCompareTarget, compareTargetValue }: ComparePanelProps) { + const styles = useStyles2(getStyles); + const { allValuesSum, unit } = item.stats; + + const total = useMemo(() => { + const formattedValue = getValueFormat(unit)(allValuesSum); + return `${formattedValue.text}${formattedValue.suffix}`; + }, [allValuesSum, unit]); + + const color = getColorByIndex(item.index); + + return ( +
+

+ {total} +

+ +
+ { + onChangeCompareTarget(newValue as CompareTarget, item); + }} + value={compareTargetValue} + /> +
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css` + display: flex; + flex-direction: column; + justify-content: space-between; + border: 1px solid ${theme.colors.border.medium}; + padding: ${theme.spacing(1)}; + width: 100%; + `, + title: css` + font-size: 24px; + width: 100%; + text-align: center; + margin-top: ${theme.spacing(5)}; + `, + radioButtonsGroup: css` + width: 100%; + + & > * { + flex-grow: 1 !important; + } + + & :checked + label { + color: #fff; + background-color: ${theme.colors.primary.main}; + } + `, +}); diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValueStat.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValueStat.tsx deleted file mode 100644 index c7cea5a6..00000000 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValueStat.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - PanelBuilders, - SceneComponentProps, - SceneObjectBase, - SceneObjectState, - VizPanel, - VizPanelState, -} from '@grafana/scenes'; -import { BigValueGraphMode, BigValueTextMode } from '@grafana/ui'; -import React from 'react'; - -import { getColorByIndex } from '../helpers/getColorByIndex'; -import { buildTimeSeriesQueryRunner } from '../infrastructure/timeseries/buildTimeSeriesQueryRunner'; -import { GridItemData } from './SceneByVariableRepeaterGrid/types/GridItemData'; - -interface SceneLabelValueStatState extends SceneObjectState { - body: VizPanel; -} - -export class SceneLabelValueStat extends SceneObjectBase { - constructor({ - item, - headerActions, - }: { - item: GridItemData; - headerActions: (item: GridItemData) => VizPanelState['headerActions']; - }) { - super({ - key: 'stat-label-value', - body: PanelBuilders.stat() - .setTitle(item.label) - .setDescription('This panel displays aggregate values over the current time period') - .setData(buildTimeSeriesQueryRunner(item.queryRunnerParams)) - .setHeaderActions(headerActions(item)) - .setOption('reduceOptions', { values: false, calcs: ['sum'] }) - .setColor({ mode: 'fixed', fixedColor: getColorByIndex(item.index) }) - .setOption('graphMode', BigValueGraphMode.None) - .setOption('textMode', BigValueTextMode.Value) - .build(), - }); - } - - static Component({ model }: SceneComponentProps) { - const { body } = model.useState(); - - return ; - } -} diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx index aa377dba..fb442fd4 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx @@ -23,6 +23,9 @@ import { import { GridItemData } from './SceneByVariableRepeaterGrid/types/GridItemData'; interface SceneLabelValuesTimeseriesState extends SceneObjectState { + item: GridItemData; + headerActions: (item: GridItemData) => VizPanelState['headerActions']; + displayAllValues: boolean; body: VizPanel; } @@ -32,12 +35,15 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase VizPanelState['headerActions']; - displayAllValues?: boolean; + item: SceneLabelValuesTimeseriesState['item']; + headerActions: SceneLabelValuesTimeseriesState['headerActions']; + displayAllValues?: SceneLabelValuesTimeseriesState['displayAllValues']; }) { super({ key: 'timeseries-label-values', + item, + headerActions, + displayAllValues: Boolean(displayAllValues), body: PanelBuilders.timeseries() .setTitle(item.label) .setData( @@ -52,10 +58,10 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { @@ -63,9 +69,9 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { - let displayName = groupByLabel ? serie.fields[1].labels?.[groupByLabel] : serie.fields[1].name; + let displayName = serie.fields[1].labels?.[groupByLabel as string] || serie.fields[1].name; if (series.length === 1) { const allValuesSum = serie.meta?.stats?.find(({ displayName }) => displayName === 'allValuesSum')?.value || 0; const { unit } = serie.fields[1].config; const formattedValue = getValueFormat(unit)(allValuesSum); - displayName = `${displayName} · total = ${formattedValue.text}${formattedValue.suffix}`; + displayName = `${displayName} / total = ${formattedValue.text}${formattedValue.suffix}`; } return { @@ -151,6 +159,11 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase serie.fields[1].labels?.[groupByLabel as string] || serie.fields[1].name); + } + updateTitle(newTitle: string) { this.state.body.setState({ title: newTitle }); } diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 1b9fbf31..f235f81c 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -380,7 +380,7 @@ export class SceneProfilesExplorer extends SceneObjectBase ( - + ))} diff --git a/src/pages/ProfilesExplorerView/domain/actions/CompareAction.tsx b/src/pages/ProfilesExplorerView/domain/actions/CompareAction.tsx deleted file mode 100644 index 86f963b2..00000000 --- a/src/pages/ProfilesExplorerView/domain/actions/CompareAction.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { css } from '@emotion/css'; -import { config } from '@grafana/runtime'; -import { - SceneComponentProps, - SceneCSSGridLayout, - sceneGraph, - SceneObjectBase, - SceneObjectState, - VariableDependencyConfig, -} from '@grafana/scenes'; -import { Checkbox, LinkButton, Tooltip, useStyles2 } from '@grafana/ui'; -import { reportInteraction } from '@shared/domain/reportInteraction'; -import { buildQuery } from '@shared/domain/url-params/parseQuery'; -import { uniq } from 'lodash'; -import React, { useMemo } from 'react'; - -import { GridItemData } from '../../components/SceneByVariableRepeaterGrid/types/GridItemData'; -import { computeRoundedTimeRange } from '../../helpers/computeRoundedTimeRange'; -import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; -import { getSceneVariableValue } from '../../helpers/getSceneVariableValue'; -import { interpolateQueryRunnerVariables } from '../../infrastructure/helpers/interpolateQueryRunnerVariables'; -import { FiltersVariable } from '../variables/FiltersVariable/FiltersVariable'; - -interface CompareActionState extends SceneObjectState { - item: GridItemData; - isChecked: boolean; - isDisabled: boolean; - isEnabled: boolean; - diffUrl: string; -} - -export class CompareAction extends SceneObjectBase { - protected _variableDependency: VariableDependencyConfig = new VariableDependencyConfig(this, { - variableNames: ['filters'], - onReferencedVariableValueChanged: () => { - if (!this.state.isChecked) { - return; - } - - const { otherCheckedAction } = this.findCompareActions(); - - if (otherCheckedAction?.state.isChecked) { - const diffUrl = this.buildDiffUrl(otherCheckedAction.state.item); - - this.setState({ diffUrl }); - otherCheckedAction.setState({ diffUrl }); - } - }, - }); - - constructor({ item }: { item: CompareActionState['item'] }) { - super({ - item, - isChecked: false, - isDisabled: false, - isEnabled: false, - diffUrl: '', - }); - } - - public onChange = () => { - let { isChecked } = this.state; - - isChecked = !isChecked; - - this.setState({ isChecked }); - - const { otherCheckedAction, allOtherActions } = this.findCompareActions(); - - const isEnabled = isChecked && Boolean(otherCheckedAction); - - const newState = { - diffUrl: isEnabled ? this.buildDiffUrl(otherCheckedAction!.state.item) : '', - isEnabled, - isDisabled: false, - }; - - this.setState(newState); - otherCheckedAction?.setState(newState); - - allOtherActions.forEach((action) => action.setState({ isDisabled: isEnabled })); - }; - - findCompareActions() { - let otherCheckedAction: CompareAction | undefined; - - const allOtherActions = sceneGraph.findAllObjects(sceneGraph.getAncestor(this, SceneCSSGridLayout), (o) => { - if (!(o instanceof CompareAction) || o === this) { - return false; - } - - if (o.state.isChecked) { - otherCheckedAction = o; - return false; - } - - return true; - }) as CompareAction[]; - - return { - otherCheckedAction, - allOtherActions, - }; - } - - buildDiffUrl(otherItem: GridItemData) { - let { appUrl } = config; - if (appUrl.at(-1) !== '/') { - // ensures that the API pathname is appended correctly (appUrl seems to always have it but better to be extra careful) - appUrl += '/'; - } - - const diffUrl = new URL('a/grafana-pyroscope-app/comparison-diff', appUrl); - - // data source - diffUrl.searchParams.set('var-dataSource', getSceneVariableValue(this, 'dataSource')); - - // time range - const { from, to } = computeRoundedTimeRange(sceneGraph.getTimeRange(this).state.value); - diffUrl.searchParams.set('from', from.toString()); - diffUrl.searchParams.set('to', to.toString()); - - const { filters: queryFilters } = (findSceneObjectByClass(this, FiltersVariable) as FiltersVariable).state; - const { serviceName: serviceId, profileMetricId } = interpolateQueryRunnerVariables(this, this.state.item); - - // query - just in case - const query = buildQuery({ - serviceId, - profileMetricId, - labels: queryFilters.map(({ key, operator, value }) => `${key}${operator}"${value}"`), - }); - diffUrl.searchParams.set('query', query); - - // left & right queries - const [leftQuery, rightQuery] = [this.state.item, otherItem] - .sort((a, b) => a.index - b.index) - .map((item) => { - const labels = [...queryFilters, ...(item.queryRunnerParams.filters || [])].map( - ({ key, operator, value }) => `${key}${operator}"${value}"` - ); - - return buildQuery({ serviceId, profileMetricId, labels: uniq(labels) }); - }); - - diffUrl.searchParams.set('leftQuery', leftQuery); - diffUrl.searchParams.set('rightQuery', rightQuery); - - return diffUrl.toString(); - } - - onClickCompareLink = () => { - reportInteraction('g_pyroscope_app_compare_link_clicked'); - }; - - onClickSelectForComparison = (event: React.MouseEvent) => { - event.preventDefault(); - this.onChange(); - }; - - public static Component = ({ model }: SceneComponentProps) => { - const styles = useStyles2(getStyles); - const { isChecked, isDisabled, isEnabled, diffUrl } = model.useState(); - - const tooltipContent = useMemo(() => { - if (isDisabled) { - return 'Two grid items have already been selected for flame graphs comparison, unselect any of them to be able to compare again'; - } - if (isEnabled) { - return 'Click to view the flame graphs comparison of the selected grid items'; - } - return 'Select two grid items to enable flame graphs comparison'; - }, [isDisabled, isEnabled]); - - return ( - -
- - Compare - - -
-
- ); - }; -} - -const getStyles = () => ({ - checkBoxWrapper: css` - display: flex; - align-items: center; - - & > a { - margin: 0 4px 0 0; - padding: 0; - } - `, -}); diff --git a/src/pages/ProfilesExplorerView/domain/events/EventSelectForCompare.tsx b/src/pages/ProfilesExplorerView/domain/events/EventSelectForCompare.tsx new file mode 100644 index 00000000..f9d63794 --- /dev/null +++ b/src/pages/ProfilesExplorerView/domain/events/EventSelectForCompare.tsx @@ -0,0 +1,13 @@ +import { BusEventWithPayload } from '@grafana/data'; + +import { GridItemDataWithStats } from '../../components/SceneExploreServiceLabels/components/SceneLabelValuesGrid'; +import { CompareTarget } from '../../components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel'; + +export interface EventSelectForComparePayload { + compareTarget: CompareTarget; + item: GridItemDataWithStats; +} + +export class EventSelectForCompare extends BusEventWithPayload { + public static type = 'select-for-compare'; +} diff --git a/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupBySelector.tsx b/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupBySelector.tsx index 215e43f2..f9bbc0c1 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupBySelector.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupBySelector.tsx @@ -72,7 +72,13 @@ export function GroupBySelector({ options, mainLabels, value, onChange, onRefres isClearable /> )} - + ); diff --git a/src/shared/infrastructure/labels/labelsRepository.ts b/src/shared/infrastructure/labels/labelsRepository.ts index 9042d513..ad205ae2 100644 --- a/src/shared/infrastructure/labels/labelsRepository.ts +++ b/src/shared/infrastructure/labels/labelsRepository.ts @@ -48,8 +48,8 @@ class LabelsRepository extends AbstractRepository 0, 'Invalid "from" parameter!'); - invariant(to > 0 && to > from, 'Invalid "to" parameter!'); + invariant(from > 0, 'Invalid timerange!'); + invariant(to > 0 && to > from, 'Invalid timerange!'); } async listLabels({ query, from, to }: ListLabelsOptions): Promise { From 162e9d550e085ac1e59b7dd7325c75b08de6201d Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 6 Aug 2024 19:30:59 +0200 Subject: [PATCH 02/76] feat(ComparePanel): Personalized tooltips on radio buttons --- .../ui/ComparePanel.tsx | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx index 1a93594e..18f29cea 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx @@ -17,29 +17,34 @@ export enum CompareTarget { COMPARISON = 'comparison', } -const OPTIONS = [ - { - label: 'Baseline', - value: CompareTarget.BASELINE, - description: 'Click to select this timeseries as baseline for comparison', - }, - { - label: 'Comparison', - value: CompareTarget.COMPARISON, - description: 'Click to select this timeseries as target for comparison', - }, -]; - export function ComparePanel({ item, onChangeCompareTarget, compareTargetValue }: ComparePanelProps) { const styles = useStyles2(getStyles); - const { allValuesSum, unit } = item.stats; + const { index, value, stats } = item; + + const color = getColorByIndex(index); + + const { allValuesSum, unit } = stats; const total = useMemo(() => { const formattedValue = getValueFormat(unit)(allValuesSum); return `${formattedValue.text}${formattedValue.suffix}`; }, [allValuesSum, unit]); - const color = getColorByIndex(item.index); + const options = useMemo( + () => [ + { + label: 'Baseline', + value: CompareTarget.BASELINE, + description: `Click to select "${value}" as baseline for comparison`, + }, + { + label: 'Comparison', + value: CompareTarget.COMPARISON, + description: `Click to select "${value}" as target for comparison`, + }, + ], + [value] + ); return (
@@ -51,7 +56,7 @@ export function ComparePanel({ item, onChangeCompareTarget, compareTargetValue } { onChangeCompareTarget(newValue as CompareTarget, item); }} From 62f551c10f64a3075d3cdfeb7a12838022767273 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 6 Aug 2024 20:21:18 +0200 Subject: [PATCH 03/76] fix: GroupByVariable now loads when landing an favs then Labels with a groupByValue --- .../SceneExploreServiceLabels.tsx | 34 +++--------------- .../SceneGroupByLabels/SceneGroupByLabels.tsx | 36 +++++++++++++++++-- .../infrastructure/labels/LabelsDataSource.ts | 2 ++ 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx index 05650044..ef12138d 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx @@ -14,11 +14,9 @@ import { FavAction } from '../../domain/actions/FavAction'; import { SelectAction } from '../../domain/actions/SelectAction'; import { EventViewServiceProfiles } from '../../domain/events/EventViewServiceProfiles'; import { FiltersVariable } from '../../domain/variables/FiltersVariable/FiltersVariable'; -import { GroupByVariable } from '../../domain/variables/GroupByVariable/GroupByVariable'; import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; -import { ScenePanelTypeSwitcher } from '../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; import { GridItemData } from '../SceneByVariableRepeaterGrid/types/GridItemData'; import { SceneGroupByLabels } from './components/SceneGroupByLabels/SceneGroupByLabels'; @@ -47,7 +45,7 @@ export class SceneExploreServiceLabels extends SceneObjectBase { - if (!newState.loading && prevState.loading) { - groupByVariable.changeValueTo(groupBy.label); - groupBySub.unsubscribe(); - } - }); - } - - if (panelType) { - const panelTypeSwitcher = findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher; - panelTypeSwitcher.setState({ panelType }); - } } // see SceneProfilesExplorer diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index 410559eb..3e1f132a 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -58,7 +58,7 @@ export interface SceneGroupByLabelsState extends SceneObjectState { } export class SceneGroupByLabels extends SceneObjectBase { - constructor() { + constructor({ item }: { item?: GridItemData }) { super({ key: 'group-by-labels', body: undefined, @@ -67,10 +67,14 @@ export class SceneGroupByLabels extends SceneObjectBase panelTypeChangeSub: undefined, }); - this.addActivationHandler(this.onActivate.bind(this)); + this.addActivationHandler(this.onActivate.bind(this, item)); } - onActivate() { + onActivate(item?: GridItemData) { + if (item) { + this.initVariablesAndControls(item); + } + const groupBySub = this.subscribeToGroupByChange(); const filtersSub = this.subscribeToFiltersChange(); const panelEventsSub = this.subscribeToPanelEvents(); @@ -84,6 +88,32 @@ export class SceneGroupByLabels extends SceneObjectBase }; } + initVariablesAndControls(item: GridItemData) { + const { queryRunnerParams, panelType } = item; + const { groupBy } = queryRunnerParams; + + if (groupBy?.label) { + const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; + + // because (to the contrary of the "Series" data) we don't load labels if the groupBy variable is not active + // (see src/pages/ProfilesExplorerView/data/labels/LabelsDataSource.ts) + // we have to wait until the new groupBy options have been loaded + // if not, its value will default to "all" regardless of our call to "changeValueTo" + // this happens, e.g., when landing on "Favorites" then jumping to "Labels" by clicking on a favorite that contains a "groupBy" label value + const groupBySub = groupByVariable.subscribeToState((newState, prevState) => { + if (!newState.loading && prevState.loading) { + groupByVariable.changeValueTo(groupBy.label); + groupBySub.unsubscribe(); + } + }); + } + + if (panelType) { + const panelTypeSwitcher = findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher; + panelTypeSwitcher.setState({ panelType }); + } + } + subscribeToGroupByChange() { const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; let clearQuickFilter = false; // do not clear the filter when the user lands on the page diff --git a/src/pages/ProfilesExplorerView/infrastructure/labels/LabelsDataSource.ts b/src/pages/ProfilesExplorerView/infrastructure/labels/LabelsDataSource.ts index 7688f7b3..9f6ef13b 100644 --- a/src/pages/ProfilesExplorerView/infrastructure/labels/LabelsDataSource.ts +++ b/src/pages/ProfilesExplorerView/infrastructure/labels/LabelsDataSource.ts @@ -72,6 +72,8 @@ export class LabelsDataSource extends RuntimeDataSource { const sceneObject = options.scopedVars?.__sceneObject?.value as GroupByVariable; // save bandwidth + // remove this when we can declare the GroupByVariable in the Scene it's used + // without messing up the variable URL sync if (!sceneObject.isActive) { return []; } From df0b747199a5a2c933b4ca8575ff028c2d666980 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 6 Aug 2024 20:21:55 +0200 Subject: [PATCH 04/76] fix(SceneExploreServiceLabels): The main timeseries should have a "View flame graph" action --- .../SceneExploreServiceLabels/SceneExploreServiceLabels.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx index ef12138d..e8f1941d 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { SceneMainServiceTimeseries } from '../../components/SceneMainServiceTimeseries'; import { FavAction } from '../../domain/actions/FavAction'; import { SelectAction } from '../../domain/actions/SelectAction'; -import { EventViewServiceProfiles } from '../../domain/events/EventViewServiceProfiles'; +import { EventViewServiceFlameGraph } from '../../domain/events/EventViewServiceFlameGraph'; import { FiltersVariable } from '../../domain/variables/FiltersVariable/FiltersVariable'; import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; @@ -39,7 +39,7 @@ export class SceneExploreServiceLabels extends SceneObjectBase [ - new SelectAction({ EventClass: EventViewServiceProfiles, item }), + new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), new FavAction({ item }), ], }), From 8edeec6d5076a193b684a86e88ccb320ac4eafed Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 6 Aug 2024 20:29:19 +0200 Subject: [PATCH 05/76] feat(ComparePanel): Remove radio button tooltip when selected --- .../SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx index 18f29cea..0f4b06a3 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx @@ -35,15 +35,17 @@ export function ComparePanel({ item, onChangeCompareTarget, compareTargetValue } { label: 'Baseline', value: CompareTarget.BASELINE, - description: `Click to select "${value}" as baseline for comparison`, + description: + compareTargetValue !== CompareTarget.BASELINE ? `Click to select "${value}" as baseline for comparison` : '', }, { label: 'Comparison', value: CompareTarget.COMPARISON, - description: `Click to select "${value}" as target for comparison`, + description: + compareTargetValue !== CompareTarget.COMPARISON ? `Click to select "${value}" as target for comparison` : '', }, ], - [value] + [compareTargetValue, value] ); return ( From dfd209538d02b4b61d08aeac514d574e8e256808 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 6 Aug 2024 20:35:30 +0200 Subject: [PATCH 06/76] chore(SceneGroupByLabels): Prevent unwanted renderGridItems call --- .../components/SceneGroupByLabels/SceneGroupByLabels.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index 3e1f132a..dce2a892 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -267,9 +267,9 @@ export class SceneGroupByLabels extends SceneObjectBase // the handler will be called each time a filter is added/removed/modified return filtersVariable.subscribeToState(() => { - if (noDataSwitcher.state.hideNoData === 'on') { + if (this.state.body instanceof SceneByVariableRepeaterGrid && noDataSwitcher.state.hideNoData === 'on') { // we force render because the filters only influence the query made in each panel, not the list of items to render (which come from the groupBy options) - (this.state.body as SceneByVariableRepeaterGrid | SceneLabelValuesGrid)?.renderGridItems(true); + this.state.body.renderGridItems(true); } }); } From cc8c6aaaefd30cf768f51beb07856cc1ebfe65a2 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 6 Aug 2024 21:03:37 +0200 Subject: [PATCH 07/76] chore: Better comment --- .../domain/variables/GroupByVariable/GroupByVariable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx b/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx index 87468335..e1a9653e 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx @@ -22,6 +22,7 @@ export class GroupByVariable extends QueryVariable { datasource: PYROSCOPE_LABELS_DATA_SOURCE, // "hack": we want to subscribe to changes of dataSource, serviceName and profileMetricId // we could also add filters, but the Service labels exploration type would reload all labels each time they are modified + // which wouldn't be great UX query: '$dataSource and $profileMetricId{service_name="$serviceName"}', loading: true, }); From c6236ed2544b63df4b325553af4b71b0011b4de0 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Wed, 7 Aug 2024 18:44:16 +0200 Subject: [PATCH 08/76] feat(*): Various improvements --- .../SceneByVariableRepeaterGrid.tsx | 34 +--- .../domain/sortFavGridItems.tsx | 23 +++ .../infrastructure/data-transformations.ts | 5 +- .../SceneExploreFavorites.tsx | 4 +- .../SceneGroupByLabels/SceneGroupByLabels.tsx | 56 +++--- .../SceneLabelValuesGrid.tsx | 174 +++++++++++------- .../SceneComparePanel/SceneComparePanel.tsx | 57 ++++++ .../SceneComparePanel}/ui/ComparePanel.tsx | 28 ++- .../components/SceneLabelValuePanel.tsx | 119 ++++++++++++ .../ui/CompareActions.tsx | 87 +++++++++ .../SceneGroupByLabels/ui/CompareActions.tsx | 63 ------- .../SceneLabelValuesStatAndTimeseries.tsx | 84 --------- .../domain/events/EventSelectForCompare.tsx | 4 +- .../components/SceneLabelValuesBarGauge.tsx | 24 ++- .../SceneLabelValuesTimeseries.tsx | 46 +++-- .../domain/events/EventDataReceived.ts | 9 + .../components/SceneMainServiceTimeseries.tsx | 2 +- .../helpers/getLabelFieldName.ts | 4 + .../helpers/getSeriesStatsValue.ts | 4 + 19 files changed, 508 insertions(+), 319 deletions(-) create mode 100644 src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.tsx rename src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/{ => SceneGroupByLabels/components/SceneLabelValuesGrid}/SceneLabelValuesGrid.tsx (58%) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/SceneComparePanel.tsx rename src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/{SceneLabelValuesStatAndTimeseries => SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel}/ui/ComparePanel.tsx (77%) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx delete mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx delete mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries.tsx rename src/pages/ProfilesExplorerView/{ => components/SceneExploreServiceLabels}/domain/events/EventSelectForCompare.tsx (52%) rename src/pages/ProfilesExplorerView/components/{ => SceneLabelValuesTimeseries}/SceneLabelValuesTimeseries.tsx (72%) create mode 100644 src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/domain/events/EventDataReceived.ts create mode 100644 src/pages/ProfilesExplorerView/helpers/getLabelFieldName.ts create mode 100644 src/pages/ProfilesExplorerView/helpers/getSeriesStatsValue.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx index 4344ceca..a6f6bc49 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx @@ -1,4 +1,4 @@ -import { DashboardCursorSync, LoadingState, VariableRefresh } from '@grafana/data'; +import { DashboardCursorSync, VariableRefresh } from '@grafana/data'; import { behaviors, EmbeddedSceneState, @@ -8,7 +8,6 @@ import { SceneCSSGridLayout, sceneGraph, SceneObjectBase, - SceneQueryRunner, VariableValueOption, VizPanelState, } from '@grafana/scenes'; @@ -17,18 +16,18 @@ import { noOp } from '@shared/domain/noOp'; import { debounce, isEqual } from 'lodash'; import React from 'react'; -import { FavAction } from '../../domain/actions/FavAction'; import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../helpers/getSceneVariableValue'; -import { FavoritesDataSource } from '../../infrastructure/favorites/FavoritesDataSource'; import { SceneLabelValuesBarGauge } from '../SceneLabelValuesBarGauge'; -import { SceneLabelValuesTimeseries } from '../SceneLabelValuesTimeseries'; +import { EventDataReceived } from '../SceneLabelValuesTimeseries/domain/events/EventDataReceived'; +import { SceneLabelValuesTimeseries } from '../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; import { SceneEmptyState } from './components/SceneEmptyState/SceneEmptyState'; import { SceneErrorState } from './components/SceneErrorState/SceneErrorState'; import { LayoutType, SceneLayoutSwitcher, SceneLayoutSwitcherState } from './components/SceneLayoutSwitcher'; import { SceneNoDataSwitcher, SceneNoDataSwitcherState } from './components/SceneNoDataSwitcher'; import { PanelType, ScenePanelTypeSwitcher } from './components/ScenePanelTypeSwitcher'; import { SceneQuickFilter, SceneQuickFilterState } from './components/SceneQuickFilter'; +import { sortFavGridItems } from './domain/sortFavGridItems'; import { GridItemData } from './types/GridItemData'; interface SceneByVariableRepeaterGridState extends EmbeddedSceneState { @@ -48,25 +47,6 @@ const GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(400px, 1fr))'; const GRID_TEMPLATE_ROWS = '1fr'; const GRID_AUTO_ROWS = '240px'; -const DEFAULT_SORT_ITEMS_FN: SceneByVariableRepeaterGridState['sortItemsFn'] = function (a, b) { - const aIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(a)); - const bIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(b)); - - if (aIsFav && bIsFav) { - return a.label.localeCompare(b.label); - } - - if (bIsFav) { - return +1; - } - - if (aIsFav) { - return -1; - } - - return 0; -}; - export class SceneByVariableRepeaterGrid extends SceneObjectBase { static buildGridItemKey(item: GridItemData) { return `grid-item-${item.index}-${item.value}`; @@ -95,7 +75,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { - if (state.data?.state !== LoadingState.Done || state.data.series.length > 0) { + const sub = vizPanel.subscribeToEvent(EventDataReceived, (event) => { + if (event.payload.series.length > 0) { return; } diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.tsx new file mode 100644 index 00000000..86d677a6 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.tsx @@ -0,0 +1,23 @@ +import { FavAction } from 'src/pages/ProfilesExplorerView/domain/actions/FavAction'; +import { FavoritesDataSource } from 'src/pages/ProfilesExplorerView/infrastructure/favorites/FavoritesDataSource'; + +import { GridItemData } from '../types/GridItemData'; + +export const sortFavGridItems: (a: GridItemData, b: GridItemData) => number = function (a, b) { + const aIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(a)); + const bIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(b)); + + if (aIsFav && bIsFav) { + return a.label.localeCompare(b.label); + } + + if (bIsFav) { + return +1; + } + + if (aIsFav) { + return -1; + } + + return 0; +}; diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/infrastructure/data-transformations.ts b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/infrastructure/data-transformations.ts index 4054ff88..e62b9a34 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/infrastructure/data-transformations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/infrastructure/data-transformations.ts @@ -2,6 +2,7 @@ import { DataFrame } from '@grafana/data'; import { merge } from 'lodash'; import { map, Observable } from 'rxjs'; +import { getSeriesStatsValue } from '../../../helpers/getSeriesStatsValue'; import { LabelsDataSource } from '../../../infrastructure/labels/LabelsDataSource'; // General note: because (e.g.) SceneLabelValuesTimeseries sets the data provider in its constructor, data can come as undefined, hence all the optional chaining operators @@ -43,8 +44,8 @@ export const sortSeries = () => (source: Observable) => source.pipe( map((data: DataFrame[]) => data?.sort((d1, d2) => { - const d1Sum = d1.meta?.stats?.find(({ displayName }) => displayName === 'allValuesSum')?.value || 0; - const d2Sum = d2.meta?.stats?.find(({ displayName }) => displayName === 'allValuesSum')?.value || 0; + const d1Sum = getSeriesStatsValue(d1, 'allValuesSum') || 0; + const d2Sum = getSeriesStatsValue(d2, 'allValuesSum') || 0; return d2Sum - d1Sum; }) ) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreFavorites/SceneExploreFavorites.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreFavorites/SceneExploreFavorites.tsx index 8eb34bcf..70c1f034 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreFavorites/SceneExploreFavorites.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreFavorites/SceneExploreFavorites.tsx @@ -5,8 +5,6 @@ import { PanelType } from '../../components/SceneByVariableRepeaterGrid/componen import { SceneByVariableRepeaterGrid } from '../../components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid'; import { GridItemData } from '../../components/SceneByVariableRepeaterGrid/types/GridItemData'; import { SceneDrawer } from '../../components/SceneDrawer'; -import { SceneLabelValuesBarGauge } from '../../components/SceneLabelValuesBarGauge'; -import { SceneLabelValuesTimeseries } from '../../components/SceneLabelValuesTimeseries'; import { FavAction } from '../../domain/actions/FavAction'; import { SelectAction } from '../../domain/actions/SelectAction'; import { EventExpandPanel } from '../../domain/events/EventExpandPanel'; @@ -17,6 +15,8 @@ import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { SceneLayoutSwitcher } from '../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; import { SceneNoDataSwitcher } from '../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; import { SceneQuickFilter } from '../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; +import { SceneLabelValuesBarGauge } from '../SceneLabelValuesBarGauge'; +import { SceneLabelValuesTimeseries } from '../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; interface SceneExploreFavoritesState extends EmbeddedSceneState { drawer: SceneDrawer; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index dce2a892..a53dfe70 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -8,7 +8,6 @@ import { SceneObject, SceneObjectBase, SceneObjectState, - SceneReactObject, } from '@grafana/scenes'; import { Stack, useStyles2 } from '@grafana/ui'; import { reportInteraction } from '@shared/domain/reportInteraction'; @@ -16,20 +15,19 @@ import { buildQuery } from '@shared/domain/url-params/parseQuery'; import React, { useMemo } from 'react'; import { Unsubscribable } from 'rxjs'; -import { computeRoundedTimeRange } from '../../../..//helpers/computeRoundedTimeRange'; -import { interpolateQueryRunnerVariables } from '../../../..//infrastructure/helpers/interpolateQueryRunnerVariables'; import { FavAction } from '../../../../domain/actions/FavAction'; import { SelectAction } from '../../../../domain/actions/SelectAction'; import { EventAddLabelToFilters } from '../../../../domain/events/EventAddLabelToFilters'; import { EventExpandPanel } from '../../../../domain/events/EventExpandPanel'; -import { EventSelectForCompare } from '../../../../domain/events/EventSelectForCompare'; import { EventSelectLabel } from '../../../../domain/events/EventSelectLabel'; import { EventViewServiceFlameGraph } from '../../../../domain/events/EventViewServiceFlameGraph'; import { addFilter } from '../../../../domain/variables/FiltersVariable/filters-ops'; import { FiltersVariable } from '../../../../domain/variables/FiltersVariable/FiltersVariable'; import { GroupByVariable } from '../../../../domain/variables/GroupByVariable/GroupByVariable'; +import { computeRoundedTimeRange } from '../../../../helpers/computeRoundedTimeRange'; import { findSceneObjectByClass } from '../../../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../../../helpers/getSceneVariableValue'; +import { interpolateQueryRunnerVariables } from '../../../../infrastructure/helpers/interpolateQueryRunnerVariables'; import { getProfileMetricLabel } from '../../../../infrastructure/series/helpers/getProfileMetricLabel'; import { SceneLayoutSwitcher } from '../../../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; import { SceneNoDataSwitcher } from '../../../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; @@ -43,12 +41,13 @@ import { SceneByVariableRepeaterGrid } from '../../../SceneByVariableRepeaterGri import { GridItemData } from '../../../SceneByVariableRepeaterGrid/types/GridItemData'; import { SceneDrawer } from '../../../SceneDrawer'; import { SceneLabelValuesBarGauge } from '../../../SceneLabelValuesBarGauge'; -import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries'; +import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; import { SceneProfilesExplorer } from '../../../SceneProfilesExplorer/SceneProfilesExplorer'; -import { GridItemDataWithStats, SceneLabelValuesGrid } from '../SceneLabelValuesGrid'; -import { SceneLabelValuesStatAndTimeseries } from '../SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries'; -import { CompareTarget } from '../SceneLabelValuesStatAndTimeseries/ui/ComparePanel'; -import { CompareActions } from './ui/CompareActions'; +import { EventSelectForCompare } from '../../domain/events/EventSelectForCompare'; +import { SceneComparePanel } from './components/SceneLabelValuesGrid/components/SceneComparePanel/SceneComparePanel'; +import { CompareTarget } from './components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel'; +import { GridItemDataWithStats, SceneLabelValuesGrid } from './components/SceneLabelValuesGrid/SceneLabelValuesGrid'; +import { CompareActions } from './components/SceneLabelValuesGrid/ui/CompareActions'; export interface SceneGroupByLabelsState extends SceneObjectState { body?: SceneObject; @@ -269,7 +268,7 @@ export class SceneGroupByLabels extends SceneObjectBase return filtersVariable.subscribeToState(() => { if (this.state.body instanceof SceneByVariableRepeaterGrid && noDataSwitcher.state.hideNoData === 'on') { // we force render because the filters only influence the query made in each panel, not the list of items to render (which come from the groupBy options) - this.state.body.renderGridItems(true); + (this.state.body as SceneByVariableRepeaterGrid | SceneLabelValuesGrid).renderGridItems(true); } }); } @@ -373,32 +372,27 @@ export class SceneGroupByLabels extends SceneObjectBase const baselineItem = compare.get(CompareTarget.BASELINE); const comparisonItem = compare.get(CompareTarget.COMPARISON); - const comparePanels = sceneGraph.findAllObjects(this, (o) => o instanceof SceneReactObject) as SceneReactObject[]; + const comparePanels = sceneGraph.findAllObjects(this, (o) => o instanceof SceneComparePanel) as SceneComparePanel[]; - let baselineItemFound = false; - let comparisonItemFound = false; + let baselineDone = false; + let comparisonDone = false; for (const panel of comparePanels) { - let compareTargetValue = undefined; - const { key, props } = panel.state; - - if ( - !baselineItemFound && - baselineItem && - key === SceneLabelValuesStatAndTimeseries.buildComparePanelKey(baselineItem) - ) { - compareTargetValue = CompareTarget.BASELINE; - baselineItemFound = true; - } else if ( - !comparisonItemFound && - comparisonItem && - key === SceneLabelValuesStatAndTimeseries.buildComparePanelKey(comparisonItem) - ) { - compareTargetValue = CompareTarget.COMPARISON; - comparisonItemFound = true; + const { key } = panel.state; + + if (!baselineDone && baselineItem && key === SceneComparePanel.buildPanelKey(baselineItem)) { + panel.updateCompareTargetValue(CompareTarget.BASELINE); + baselineDone = true; + continue; + } + + if (!comparisonDone && comparisonItem && key === SceneComparePanel.buildPanelKey(comparisonItem)) { + panel.updateCompareTargetValue(CompareTarget.COMPARISON); + comparisonDone = true; + continue; } - panel.setState({ props: { ...props, compareTargetValue } }); + panel.updateCompareTargetValue(undefined); } } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx similarity index 58% rename from src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index c0b25c59..8a368a52 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -14,58 +14,50 @@ import { Spinner } from '@grafana/ui'; import { debounce, isEqual } from 'lodash'; import React from 'react'; -import { FavAction } from '../../../domain/actions/FavAction'; -import { findSceneObjectByClass } from '../../../helpers/findSceneObjectByClass'; -import { FavoritesDataSource } from '../../../infrastructure/favorites/FavoritesDataSource'; -import { buildTimeSeriesQueryRunner } from '../../../infrastructure/timeseries/buildTimeSeriesQueryRunner'; -import { SceneEmptyState } from '../../SceneByVariableRepeaterGrid/components/SceneEmptyState/SceneEmptyState'; -import { SceneErrorState } from '../../SceneByVariableRepeaterGrid/components/SceneErrorState/SceneErrorState'; +import { findSceneObjectByClass } from '../../../../../../helpers/findSceneObjectByClass'; +import { getSceneVariableValue } from '../../../../../../helpers/getSceneVariableValue'; +import { getSeriesStatsValue } from '../../../../../../helpers/getSeriesStatsValue'; +import { buildTimeSeriesQueryRunner } from '../../../../../../infrastructure/timeseries/buildTimeSeriesQueryRunner'; +import { SceneEmptyState } from '../../../../../SceneByVariableRepeaterGrid/components/SceneEmptyState/SceneEmptyState'; +import { SceneErrorState } from '../../../../../SceneByVariableRepeaterGrid/components/SceneErrorState/SceneErrorState'; import { LayoutType, SceneLayoutSwitcher, SceneLayoutSwitcherState, -} from '../../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; -import { SceneQuickFilter, SceneQuickFilterState } from '../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; -import { addRefId, addStats, sortSeries } from '../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; -import { GridItemData } from '../../SceneByVariableRepeaterGrid/types/GridItemData'; -import { SceneGroupByLabels } from './SceneGroupByLabels/SceneGroupByLabels'; -import { SceneLabelValuesStatAndTimeseries } from './SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries'; -import { CompareTarget } from './SceneLabelValuesStatAndTimeseries/ui/ComparePanel'; - -export type GridItemDataWithStats = GridItemData & { stats: Record }; +} from '../../../../../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; +import { PanelType } from '../../../../../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; +import { + SceneQuickFilter, + SceneQuickFilterState, +} from '../../../../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; +import { sortFavGridItems } from '../../../../../SceneByVariableRepeaterGrid/domain/sortFavGridItems'; +import { addRefId, addStats } from '../../../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; +import { GridItemData } from '../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; +import { SceneGroupByLabels } from '../../SceneGroupByLabels'; +import { CompareTarget } from './components/SceneComparePanel/ui/ComparePanel'; +import { SceneLabelValuePanel } from './components/SceneLabelValuePanel'; + +export type GridItemDataWithStats = GridItemData & { + stats: { + allValuesSum: number; + unit: string; + }; +}; -interface SceneLabelValuesGridState extends EmbeddedSceneState { +export interface SceneLabelValuesGridState extends EmbeddedSceneState { $data: SceneDataProvider; + isLoading: boolean; items: GridItemDataWithStats[]; label: string; startColorIndex: number; headerActions: (item: GridItemData, items: GridItemData[]) => VizPanelState['headerActions']; - sortItemsFn: (a: GridItemData, b: GridItemData) => number; + sortItemsFn: (a: GridItemDataWithStats, b: GridItemDataWithStats) => number; } const GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(800px, 1fr))'; const GRID_TEMPLATE_ROWS = '1fr'; export const GRID_AUTO_ROWS = '160px'; -const DEFAULT_SORT_ITEMS_FN: SceneLabelValuesGridState['sortItemsFn'] = function (a, b) { - const aIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(a)); - const bIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(b)); - - if (aIsFav && bIsFav) { - return a.label.localeCompare(b.label); - } - - if (bIsFav) { - return +1; - } - - if (aIsFav) { - return -1; - } - - return 0; -}; - export class SceneLabelValuesGrid extends SceneObjectBase { constructor({ key, @@ -83,12 +75,13 @@ export class SceneLabelValuesGrid extends SceneObjectBase { layoutChangeSub.unsubscribe(); quickFilterSub.unsubscribe(); - dataSub.unsubscribe(); + refreshSub.unsubscribe(); }; } - subscribeToDataChange() { - return this.state.$data.subscribeToState((newState) => { + fetchData() { + this.setState({ isLoading: true }); + + const dataSub = this.state.$data.subscribeToState((newState) => { if (newState.data?.state !== LoadingState.Loading) { + dataSub.unsubscribe(); + this.renderGridItems(); + + this.setState({ isLoading: false }); } }); } + subscribeToRefreshClick() { + const onClickRefresh = () => { + this.fetchData(); + }; + + // start of hack, for a better UX: we disable the variable "refresh" option and we allow the user to reload the list only by clicking on the "Refresh" button + // if we don't do this, every time the time range changes (even with auto-refresh on), + // all the timeseries present on the screen would be re-created, resulting in blinking and a poor UX + const refreshButton = document.querySelector( + '[data-testid="data-testid RefreshPicker run button"]' + ) as HTMLButtonElement; + + if (!refreshButton) { + console.error('SceneByVariableRepeaterGrid: Refresh button not found! The list of items will never be updated.'); + } + + refreshButton?.addEventListener('click', onClickRefresh); + refreshButton?.setAttribute('title', 'Click to completely refresh all the panels present on the screen'); + // end of hack + + return { + unsubscribe() { + refreshButton?.removeAttribute('title'); + refreshButton?.removeEventListener('click', onClickRefresh); + }, + }; + } + subscribeToQuickFilterChange() { const quickFilter = findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter; @@ -166,31 +195,37 @@ export class SceneLabelValuesGrid extends SceneObjectBase { - const labelFromSerieLabels = fields[1].labels?.[label]; - const labelFromSerieName = fields[1].name; - const labelValue = labelFromSerieLabels || labelFromSerieName; - - const allValuesSum = meta?.stats?.find(({ displayName }) => displayName === 'allValuesSum')?.value || 0; - const { unit } = fields[1].config; - - return { - index: startColorIndex + index, - value: labelValue, - label: labelValue, - queryRunnerParams: { - filters: [{ key: label, operator: '=', value: labelFromSerieLabels || '' }], - }, - stats: { - allValuesSum, - unit, - }, - }; - }); + const serviceName = getSceneVariableValue(this, 'serviceName'); + const profileMetricId = getSceneVariableValue(this, 'profileMetricId'); + + const { label, startColorIndex, sortItemsFn } = this.state; + + const items = series + .sort((s1, s2) => (getSeriesStatsValue(s2, 'allValuesSum') || 0) - (getSeriesStatsValue(s1, 'allValuesSum') || 0)) + .map((s, index) => { + const metricField = s.fields[1]; + const labelFromSerieLabels = metricField.labels?.[label]; + const labelFromSerieName = metricField.name; + const labelValue = labelFromSerieLabels || labelFromSerieName; + + return { + index: startColorIndex + index, + value: labelValue, + label: labelValue, + queryRunnerParams: { + serviceName, + profileMetricId, + filters: [{ key: label, operator: '=', value: labelFromSerieLabels || '' }], + }, + panelType: PanelType.TIMESERIES, + stats: { + allValuesSum: getSeriesStatsValue(s, 'allValuesSum') || 0, + unit: metricField.config.unit as string, + }, + }; + }); - return this.filterItems(items).sort(this.state.sortItemsFn); + return this.filterItems(items).sort(sortItemsFn); } renderGridItems(forceRender = false) { @@ -222,7 +257,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase { - const vizPanel = new SceneLabelValuesStatAndTimeseries({ + const vizPanel = new SceneLabelValuePanel({ item, headerActions: this.state.headerActions.bind(null, item, this.state.items), compareTargetValue: this.getItemCompareTargetValue(item, compare), @@ -306,9 +341,8 @@ export class SceneLabelValuesGrid extends SceneObjectBase) { - const { body, $data } = model.useState(); - const $dataState = $data?.useState(); + const { body, isLoading } = model.useState(); - return $dataState.data?.state === LoadingState.Loading ? : ; + return isLoading ? : ; } } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/SceneComparePanel.tsx new file mode 100644 index 00000000..cd67ba5f --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/SceneComparePanel.tsx @@ -0,0 +1,57 @@ +import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import React from 'react'; + +import { EventSelectForCompare } from '../../../../../../domain/events/EventSelectForCompare'; +import { GridItemDataWithStats } from '../../SceneLabelValuesGrid'; +import { ComparePanel, CompareTarget } from './ui/ComparePanel'; + +interface SceneComparePanelState extends SceneObjectState { + item: GridItemDataWithStats; + compareTargetValue?: CompareTarget; +} + +export class SceneComparePanel extends SceneObjectBase { + static WIDTH_IN_PIXELS = 180; + + static buildPanelKey(item: GridItemDataWithStats) { + return `compare-panel-${item.value}`; + } + + constructor({ item, compareTargetValue }: { item: GridItemDataWithStats; compareTargetValue?: CompareTarget }) { + super({ + key: SceneComparePanel.buildPanelKey(item), + item, + compareTargetValue, + }); + } + + updateCompareTargetValue(compareTargetValue?: CompareTarget) { + this.setState({ compareTargetValue }); + } + + getStats() { + return this.state.item.stats; + } + + updateStats(stats: GridItemDataWithStats['stats']) { + const { item } = this.state; + this.setState({ item: { ...item, stats } }); + } + + onChangeCompareTarget = (compareTarget: CompareTarget) => { + const { item } = this.state; + this.publishEvent(new EventSelectForCompare({ compareTarget, item }), true); + }; + + static Component({ model }: SceneComponentProps) { + const { item, compareTargetValue } = model.useState(); + + return ( + + ); + } +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel.tsx similarity index 77% rename from src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel.tsx index 0f4b06a3..d683872b 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel.tsx @@ -3,13 +3,13 @@ import { getValueFormat, GrafanaTheme2 } from '@grafana/data'; import { RadioButtonGroup, useStyles2 } from '@grafana/ui'; import React, { useMemo } from 'react'; -import { getColorByIndex } from '../../../../../helpers/getColorByIndex'; -import { GridItemDataWithStats } from '../../SceneLabelValuesGrid'; +import { getColorByIndex } from '../../../../../../../../../helpers/getColorByIndex'; +import { GridItemDataWithStats } from '../../../SceneLabelValuesGrid'; export type ComparePanelProps = { item: GridItemDataWithStats; - onChangeCompareTarget: (compareTarget: CompareTarget, item: GridItemDataWithStats) => void; compareTargetValue?: CompareTarget; + onChangeCompareTarget: (compareTarget: CompareTarget, item: GridItemDataWithStats) => void; }; export enum CompareTarget { @@ -17,14 +17,14 @@ export enum CompareTarget { COMPARISON = 'comparison', } -export function ComparePanel({ item, onChangeCompareTarget, compareTargetValue }: ComparePanelProps) { +export function ComparePanel({ item, compareTargetValue, onChangeCompareTarget }: ComparePanelProps) { const styles = useStyles2(getStyles); + const { index, value, stats } = item; + const { allValuesSum, unit } = stats; const color = getColorByIndex(index); - const { allValuesSum, unit } = stats; - const total = useMemo(() => { const formattedValue = getValueFormat(unit)(allValuesSum); return `${formattedValue.text}${formattedValue.suffix}`; @@ -74,9 +74,12 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: flex; flex-direction: column; justify-content: space-between; - border: 1px solid ${theme.colors.border.medium}; - padding: ${theme.spacing(1)}; width: 100%; + background-color: ${theme.colors.background.canvas}; + padding: ${theme.spacing(1)}; + border: 1px solid ${theme.colors.border.weak}; + border-right: none; + border-radius: 2px 0 0 2px; `, title: css` font-size: 24px; @@ -91,9 +94,14 @@ const getStyles = (theme: GrafanaTheme2) => ({ flex-grow: 1 !important; } - & :checked + label { + & :nth-child(1):checked + label { + color: #fff; + background-color: ${theme.colors.primary.main}; // TODO + } + + & :nth-child(2):checked + label { color: #fff; - background-color: ${theme.colors.primary.main}; + background-color: ${theme.colors.primary.main}; // TODO } `, }); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx new file mode 100644 index 00000000..7e0e9a78 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx @@ -0,0 +1,119 @@ +import { css, cx } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanelState } from '@grafana/scenes'; +import { useStyles2 } from '@grafana/ui'; +import React from 'react'; + +import { getSeriesStatsValue } from '../../../../../../../helpers/getSeriesStatsValue'; +import { GridItemData } from '../../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; +import { EventDataReceived } from '../../../../../../SceneLabelValuesTimeseries/domain/events/EventDataReceived'; +import { SceneLabelValuesTimeseries } from '../../../../../../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; +import { GRID_AUTO_ROWS, GridItemDataWithStats } from '../SceneLabelValuesGrid'; +import { SceneComparePanel } from './SceneComparePanel/SceneComparePanel'; +import { CompareTarget } from './SceneComparePanel/ui/ComparePanel'; + +interface SceneLabelValuesStatAndTimeseriesState extends SceneObjectState { + comparePanel: SceneComparePanel; + timeseriesPanel: SceneLabelValuesTimeseries; +} + +export class SceneLabelValuePanel extends SceneObjectBase { + static buildPanelKey(item: GridItemDataWithStats) { + return `compare-panel-${item.value}`; + } + + constructor({ + item, + headerActions, + compareTargetValue, + }: { + item: GridItemDataWithStats; + headerActions: (item: GridItemData) => VizPanelState['headerActions']; + compareTargetValue?: CompareTarget; + }) { + super({ + key: 'label-value-panel', + comparePanel: new SceneComparePanel({ item, compareTargetValue }), + timeseriesPanel: new SceneLabelValuesTimeseries({ item, headerActions }), + }); + + this.addActivationHandler(this.onActivate.bind(this)); + } + + onActivate() { + const { comparePanel, timeseriesPanel } = this.state; + + const timeseriesSub = timeseriesPanel.subscribeToEvent(EventDataReceived, (event) => { + const [s] = event.payload.series; + const allValuesSum = s ? getSeriesStatsValue(s, 'allValuesSum') || 0 : 0; + + if (comparePanel.getStats().allValuesSum !== allValuesSum) { + comparePanel.updateStats({ + allValuesSum, + unit: s ? (s.fields[1].config.unit as string) : 'short', + }); + } + }); + + return () => { + timeseriesSub.unsubscribe(); + }; + } + + static Component({ model }: SceneComponentProps) { + const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks + const { comparePanel, timeseriesPanel } = model.useState(); + const { compareTargetValue } = comparePanel.useState(); + + return ( +
+
+ +
+
+ +
+
+ ); + } +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css` + display: flex; + min-width: 0px; + min-height: ${GRID_AUTO_ROWS}; + flex-flow: row; + + & > div { + display: flex; + position: relative; + flex-direction: row; + align-self: stretch; + min-height: ${GRID_AUTO_ROWS}; + } + `, + comparePanel: css` + width: ${SceneComparePanel.WIDTH_IN_PIXELS}px; + + &.selected > div { + border-top: 1px solid ${theme.colors.primary.main}; + border-bottom: 1px solid ${theme.colors.primary.main}; + border-left: 1px solid ${theme.colors.primary.main}; + } + `, + timeseriesPanel: css` + flex-grow: 1; + + & [data-viz-panel-key] > div { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &.selected [data-viz-panel-key] > div { + border-top: 1px solid ${theme.colors.primary.main}; + border-bottom: 1px solid ${theme.colors.primary.main}; + border-right: 1px solid ${theme.colors.primary.main}; + } + `, +}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx new file mode 100644 index 00000000..9f771861 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx @@ -0,0 +1,87 @@ +import { css, cx } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Button, useStyles2 } from '@grafana/ui'; +import { noOp } from '@shared/domain/noOp'; +import React from 'react'; + +import { SceneGroupByLabelsState } from '../../../SceneGroupByLabels'; +import { SceneComparePanel } from '../components/SceneComparePanel/SceneComparePanel'; +import { CompareTarget } from '../components/SceneComparePanel/ui/ComparePanel'; + +type CompareButtonProps = { + compare: SceneGroupByLabelsState['compare']; + onClickCompare: () => void; + onClickClear: () => void; +}; + +export function CompareActions({ compare, onClickCompare, onClickClear }: CompareButtonProps) { + const styles = useStyles2(getStyles); + const compareIsDisabled = compare.size < 2; + const hasSelection = compare.size > 0; + + return ( +
+ + +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css` + display: flex; + align-items: center; + width: ${SceneComparePanel.WIDTH_IN_PIXELS}px; + `, + compareButton: css` + width: ${SceneComparePanel.WIDTH_IN_PIXELS - 32}px; + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + `, + clearButton: css` + box-sizing: border-box; + width: 32px !important; + height: 32px !important; + color: ${theme.colors.text.secondary}; + background-color: transparent; + border-left: none !important; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + &:not([disabled]), + &:not([disabled]):hover { + background-color: transparent; + box-shadow: none; + } + `, + clearButtonActive: css` + border-color: ${theme.colors.border.medium}; + + &:hover { + border-color: ${theme.colors.border.medium}; + } + `, +}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx deleted file mode 100644 index 280edd2f..00000000 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { css } from '@emotion/css'; -import { GrafanaTheme2 } from '@grafana/data'; -import { Button, IconButton, useStyles2 } from '@grafana/ui'; -import { noOp } from '@shared/domain/noOp'; -import React from 'react'; - -import { WIDTH_COMPARE_PANEL } from '../../SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries'; -import { CompareTarget } from '../../SceneLabelValuesStatAndTimeseries/ui/ComparePanel'; -import { SceneGroupByLabelsState } from '../SceneGroupByLabels'; - -type CompareButtonProps = { - compare: SceneGroupByLabelsState['compare']; - onClickCompare: () => void; - onClickClear: () => void; -}; - -export function CompareActions({ compare, onClickCompare, onClickClear }: CompareButtonProps) { - const styles = useStyles2(getStyles); - const disabled = compare.size < 2; - const hasSelection = compare.size > 0; - - return ( -
- - - -
- ); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - container: css` - display: flex; - align-items: center; - justify-content: space-between; - width: ${WIDTH_COMPARE_PANEL}; - gap: ${theme.spacing(1)}; - `, - compareButton: css` - flex-grow: 1; - `, - clearButton: css``, -}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries.tsx deleted file mode 100644 index 367c8dfd..00000000 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/SceneLabelValuesStatAndTimeseries.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - SceneComponentProps, - SceneFlexItem, - SceneFlexLayout, - SceneObjectBase, - SceneObjectState, - SceneReactObject, - VizPanelState, -} from '@grafana/scenes'; -import React from 'react'; - -import { EventSelectForCompare } from '../../../../domain/events/EventSelectForCompare'; -import { GridItemData } from '../../../SceneByVariableRepeaterGrid/types/GridItemData'; -import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries'; -import { GRID_AUTO_ROWS, GridItemDataWithStats } from '../SceneLabelValuesGrid'; -import { ComparePanel, CompareTarget } from './ui/ComparePanel'; - -interface SceneLabelValuesStatAndTimeseriesState extends SceneObjectState { - body: SceneFlexLayout; -} - -export const WIDTH_COMPARE_PANEL = '180px'; - -export class SceneLabelValuesStatAndTimeseries extends SceneObjectBase { - static buildComparePanelKey(item: GridItemDataWithStats) { - return `compare-panel-${item.value}`; - } - - constructor(options: { - item: GridItemDataWithStats; - headerActions: (item: GridItemData) => VizPanelState['headerActions']; - compareTargetValue?: CompareTarget; - }) { - super({ - key: 'stat-and-timeseries-label-values', - body: new SceneFlexLayout({ - direction: 'row', - minHeight: GRID_AUTO_ROWS, - children: [], - }), - }); - - this.addActivationHandler(this.onActivate.bind(this, options)); - } - - onActivate({ - item, - headerActions, - compareTargetValue, - }: { - item: GridItemDataWithStats; - headerActions: (item: GridItemData) => VizPanelState['headerActions']; - compareTargetValue?: CompareTarget; - }) { - this.state.body.setState({ - minHeight: GRID_AUTO_ROWS, - children: [ - new SceneFlexItem({ - width: WIDTH_COMPARE_PANEL, - body: new SceneReactObject({ - key: SceneLabelValuesStatAndTimeseries.buildComparePanelKey(item), - component: ComparePanel, - props: { - item, - onChangeCompareTarget: (compareTarget: CompareTarget, item: GridItemDataWithStats) => { - this.publishEvent(new EventSelectForCompare({ compareTarget, item }), true); - }, - compareTargetValue, - }, - }), - }), - new SceneFlexItem({ - body: new SceneLabelValuesTimeseries({ item, headerActions }), - }), - ], - }); - } - - static Component({ model }: SceneComponentProps) { - const { body } = model.useState(); - - return ; - } -} diff --git a/src/pages/ProfilesExplorerView/domain/events/EventSelectForCompare.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/domain/events/EventSelectForCompare.tsx similarity index 52% rename from src/pages/ProfilesExplorerView/domain/events/EventSelectForCompare.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/domain/events/EventSelectForCompare.tsx index f9d63794..75756b20 100644 --- a/src/pages/ProfilesExplorerView/domain/events/EventSelectForCompare.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/domain/events/EventSelectForCompare.tsx @@ -1,7 +1,7 @@ import { BusEventWithPayload } from '@grafana/data'; -import { GridItemDataWithStats } from '../../components/SceneExploreServiceLabels/components/SceneLabelValuesGrid'; -import { CompareTarget } from '../../components/SceneExploreServiceLabels/components/SceneLabelValuesStatAndTimeseries/ui/ComparePanel'; +import { CompareTarget } from '../../components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel'; +import { GridItemDataWithStats } from '../../components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid'; export interface EventSelectForComparePayload { compareTarget: CompareTarget; diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx index fe33aa15..0f5a977b 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx @@ -12,9 +12,12 @@ import { BarGaugeDisplayMode, BarGaugeNamePlacement, BarGaugeSizing, BarGaugeVal import React from 'react'; import { getColorByIndex } from '../helpers/getColorByIndex'; +import { getLabelFieldName } from '../helpers/getLabelFieldName'; +import { getSeriesStatsValue } from '../helpers/getSeriesStatsValue'; import { buildTimeSeriesQueryRunner } from '../infrastructure/timeseries/buildTimeSeriesQueryRunner'; import { addRefId, addStats, sortSeries } from './SceneByVariableRepeaterGrid/infrastructure/data-transformations'; import { GridItemData } from './SceneByVariableRepeaterGrid/types/GridItemData'; +import { EventDataReceived } from './SceneLabelValuesTimeseries/domain/events/EventDataReceived'; interface SceneLabelValuesBarGaugeState extends SceneObjectState { body: VizPanel; @@ -53,7 +56,11 @@ export class SceneLabelValuesBarGauge extends SceneObjectBase { @@ -64,8 +71,8 @@ export class SceneLabelValuesBarGauge extends SceneObjectBase displayName === 'allValuesSum')?.value || 0; + for (const s of series) { + const allValuesSum = getSeriesStatsValue(s, 'allValuesSum') || 0; if (allValuesSum > max) { max = allValuesSum; @@ -111,18 +118,19 @@ export class SceneLabelValuesBarGauge extends SceneObjectBase ({ - matcher: { id: FieldMatcherID.byFrameRefID, options: serie.refId }, + return series.map((s, i) => ({ + matcher: { id: FieldMatcherID.byFrameRefID, options: s.refId }, properties: [ { id: 'displayName', - value: groupByLabel ? serie.fields[1].labels?.[groupByLabel] : serie.fields[1].name, + value: getLabelFieldName(s.fields[1], groupByLabel), }, { id: 'color', - value: { mode: 'fixed', fixedColor: getColorByIndex(item.index + i) }, + value: { mode: 'fixed', fixedColor: getColorByIndex(startColorIndex + i) }, }, ], })); diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx similarity index 72% rename from src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx rename to src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx index fb442fd4..0f14194d 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx @@ -11,16 +11,19 @@ import { import { GraphGradientMode } from '@grafana/schema'; import React from 'react'; -import { getColorByIndex } from '../helpers/getColorByIndex'; -import { LabelsDataSource } from '../infrastructure/labels/LabelsDataSource'; -import { buildTimeSeriesQueryRunner } from '../infrastructure/timeseries/buildTimeSeriesQueryRunner'; +import { getColorByIndex } from '../../helpers/getColorByIndex'; +import { getLabelFieldName } from '../../helpers/getLabelFieldName'; +import { getSeriesStatsValue } from '../../helpers/getSeriesStatsValue'; +import { LabelsDataSource } from '../../infrastructure/labels/LabelsDataSource'; +import { buildTimeSeriesQueryRunner } from '../../infrastructure/timeseries/buildTimeSeriesQueryRunner'; import { addRefId, addStats, limitNumberOfSeries, sortSeries, -} from './SceneByVariableRepeaterGrid/infrastructure/data-transformations'; -import { GridItemData } from './SceneByVariableRepeaterGrid/types/GridItemData'; +} from '../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; +import { GridItemData } from '../SceneByVariableRepeaterGrid/types/GridItemData'; +import { EventDataReceived } from './domain/events/EventDataReceived'; interface SceneLabelValuesTimeseriesState extends SceneObjectState { item: GridItemData; @@ -65,13 +68,19 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { - if (state.data?.state !== LoadingState.Done || !state.data.series.length) { + if (state.data?.state !== LoadingState.Done) { return; } - const config = this.state.displayAllValues - ? this.getAllValuesConfig(state.data.series) - : this.getConfig(state.data.series); + const { series } = state.data; + + this.publishEvent(new EventDataReceived({ series })); + + if (!series.length) { + return; + } + + const config = this.state.displayAllValues ? this.getAllValuesConfig(series) : this.getConfig(series); body.setState(config); }); @@ -89,8 +98,7 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase 1 ? `${item.label} (${series.length})` : item.label; - const totalSeriesCount = - series[0].meta?.stats?.find(({ displayName }) => displayName === 'totalSeriesCount')?.value || 0; + const totalSeriesCount = getSeriesStatsValue(series[0], 'totalSeriesCount') || 0; const hasTooManySeries = totalSeriesCount > LabelsDataSource.MAX_TIMESERIES_LABEL_VALUES; description = hasTooManySeries @@ -132,19 +140,19 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { - let displayName = serie.fields[1].labels?.[groupByLabel as string] || serie.fields[1].name; + return series.map((s, i) => { + const metricField = s.fields[1]; + let displayName = getLabelFieldName(metricField, groupByLabel); if (series.length === 1) { - const allValuesSum = serie.meta?.stats?.find(({ displayName }) => displayName === 'allValuesSum')?.value || 0; - const { unit } = serie.fields[1].config; - const formattedValue = getValueFormat(unit)(allValuesSum); + const allValuesSum = getSeriesStatsValue(s, 'allValuesSum') || 0; + const formattedValue = getValueFormat(metricField.config.unit)(allValuesSum); displayName = `${displayName} / total = ${formattedValue.text}${formattedValue.suffix}`; } return { - matcher: { id: FieldMatcherID.byFrameRefID, options: serie.refId }, + matcher: { id: FieldMatcherID.byFrameRefID, options: s.refId }, properties: [ { id: 'displayName', @@ -161,7 +169,7 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase serie.fields[1].labels?.[groupByLabel as string] || serie.fields[1].name); + return series.map((s) => getLabelFieldName(s.fields[1], groupByLabel)); } updateTitle(newTitle: string) { @@ -171,6 +179,6 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase) { const { body } = model.useState(); - return ; + return ; } } diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/domain/events/EventDataReceived.ts b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/domain/events/EventDataReceived.ts new file mode 100644 index 00000000..782ab841 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/domain/events/EventDataReceived.ts @@ -0,0 +1,9 @@ +import { BusEventWithPayload, DataFrame } from '@grafana/data'; + +export interface EventDataReceivedPayload { + series: DataFrame[]; +} + +export class EventDataReceived extends BusEventWithPayload { + public static type = 'data-received'; +} diff --git a/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx index cd53661e..3640fda9 100644 --- a/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx @@ -12,7 +12,7 @@ import { getSceneVariableValue } from '../helpers/getSceneVariableValue'; import { getProfileMetricLabel } from '../infrastructure/series/helpers/getProfileMetricLabel'; import { PanelType } from './SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; import { GridItemData } from './SceneByVariableRepeaterGrid/types/GridItemData'; -import { SceneLabelValuesTimeseries } from './SceneLabelValuesTimeseries'; +import { SceneLabelValuesTimeseries } from './SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; interface SceneMainServiceTimeseriesState extends SceneObjectState { body?: SceneLabelValuesTimeseries; diff --git a/src/pages/ProfilesExplorerView/helpers/getLabelFieldName.ts b/src/pages/ProfilesExplorerView/helpers/getLabelFieldName.ts new file mode 100644 index 00000000..8c1dfa2b --- /dev/null +++ b/src/pages/ProfilesExplorerView/helpers/getLabelFieldName.ts @@ -0,0 +1,4 @@ +import { Field } from '@grafana/data'; + +export const getLabelFieldName = (metricField: Field, label?: string) => + metricField.labels?.[label as string] || metricField.name; diff --git a/src/pages/ProfilesExplorerView/helpers/getSeriesStatsValue.ts b/src/pages/ProfilesExplorerView/helpers/getSeriesStatsValue.ts new file mode 100644 index 00000000..7c008652 --- /dev/null +++ b/src/pages/ProfilesExplorerView/helpers/getSeriesStatsValue.ts @@ -0,0 +1,4 @@ +import { DataFrame } from '@grafana/data'; + +export const getSeriesStatsValue = (series: DataFrame, displayName: string) => + series.meta?.stats?.find((s) => s.displayName === displayName)?.value; From f364e1c7ed02333eaeea2e70ce84e8f53e705c47 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Wed, 7 Aug 2024 19:08:22 +0200 Subject: [PATCH 09/76] feat(SceneLabelValuesGrid): Add "Hide panels without data" switcher --- .../SceneGroupByLabels/SceneGroupByLabels.tsx | 3 +- .../SceneLabelValuesGrid.tsx | 69 +++++++++++++++++++ .../components/SceneLabelValuesBarGauge.tsx | 2 +- .../SceneLabelValuesTimeseries.tsx | 2 +- 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index a53dfe70..5ca97a98 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -266,7 +266,7 @@ export class SceneGroupByLabels extends SceneObjectBase // the handler will be called each time a filter is added/removed/modified return filtersVariable.subscribeToState(() => { - if (this.state.body instanceof SceneByVariableRepeaterGrid && noDataSwitcher.state.hideNoData === 'on') { + if (noDataSwitcher.state.hideNoData === 'on') { // we force render because the filters only influence the query made in each panel, not the list of items to render (which come from the groupBy options) (this.state.body as SceneByVariableRepeaterGrid | SceneLabelValuesGrid).renderGridItems(true); } @@ -478,6 +478,7 @@ export class SceneGroupByLabels extends SceneObjectBase : ([ findSceneObjectByClass(model, SceneQuickFilter), findSceneObjectByClass(model, SceneLayoutSwitcher), + findSceneObjectByClass(model, SceneNoDataSwitcher), ] as SceneObject[]), [groupByVariableValue, model] ); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index 8a368a52..e16e9945 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -7,6 +7,7 @@ import { SceneCSSGridLayout, SceneDataProvider, SceneDataTransformer, + sceneGraph, SceneObjectBase, VizPanelState, } from '@grafana/scenes'; @@ -25,6 +26,10 @@ import { SceneLayoutSwitcher, SceneLayoutSwitcherState, } from '../../../../../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; +import { + SceneNoDataSwitcher, + SceneNoDataSwitcherState, +} from '../../../../../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; import { PanelType } from '../../../../../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; import { SceneQuickFilter, @@ -33,6 +38,7 @@ import { import { sortFavGridItems } from '../../../../../SceneByVariableRepeaterGrid/domain/sortFavGridItems'; import { addRefId, addStats } from '../../../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; import { GridItemData } from '../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; +import { EventDataReceived } from '../../../../../SceneLabelValuesTimeseries/domain/events/EventDataReceived'; import { SceneGroupByLabels } from '../../SceneGroupByLabels'; import { CompareTarget } from './components/SceneComparePanel/ui/ComparePanel'; import { SceneLabelValuePanel } from './components/SceneLabelValuePanel'; @@ -52,6 +58,7 @@ export interface SceneLabelValuesGridState extends EmbeddedSceneState { startColorIndex: number; headerActions: (item: GridItemData, items: GridItemData[]) => VizPanelState['headerActions']; sortItemsFn: (a: GridItemDataWithStats, b: GridItemDataWithStats) => number; + hideNoData: boolean; } const GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(800px, 1fr))'; @@ -59,6 +66,10 @@ const GRID_TEMPLATE_ROWS = '1fr'; export const GRID_AUTO_ROWS = '160px'; export class SceneLabelValuesGrid extends SceneObjectBase { + static buildGridItemKey(item: GridItemData) { + return `grid-item-${item.index}-${item.value}`; + } + constructor({ key, label, @@ -80,6 +91,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase { + hideNoDataSub.unsubscribe(); layoutChangeSub.unsubscribe(); quickFilterSub.unsubscribe(); refreshSub.unsubscribe(); @@ -184,6 +198,30 @@ export class SceneLabelValuesGrid extends SceneObjectBase { + if (newState.hideNoData !== prevState.hideNoData) { + this.setState({ + hideNoData: newState.hideNoData === 'on', + $data: new SceneDataTransformer({ + $data: buildTimeSeriesQueryRunner({ groupBy: { label: this.state.label } }), + transformations: [addRefId, addStats], + }), + }); + + this.fetchData(); + } + }; + + return noDataSwitcher.subscribeToState(onChangeState); + } + shouldRenderItems(newItems: SceneLabelValuesGridState['items']) { const { items } = this.state; @@ -263,7 +301,12 @@ export class SceneLabelValuesGrid extends SceneObjectBase { + if (event.payload.series.length > 0) { + return; + } + + const gridItem = sceneGraph.getAncestor(vizPanel, SceneCSSGridItem); + const { key: gridItemKey } = gridItem.state; + const grid = sceneGraph.getAncestor(gridItem, SceneCSSGridLayout); + + const filteredChildren = grid.state.children.filter((c) => c.state.key !== gridItemKey); + + if (!filteredChildren.length) { + this.renderEmptyState(); + } else { + grid.setState({ children: filteredChildren }); + } + }); + + vizPanel.addActivationHandler(() => { + return () => { + sub.unsubscribe(); + }; + }); + } + getItemCompareTargetValue(item: GridItemDataWithStats, compare: Map) { if (compare.get(CompareTarget.BASELINE)?.value === item.value) { return CompareTarget.BASELINE; diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx index 0f5a977b..3a6d4554 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx @@ -58,7 +58,7 @@ export class SceneLabelValuesBarGauge extends SceneObjectBase Date: Wed, 7 Aug 2024 20:00:07 +0200 Subject: [PATCH 10/76] refactor: Slight change --- .../SceneLabelValuesGrid.tsx | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index e16e9945..62f27799 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -295,19 +295,9 @@ export class SceneLabelValuesGrid extends SceneObjectBase { - const vizPanel = new SceneLabelValuePanel({ - item, - headerActions: this.state.headerActions.bind(null, item, this.state.items), - compareTargetValue: this.getItemCompareTargetValue(item, compare), - }); - - if (this.state.hideNoData) { - this.setupHideNoData(vizPanel); - } - return new SceneCSSGridItem({ key: SceneLabelValuesGrid.buildGridItemKey(item), - body: vizPanel, + body: this.buildVizPanel(item, compare), }); }); @@ -317,22 +307,28 @@ export class SceneLabelValuesGrid extends SceneObjectBase) { + const vizPanel = new SceneLabelValuePanel({ + item, + headerActions: this.state.headerActions.bind(null, item, this.state.items), + compareTargetValue: this.getItemCompareTargetValue(item, compare), + }); + const sub = vizPanel.subscribeToEvent(EventDataReceived, (event) => { - if (event.payload.series.length > 0) { - return; - } + // we might have to consider if we update the item.stats here (will impact sorting if we don't do it) - const gridItem = sceneGraph.getAncestor(vizPanel, SceneCSSGridItem); - const { key: gridItemKey } = gridItem.state; - const grid = sceneGraph.getAncestor(gridItem, SceneCSSGridLayout); + if (this.state.hideNoData && !event.payload.series.length) { + const gridItem = sceneGraph.getAncestor(vizPanel, SceneCSSGridItem); + const { key: gridItemKey } = gridItem.state; + const grid = sceneGraph.getAncestor(gridItem, SceneCSSGridLayout); - const filteredChildren = grid.state.children.filter((c) => c.state.key !== gridItemKey); + const filteredChildren = grid.state.children.filter((c) => c.state.key !== gridItemKey); - if (!filteredChildren.length) { - this.renderEmptyState(); - } else { - grid.setState({ children: filteredChildren }); + if (!filteredChildren.length) { + this.renderEmptyState(); + } else { + grid.setState({ children: filteredChildren }); + } } }); @@ -341,6 +337,8 @@ export class SceneLabelValuesGrid extends SceneObjectBase) { From 309b72531447ae5e32a799295308298e5fef98d1 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 8 Aug 2024 10:26:29 +0200 Subject: [PATCH 11/76] feat(SceneLabelValuesGrid): Hide no data don't fetch data but render items --- .../SceneLabelValuesGrid.tsx | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index 62f27799..a3756c30 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -201,24 +201,17 @@ export class SceneLabelValuesGrid extends SceneObjectBase { - if (newState.hideNoData !== prevState.hideNoData) { - this.setState({ - hideNoData: newState.hideNoData === 'on', - $data: new SceneDataTransformer({ - $data: buildTimeSeriesQueryRunner({ groupBy: { label: this.state.label } }), - transformations: [addRefId, addStats], - }), - }); + const onChangeState = (newState: SceneNoDataSwitcherState, prevState?: SceneNoDataSwitcherState) => { + if (newState.hideNoData !== prevState?.hideNoData) { + this.setState({ hideNoData: newState.hideNoData === 'on' }); - this.fetchData(); + // we force render because this.state.items certainly have not changed but we want to update the UI panels anyway + this.renderGridItems(true); } }; + onChangeState(noDataSwitcher.state); + return noDataSwitcher.subscribeToState(onChangeState); } @@ -267,7 +260,11 @@ export class SceneLabelValuesGrid extends SceneObjectBase Date: Thu, 8 Aug 2024 17:52:43 +0200 Subject: [PATCH 12/76] refactor(*): Various fixes and improvements --- .../SceneByVariableRepeaterGrid.tsx | 24 +- .../infrastructure/data-transformations.ts | 2 +- .../SceneGroupByLabels/SceneGroupByLabels.tsx | 240 +++++++----------- .../SceneLabelValuesGrid.tsx | 164 +++++++----- .../SceneComparePanel/SceneComparePanel.tsx | 30 ++- .../SceneComparePanel/ui/ComparePanel.tsx | 27 +- .../components/SceneLabelValuePanel.tsx | 10 +- .../domain}/getSeriesStatsValue.ts | 0 .../domain/events/EventSelectForCompare.tsx | 4 +- .../components/SceneLabelValuesBarGauge.tsx | 2 +- .../SceneLabelValuesTimeseries.tsx | 4 +- 11 files changed, 248 insertions(+), 259 deletions(-) rename src/pages/ProfilesExplorerView/{helpers => components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain}/getSeriesStatsValue.ts (100%) diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx index a6f6bc49..a0908047 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx @@ -16,6 +16,7 @@ import { noOp } from '@shared/domain/noOp'; import { debounce, isEqual } from 'lodash'; import React from 'react'; +import { FiltersVariable } from '../../domain/variables/FiltersVariable/FiltersVariable'; import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../helpers/getSceneVariableValue'; import { SceneLabelValuesBarGauge } from '../SceneLabelValuesBarGauge'; @@ -112,8 +113,10 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { + filtersSub.unsubscribe(); hideNoDataSub.unsubscribe(); layoutChangeSub.unsubscribe(); quickFilterSub.unsubscribe(); @@ -212,6 +215,19 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { + if (noDataSwitcher.state.hideNoData === 'on') { + // to be sure the list is updated we force render because the filters only influence the query made in each panel + this.renderGridItems(true); + } + }); + } + buildItemsData(variable: QueryVariable) { const { mapOptionToItem } = this.state; @@ -350,9 +366,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase; + compare: Map; panelTypeChangeSub?: Unsubscribable; } @@ -66,20 +66,26 @@ export class SceneGroupByLabels extends SceneObjectBase panelTypeChangeSub: undefined, }); - this.addActivationHandler(this.onActivate.bind(this, item)); + this.addActivationHandler(() => { + this.onActivate(item); + }); } - onActivate(item?: GridItemData) { + async onActivate(item?: GridItemData) { + // initial load + const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; + await groupByVariable.update(); + if (item) { this.initVariablesAndControls(item); } + this.renderBody(groupByVariable.state); + const groupBySub = this.subscribeToGroupByChange(); - const filtersSub = this.subscribeToFiltersChange(); const panelEventsSub = this.subscribeToPanelEvents(); return () => { - filtersSub.unsubscribe(); panelEventsSub.unsubscribe(); groupBySub.unsubscribe(); @@ -93,18 +99,7 @@ export class SceneGroupByLabels extends SceneObjectBase if (groupBy?.label) { const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; - - // because (to the contrary of the "Series" data) we don't load labels if the groupBy variable is not active - // (see src/pages/ProfilesExplorerView/data/labels/LabelsDataSource.ts) - // we have to wait until the new groupBy options have been loaded - // if not, its value will default to "all" regardless of our call to "changeValueTo" - // this happens, e.g., when landing on "Favorites" then jumping to "Labels" by clicking on a favorite that contains a "groupBy" label value - const groupBySub = groupByVariable.subscribeToState((newState, prevState) => { - if (!newState.loading && prevState.loading) { - groupByVariable.changeValueTo(groupBy.label); - groupBySub.unsubscribe(); - } - }); + groupByVariable.changeValueTo(groupBy.label); } if (panelType) { @@ -115,35 +110,68 @@ export class SceneGroupByLabels extends SceneObjectBase subscribeToGroupByChange() { const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; - let clearQuickFilter = false; // do not clear the filter when the user lands on the page + const quickFilter = findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter; + + return groupByVariable.subscribeToState((newState, prevState) => { + if (newState.value !== prevState?.value) { + quickFilter.clear(); - const onChangeState = (newState: MultiValueVariableState, prevState?: MultiValueVariableState) => { - if (newState.value === prevState?.value) { - return; + this.renderBody(newState); } + }); + } - const quickFilter = findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter; + subscribeToPanelEvents() { + const selectLabelSub = this.subscribeToEvent(EventSelectLabel, (event) => { + this.selectLabel(event.payload.item); + }); - if (clearQuickFilter) { - quickFilter.clear(); - } + const expandPanelSub = this.subscribeToEvent(EventExpandPanel, async (event) => { + this.openExpandedPanelDrawer(event.payload.item); + }); - this.state.panelTypeChangeSub?.unsubscribe(); + const selectForCompareSub = this.subscribeToEvent(EventSelectForCompare, (event) => { + const { compareTarget, item } = event.payload; + this.selectForCompare(compareTarget, item); + }); - if (newState.value === 'all') { - // we have to resubscribe every time because the subscription is removed every time the ScenePanelTypeSwitcher UI component is unmounted - this.setState({ panelTypeChangeSub: this.subscribeToPanelTypeChange() }); + const addToFiltersSub = this.subscribeToEvent(EventAddLabelToFilters, (event) => { + this.addLabelValueToFilters(event.payload.item); + }); - this.switchToLabelNamesGrid(); - } else { - this.switchToLabelValuesGrid(newState); - } + return { + unsubscribe() { + addToFiltersSub.unsubscribe(); + selectForCompareSub.unsubscribe(); + expandPanelSub.unsubscribe(); + selectLabelSub.unsubscribe(); + }, }; + } + + subscribeToPanelTypeChange() { + const panelTypeSwitcher = findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher; + + return panelTypeSwitcher.subscribeToState( + (newState: ScenePanelTypeSwitcherState, prevState?: ScenePanelTypeSwitcherState) => { + if (newState.panelType !== prevState?.panelType) { + (this.state.body as SceneByVariableRepeaterGrid)?.renderGridItems(); + } + } + ); + } + + renderBody(groupByVariableState: MultiValueVariableState) { + this.state.panelTypeChangeSub?.unsubscribe(); - onChangeState(groupByVariable.state); - clearQuickFilter = true; + if (groupByVariableState.value === 'all') { + // we have to resubscribe every time because the subscription is removed every time the ScenePanelTypeSwitcher UI component is unmounted + this.setState({ panelTypeChangeSub: this.subscribeToPanelTypeChange() }); - return groupByVariable.subscribeToState(onChangeState); + this.switchToLabelNamesGrid(); + } else { + this.switchToLabelValuesGrid(groupByVariableState); + } } switchToLabelNamesGrid() { @@ -183,44 +211,20 @@ export class SceneGroupByLabels extends SceneObjectBase panelType: panelType as PanelType, }; }, - headerActions: (item) => { - const { queryRunnerParams } = item; - - if (!queryRunnerParams.groupBy) { - return [ - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), - new SelectAction({ EventClass: EventAddLabelToFilters, item }), - new FavAction({ item }), - ]; - } - - return [ - new SelectAction({ EventClass: EventSelectLabel, item }), - new SelectAction({ EventClass: EventExpandPanel, item }), - new FavAction({ item }), - ]; - }, + headerActions: (item) => [ + new SelectAction({ EventClass: EventSelectLabel, item }), + new SelectAction({ EventClass: EventExpandPanel, item }), + new FavAction({ item }), + ], }); } - subscribeToPanelTypeChange() { - const panelTypeSwitcher = findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher; - - return panelTypeSwitcher.subscribeToState( - (newState: ScenePanelTypeSwitcherState, prevState?: ScenePanelTypeSwitcherState) => { - if (newState.panelType !== prevState?.panelType) { - (this.state.body as SceneByVariableRepeaterGrid)?.renderGridItems(); - } - } - ); - } - - switchToLabelValuesGrid(newState: MultiValueVariableState) { + switchToLabelValuesGrid(groupByVariableState: MultiValueVariableState) { (findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter).setPlaceholder( 'Search label values (comma-separated regexes are supported)' ); - const { value, options } = newState; + const { value, options } = groupByVariableState; const index = options .filter((o) => o.value !== 'all') @@ -229,7 +233,7 @@ export class SceneGroupByLabels extends SceneObjectBase const startColorIndex = index > -1 ? index : 0; this.setState({ - body: this.buildSceneLabelValuesGrid(newState.value as string, startColorIndex), + body: this.buildSceneLabelValuesGrid(value as string, startColorIndex), }); this.clearCompare(); @@ -240,65 +244,12 @@ export class SceneGroupByLabels extends SceneObjectBase key: 'service-label-values-grid', startColorIndex, label, - headerActions: (item) => { - const { queryRunnerParams } = item; - - if (!queryRunnerParams.groupBy) { - return [ - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), - new SelectAction({ EventClass: EventAddLabelToFilters, item }), - new FavAction({ item }), - ]; - } - - return [ - new SelectAction({ EventClass: EventSelectLabel, item }), - new SelectAction({ EventClass: EventExpandPanel, item }), - new FavAction({ item }), - ]; - }, - }); - } - - subscribeToFiltersChange() { - const filtersVariable = findSceneObjectByClass(this, FiltersVariable) as FiltersVariable; - const noDataSwitcher = findSceneObjectByClass(this, SceneNoDataSwitcher) as SceneNoDataSwitcher; - - // the handler will be called each time a filter is added/removed/modified - return filtersVariable.subscribeToState(() => { - if (noDataSwitcher.state.hideNoData === 'on') { - // we force render because the filters only influence the query made in each panel, not the list of items to render (which come from the groupBy options) - (this.state.body as SceneByVariableRepeaterGrid | SceneLabelValuesGrid).renderGridItems(true); - } - }); - } - - subscribeToPanelEvents() { - const selectLabelSub = this.subscribeToEvent(EventSelectLabel, (event) => { - this.selectLabel(event.payload.item); - }); - - const addToFiltersSub = this.subscribeToEvent(EventAddLabelToFilters, (event) => { - this.addLabelValueToFilters(event.payload.item); - }); - - const expandPanelSub = this.subscribeToEvent(EventExpandPanel, async (event) => { - this.openExpandedPanelDrawer(event.payload.item); - }); - - const selectForCompareSub = this.subscribeToEvent(EventSelectForCompare, (event) => { - const { compareTarget, item } = event.payload; - this.selectForCompare(compareTarget, item); + headerActions: (item) => [ + new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), + new SelectAction({ EventClass: EventAddLabelToFilters, item }), + new FavAction({ item }), + ], }); - - return { - unsubscribe() { - selectForCompareSub.unsubscribe(); - expandPanelSub.unsubscribe(); - addToFiltersSub.unsubscribe(); - selectLabelSub.unsubscribe(); - }, - }; } selectLabel({ queryRunnerParams }: GridItemData) { @@ -312,23 +263,15 @@ export class SceneGroupByLabels extends SceneObjectBase } addLabelValueToFilters(item: GridItemData) { - const filterByVariable = findSceneObjectByClass(this, FiltersVariable) as FiltersVariable; - - let filterToAdd: AdHocVariableFilter; - const { filters, groupBy } = item.queryRunnerParams; + const { filters } = item.queryRunnerParams; if (filters?.[0]) { - filterToAdd = filters?.[0]; - } else if (groupBy?.values.length === 1) { - filterToAdd = { key: groupBy.label, operator: '=', value: groupBy.values[0] }; - } else { - const error = new Error('Cannot build filter! Missing "filters" and "groupBy" value.'); - console.error(error); - console.info(item); - throw error; + const filterToAdd = filters?.[0]; + addFilter(findSceneObjectByClass(this, FiltersVariable) as FiltersVariable, filterToAdd); + return; } - addFilter(filterByVariable, filterToAdd); + console.error('Cannot build filter! Missing "filters" and "groupBy" value.', item); } openExpandedPanelDrawer(item: GridItemData) { @@ -351,7 +294,7 @@ export class SceneGroupByLabels extends SceneObjectBase }); } - selectForCompare(compareTarget: CompareTarget, item: GridItemDataWithStats) { + selectForCompare(compareTarget: CompareTarget, item: GridItemData) { const compare = new Map(this.state.compare); const otherTarget = compareTarget === CompareTarget.BASELINE ? CompareTarget.COMPARISON : CompareTarget.BASELINE; @@ -374,21 +317,18 @@ export class SceneGroupByLabels extends SceneObjectBase const comparePanels = sceneGraph.findAllObjects(this, (o) => o instanceof SceneComparePanel) as SceneComparePanel[]; - let baselineDone = false; - let comparisonDone = false; - + // TODO: optimize if needed + // we can remove the loop if we clear the current selection in the UI before updating the compare map (see selectForCompare() and onClickClearCompareButton()) for (const panel of comparePanels) { - const { key } = panel.state; + const { item } = panel.state; - if (!baselineDone && baselineItem && key === SceneComparePanel.buildPanelKey(baselineItem)) { + if (baselineItem?.value === item.value) { panel.updateCompareTargetValue(CompareTarget.BASELINE); - baselineDone = true; continue; } - if (!comparisonDone && comparisonItem && key === SceneComparePanel.buildPanelKey(comparisonItem)) { + if (comparisonItem?.value === item.value) { panel.updateCompareTargetValue(CompareTarget.COMPARISON); - comparisonDone = true; continue; } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index a3756c30..e68f7cf4 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -15,9 +15,10 @@ import { Spinner } from '@grafana/ui'; import { debounce, isEqual } from 'lodash'; import React from 'react'; +import { FiltersVariable } from '../../../../../../domain/variables/FiltersVariable/FiltersVariable'; +import { GroupByVariable } from '../../../../../../domain/variables/GroupByVariable/GroupByVariable'; import { findSceneObjectByClass } from '../../../../../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../../../../../helpers/getSceneVariableValue'; -import { getSeriesStatsValue } from '../../../../../../helpers/getSeriesStatsValue'; import { buildTimeSeriesQueryRunner } from '../../../../../../infrastructure/timeseries/buildTimeSeriesQueryRunner'; import { SceneEmptyState } from '../../../../../SceneByVariableRepeaterGrid/components/SceneEmptyState/SceneEmptyState'; import { SceneErrorState } from '../../../../../SceneByVariableRepeaterGrid/components/SceneErrorState/SceneErrorState'; @@ -36,28 +37,25 @@ import { SceneQuickFilterState, } from '../../../../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; import { sortFavGridItems } from '../../../../../SceneByVariableRepeaterGrid/domain/sortFavGridItems'; -import { addRefId, addStats } from '../../../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; +import { + addRefId, + addStats, + sortSeries, +} from '../../../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; import { GridItemData } from '../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; import { EventDataReceived } from '../../../../../SceneLabelValuesTimeseries/domain/events/EventDataReceived'; import { SceneGroupByLabels } from '../../SceneGroupByLabels'; import { CompareTarget } from './components/SceneComparePanel/ui/ComparePanel'; import { SceneLabelValuePanel } from './components/SceneLabelValuePanel'; -export type GridItemDataWithStats = GridItemData & { - stats: { - allValuesSum: number; - unit: string; - }; -}; - export interface SceneLabelValuesGridState extends EmbeddedSceneState { $data: SceneDataProvider; isLoading: boolean; - items: GridItemDataWithStats[]; + items: GridItemData[]; label: string; startColorIndex: number; headerActions: (item: GridItemData, items: GridItemData[]) => VizPanelState['headerActions']; - sortItemsFn: (a: GridItemDataWithStats, b: GridItemDataWithStats) => number; + sortItemsFn: (a: GridItemData, b: GridItemData) => number; hideNoData: boolean; } @@ -89,7 +87,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase { + filtersSub.unsubscribe(); hideNoDataSub.unsubscribe(); layoutChangeSub.unsubscribe(); quickFilterSub.unsubscribe(); refreshSub.unsubscribe(); + groupBySub.unsubscribe(); }; } - fetchData() { - this.setState({ isLoading: true }); - + subscribeOnceToDataChange(forceRender = false) { const dataSub = this.state.$data.subscribeToState((newState) => { - if (newState.data?.state !== LoadingState.Loading) { - dataSub.unsubscribe(); + if (newState.data?.state === LoadingState.Loading) { + return; + } - this.renderGridItems(); + dataSub.unsubscribe(); + + this.renderGridItems(forceRender); + + this.setState({ isLoading: false }); + }); + } + + subscribeToGroupByChange() { + const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; - this.setState({ isLoading: false }); + return groupByVariable.subscribeToState((newState, prevState) => { + if (!newState.loading && prevState.loading) { + this.refetchData(); } }); } subscribeToRefreshClick() { const onClickRefresh = () => { - this.fetchData(); + this.refetchData(); }; // start of hack, for a better UX: we disable the variable "refresh" option and we allow the user to reload the list only by clicking on the "Refresh" button @@ -201,20 +214,43 @@ export class SceneLabelValuesGrid extends SceneObjectBase { if (newState.hideNoData !== prevState?.hideNoData) { this.setState({ hideNoData: newState.hideNoData === 'on' }); - // we force render because this.state.items certainly have not changed but we want to update the UI panels anyway - this.renderGridItems(true); + this.refetchData(true); } }; - onChangeState(noDataSwitcher.state); - return noDataSwitcher.subscribeToState(onChangeState); } + subscribeToFiltersChange() { + const filtersVariable = findSceneObjectByClass(this, FiltersVariable) as FiltersVariable; + const noDataSwitcher = findSceneObjectByClass(this, SceneNoDataSwitcher) as SceneNoDataSwitcher; + + // the handler will be called each time a filter is added/removed/modified + return filtersVariable.subscribeToState(() => { + if (noDataSwitcher.state.hideNoData === 'on') { + // to be sure the list is updated we refetch because the filters only influence the query made in each panel + this.refetchData(); + } + }); + } + + refetchData(forceRender = false) { + this.setState({ + $data: new SceneDataTransformer({ + $data: buildTimeSeriesQueryRunner({ groupBy: { label: this.state.label } }), + transformations: [addRefId, addStats, sortSeries], + }), + }); + + this.subscribeOnceToDataChange(forceRender); + } + shouldRenderItems(newItems: SceneLabelValuesGridState['items']) { const { items } = this.state; @@ -231,30 +267,26 @@ export class SceneLabelValuesGrid extends SceneObjectBase (getSeriesStatsValue(s2, 'allValuesSum') || 0) - (getSeriesStatsValue(s1, 'allValuesSum') || 0)) - .map((s, index) => { - const metricField = s.fields[1]; - const labelFromSerieLabels = metricField.labels?.[label]; - const labelFromSerieName = metricField.name; - const labelValue = labelFromSerieLabels || labelFromSerieName; - - return { - index: startColorIndex + index, - value: labelValue, - label: labelValue, - queryRunnerParams: { - serviceName, - profileMetricId, - filters: [{ key: label, operator: '=', value: labelFromSerieLabels || '' }], - }, - panelType: PanelType.TIMESERIES, - stats: { - allValuesSum: getSeriesStatsValue(s, 'allValuesSum') || 0, - unit: metricField.config.unit as string, - }, - }; - }); + // the series are already sorted by the data transformation + const items = series.map((s, index) => { + const metricField = s.fields[1]; + const labelValueFromFieldLabels = metricField.labels?.[label]; // can be empty when the ingested profiles do not have a label value set + const labelValue = labelValueFromFieldLabels || metricField.name; // ensures a non-empy value for the UI + const labelName = labelValue; + + return { + index: startColorIndex + index, + value: labelValue, + label: labelName, + queryRunnerParams: { + serviceName, + profileMetricId, + // defaults to an "is empty" operator in the UI when the label value is not set + filters: [{ key: label, operator: '=', value: labelValueFromFieldLabels || '' }], + }, + panelType: PanelType.TIMESERIES, + }; + }); return this.filterItems(items).sort(sortItemsFn); } @@ -304,7 +336,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase) { + buildVizPanel(item: GridItemData, compare: Map) { const vizPanel = new SceneLabelValuePanel({ item, headerActions: this.state.headerActions.bind(null, item, this.state.items), @@ -312,20 +344,20 @@ export class SceneLabelValuesGrid extends SceneObjectBase { - // we might have to consider if we update the item.stats here (will impact sorting if we don't do it) + if (!this.state.hideNoData || event.payload.series.length) { + return; + } - if (this.state.hideNoData && !event.payload.series.length) { - const gridItem = sceneGraph.getAncestor(vizPanel, SceneCSSGridItem); - const { key: gridItemKey } = gridItem.state; - const grid = sceneGraph.getAncestor(gridItem, SceneCSSGridLayout); + const gridItem = sceneGraph.getAncestor(vizPanel, SceneCSSGridItem); + const { key: gridItemKey } = gridItem.state; + const grid = sceneGraph.getAncestor(gridItem, SceneCSSGridLayout); - const filteredChildren = grid.state.children.filter((c) => c.state.key !== gridItemKey); + const filteredChildren = grid.state.children.filter((c) => c.state.key !== gridItemKey); - if (!filteredChildren.length) { - this.renderEmptyState(); - } else { - grid.setState({ children: filteredChildren }); - } + if (!filteredChildren.length) { + this.renderEmptyState(); + } else { + grid.setState({ children: filteredChildren }); } }); @@ -338,7 +370,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase) { + getItemCompareTargetValue(item: GridItemData, compare: Map) { if (compare.get(CompareTarget.BASELINE)?.value === item.value) { return CompareTarget.BASELINE; } @@ -375,9 +407,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase { static WIDTH_IN_PIXELS = 180; - static buildPanelKey(item: GridItemDataWithStats) { - return `compare-panel-${item.value}`; - } - - constructor({ item, compareTargetValue }: { item: GridItemDataWithStats; compareTargetValue?: CompareTarget }) { + constructor({ item, compareTargetValue }: { item: GridItemData; compareTargetValue?: CompareTarget }) { super({ - key: SceneComparePanel.buildPanelKey(item), item, + itemStats: undefined, compareTargetValue, }); } @@ -29,13 +31,12 @@ export class SceneComparePanel extends SceneObjectBase { this.setState({ compareTargetValue }); } - getStats() { - return this.state.item.stats; + getItemStats() { + return this.state.itemStats; } - updateStats(stats: GridItemDataWithStats['stats']) { - const { item } = this.state; - this.setState({ item: { ...item, stats } }); + updateStats(itemStats: ItemStats) { + this.setState({ itemStats }); } onChangeCompareTarget = (compareTarget: CompareTarget) => { @@ -44,11 +45,12 @@ export class SceneComparePanel extends SceneObjectBase { }; static Component({ model }: SceneComponentProps) { - const { item, compareTargetValue } = model.useState(); + const { item, itemStats, compareTargetValue } = model.useState(); return ( diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel.tsx index d683872b..03343117 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel.tsx @@ -1,15 +1,17 @@ import { css } from '@emotion/css'; import { getValueFormat, GrafanaTheme2 } from '@grafana/data'; -import { RadioButtonGroup, useStyles2 } from '@grafana/ui'; +import { RadioButtonGroup, Spinner, useStyles2 } from '@grafana/ui'; import React, { useMemo } from 'react'; +import { GridItemData } from 'src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/types/GridItemData'; import { getColorByIndex } from '../../../../../../../../../helpers/getColorByIndex'; -import { GridItemDataWithStats } from '../../../SceneLabelValuesGrid'; +import { ItemStats } from '../SceneComparePanel'; export type ComparePanelProps = { - item: GridItemDataWithStats; + item: GridItemData; + itemStats?: ItemStats; compareTargetValue?: CompareTarget; - onChangeCompareTarget: (compareTarget: CompareTarget, item: GridItemDataWithStats) => void; + onChangeCompareTarget: (compareTarget: CompareTarget, item: GridItemData) => void; }; export enum CompareTarget { @@ -17,18 +19,23 @@ export enum CompareTarget { COMPARISON = 'comparison', } -export function ComparePanel({ item, compareTargetValue, onChangeCompareTarget }: ComparePanelProps) { +export function ComparePanel({ item, itemStats, compareTargetValue, onChangeCompareTarget }: ComparePanelProps) { const styles = useStyles2(getStyles); - const { index, value, stats } = item; - const { allValuesSum, unit } = stats; + const { index, value } = item; const color = getColorByIndex(index); const total = useMemo(() => { - const formattedValue = getValueFormat(unit)(allValuesSum); - return `${formattedValue.text}${formattedValue.suffix}`; - }, [allValuesSum, unit]); + if (!itemStats) { + return ; + } + + const { allValuesSum, unit } = itemStats; + const { text, suffix } = getValueFormat(unit)(allValuesSum); + + return `${text}${suffix}`; + }, [itemStats]); const options = useMemo( () => [ diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx index 7e0e9a78..a68ed09a 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx @@ -4,11 +4,11 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanelState } import { useStyles2 } from '@grafana/ui'; import React from 'react'; -import { getSeriesStatsValue } from '../../../../../../../helpers/getSeriesStatsValue'; import { GridItemData } from '../../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; import { EventDataReceived } from '../../../../../../SceneLabelValuesTimeseries/domain/events/EventDataReceived'; import { SceneLabelValuesTimeseries } from '../../../../../../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; -import { GRID_AUTO_ROWS, GridItemDataWithStats } from '../SceneLabelValuesGrid'; +import { getSeriesStatsValue } from '../domain/getSeriesStatsValue'; +import { GRID_AUTO_ROWS } from '../SceneLabelValuesGrid'; import { SceneComparePanel } from './SceneComparePanel/SceneComparePanel'; import { CompareTarget } from './SceneComparePanel/ui/ComparePanel'; @@ -18,7 +18,7 @@ interface SceneLabelValuesStatAndTimeseriesState extends SceneObjectState { } export class SceneLabelValuePanel extends SceneObjectBase { - static buildPanelKey(item: GridItemDataWithStats) { + static buildPanelKey(item: GridItemData) { return `compare-panel-${item.value}`; } @@ -27,7 +27,7 @@ export class SceneLabelValuePanel extends SceneObjectBase VizPanelState['headerActions']; compareTargetValue?: CompareTarget; }) { @@ -47,7 +47,7 @@ export class SceneLabelValuePanel extends SceneObjectBase { diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx index 3a6d4554..7b5538ac 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx @@ -13,10 +13,10 @@ import React from 'react'; import { getColorByIndex } from '../helpers/getColorByIndex'; import { getLabelFieldName } from '../helpers/getLabelFieldName'; -import { getSeriesStatsValue } from '../helpers/getSeriesStatsValue'; import { buildTimeSeriesQueryRunner } from '../infrastructure/timeseries/buildTimeSeriesQueryRunner'; import { addRefId, addStats, sortSeries } from './SceneByVariableRepeaterGrid/infrastructure/data-transformations'; import { GridItemData } from './SceneByVariableRepeaterGrid/types/GridItemData'; +import { getSeriesStatsValue } from './SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/getSeriesStatsValue'; import { EventDataReceived } from './SceneLabelValuesTimeseries/domain/events/EventDataReceived'; interface SceneLabelValuesBarGaugeState extends SceneObjectState { diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx index bd8e595d..64b8614c 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx @@ -13,7 +13,6 @@ import React from 'react'; import { getColorByIndex } from '../../helpers/getColorByIndex'; import { getLabelFieldName } from '../../helpers/getLabelFieldName'; -import { getSeriesStatsValue } from '../../helpers/getSeriesStatsValue'; import { LabelsDataSource } from '../../infrastructure/labels/LabelsDataSource'; import { buildTimeSeriesQueryRunner } from '../../infrastructure/timeseries/buildTimeSeriesQueryRunner'; import { @@ -23,6 +22,7 @@ import { sortSeries, } from '../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; import { GridItemData } from '../SceneByVariableRepeaterGrid/types/GridItemData'; +import { getSeriesStatsValue } from '../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/getSeriesStatsValue'; import { EventDataReceived } from './domain/events/EventDataReceived'; interface SceneLabelValuesTimeseriesState extends SceneObjectState { @@ -179,6 +179,6 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase) { const { body } = model.useState(); - return ; + return ; } } From acf4b35b64f259c570c9ae87e6945c05282b63a0 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 8 Aug 2024 18:02:03 +0200 Subject: [PATCH 13/76] fix(SceneLabelValuesGrid): Add missing loading state change --- .../components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index e68f7cf4..3ff61585 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -242,6 +242,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase Date: Thu, 8 Aug 2024 18:07:58 +0200 Subject: [PATCH 14/76] feat(SceneMainServiceTimeseries): Preserve color when an item is provided --- .../SceneExploreServiceFlameGraph.tsx | 1 + .../SceneExploreServiceLabels.tsx | 1 + .../components/SceneMainServiceTimeseries.tsx | 20 +++++++++++++++---- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx index e0a5e990..268c295d 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx @@ -25,6 +25,7 @@ export class SceneExploreServiceFlameGraph extends SceneObjectBase [ new SelectAction({ EventClass: EventViewServiceLabels, item }), new FavAction({ item }), diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx index e8f1941d..f8dd0616 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx @@ -38,6 +38,7 @@ export class SceneExploreServiceLabels extends SceneObjectBase [ new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), new FavAction({ item }), diff --git a/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx index 3640fda9..42d00971 100644 --- a/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx @@ -15,6 +15,8 @@ import { GridItemData } from './SceneByVariableRepeaterGrid/types/GridItemData'; import { SceneLabelValuesTimeseries } from './SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; interface SceneMainServiceTimeseriesState extends SceneObjectState { + item?: GridItemData; + headerActions: (item: GridItemData) => VizPanelState['headerActions']; body?: SceneLabelValuesTimeseries; } @@ -28,19 +30,29 @@ export class SceneMainServiceTimeseries extends SceneObjectBase VizPanelState['headerActions'] }) { + constructor({ + item, + headerActions, + }: { + item: SceneMainServiceTimeseriesState['item']; + headerActions: SceneMainServiceTimeseriesState['headerActions']; + }) { super({ + item, + headerActions, body: undefined, }); - this.addActivationHandler(this.onActivate.bind(this, headerActions)); + this.addActivationHandler(this.onActivate.bind(this)); } - onActivate(headerActions: (item: GridItemData) => VizPanelState['headerActions']) { + onActivate() { + const { item, headerActions } = this.state; + this.setState({ body: new SceneLabelValuesTimeseries({ item: { - index: 0, + index: item ? item.index : 0, value: '', label: this.buildTitle(), panelType: PanelType.TIMESERIES, From 08311d516da4c3ebe9b5bc9d753aa5dd4365eb77 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 8 Aug 2024 18:11:44 +0200 Subject: [PATCH 15/76] feat: Comment previous change --- .../components/SceneMainServiceTimeseries.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx index 42d00971..b3c36963 100644 --- a/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx @@ -47,12 +47,15 @@ export class SceneMainServiceTimeseries extends SceneObjectBase Date: Fri, 9 Aug 2024 11:52:21 +0200 Subject: [PATCH 16/76] refactor(*): Better naming and files org --- .../SceneGroupByLabels/SceneGroupByLabels.tsx | 16 ++++++------ .../SceneLabelValuesGrid.tsx | 2 +- .../components/SceneLabelValuePanel.tsx | 26 +++++++++---------- .../SceneStatsPanel.tsx} | 15 ++++++----- .../ui/StatsPanel.tsx} | 12 +++------ .../SceneLabelValuesGrid/domain/types.ts | 4 +++ .../ui/CompareActions.tsx | 8 +++--- .../domain/events/EventSelectForCompare.tsx | 4 +-- 8 files changed, 44 insertions(+), 43 deletions(-) rename src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/{SceneComparePanel/SceneComparePanel.tsx => SceneStatsPanel/SceneStatsPanel.tsx} (75%) rename src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/{SceneComparePanel/ui/ComparePanel.tsx => SceneStatsPanel/ui/StatsPanel.tsx} (90%) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types.ts rename src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/{ => components/SceneGroupByLabels}/domain/events/EventSelectForCompare.tsx (56%) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index 123f726f..357c47ed 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -43,11 +43,11 @@ import { SceneDrawer } from '../../../SceneDrawer'; import { SceneLabelValuesBarGauge } from '../../../SceneLabelValuesBarGauge'; import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; import { SceneProfilesExplorer } from '../../../SceneProfilesExplorer/SceneProfilesExplorer'; -import { EventSelectForCompare } from '../../domain/events/EventSelectForCompare'; -import { SceneComparePanel } from './components/SceneLabelValuesGrid/components/SceneComparePanel/SceneComparePanel'; -import { CompareTarget } from './components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel'; +import { SceneStatsPanel } from './components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel'; +import { CompareTarget } from './components/SceneLabelValuesGrid/domain/types'; import { SceneLabelValuesGrid } from './components/SceneLabelValuesGrid/SceneLabelValuesGrid'; import { CompareActions } from './components/SceneLabelValuesGrid/ui/CompareActions'; +import { EventSelectForCompare } from './domain/events/EventSelectForCompare'; export interface SceneGroupByLabelsState extends SceneObjectState { body?: SceneObject; @@ -307,19 +307,19 @@ export class SceneGroupByLabels extends SceneObjectBase this.setState({ compare }); - this.updateComparePanels(); + this.updateStatsPanels(); } - updateComparePanels() { + updateStatsPanels() { const { compare } = this.state; const baselineItem = compare.get(CompareTarget.BASELINE); const comparisonItem = compare.get(CompareTarget.COMPARISON); - const comparePanels = sceneGraph.findAllObjects(this, (o) => o instanceof SceneComparePanel) as SceneComparePanel[]; + const statsPanels = sceneGraph.findAllObjects(this, (o) => o instanceof SceneStatsPanel) as SceneStatsPanel[]; // TODO: optimize if needed // we can remove the loop if we clear the current selection in the UI before updating the compare map (see selectForCompare() and onClickClearCompareButton()) - for (const panel of comparePanels) { + for (const panel of statsPanels) { const { item } = panel.state; if (baselineItem?.value === item.value) { @@ -400,7 +400,7 @@ export class SceneGroupByLabels extends SceneObjectBase onClickClearCompareButton = () => { this.clearCompare(); - this.updateComparePanels(); + this.updateStatsPanels(); }; static Component = ({ model }: SceneComponentProps) => { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index 3ff61585..8f247d88 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -45,8 +45,8 @@ import { import { GridItemData } from '../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; import { EventDataReceived } from '../../../../../SceneLabelValuesTimeseries/domain/events/EventDataReceived'; import { SceneGroupByLabels } from '../../SceneGroupByLabels'; -import { CompareTarget } from './components/SceneComparePanel/ui/ComparePanel'; import { SceneLabelValuePanel } from './components/SceneLabelValuePanel'; +import { CompareTarget } from './domain/types'; export interface SceneLabelValuesGridState extends EmbeddedSceneState { $data: SceneDataProvider; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx index a68ed09a..d97139b7 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx @@ -8,12 +8,12 @@ import { GridItemData } from '../../../../../../SceneByVariableRepeaterGrid/type import { EventDataReceived } from '../../../../../../SceneLabelValuesTimeseries/domain/events/EventDataReceived'; import { SceneLabelValuesTimeseries } from '../../../../../../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; import { getSeriesStatsValue } from '../domain/getSeriesStatsValue'; +import { CompareTarget } from '../domain/types'; import { GRID_AUTO_ROWS } from '../SceneLabelValuesGrid'; -import { SceneComparePanel } from './SceneComparePanel/SceneComparePanel'; -import { CompareTarget } from './SceneComparePanel/ui/ComparePanel'; +import { SceneStatsPanel } from './SceneStatsPanel/SceneStatsPanel'; interface SceneLabelValuesStatAndTimeseriesState extends SceneObjectState { - comparePanel: SceneComparePanel; + statsPanel: SceneStatsPanel; timeseriesPanel: SceneLabelValuesTimeseries; } @@ -33,7 +33,7 @@ export class SceneLabelValuePanel extends SceneObjectBase { const [s] = event.payload.series; const allValuesSum = s ? getSeriesStatsValue(s, 'allValuesSum') || 0 : 0; - if (comparePanel.getItemStats()?.allValuesSum !== allValuesSum) { - comparePanel.updateStats({ + if (statsPanel.getStats()?.allValuesSum !== allValuesSum) { + statsPanel.updateStats({ allValuesSum, unit: s ? (s.fields[1].config.unit as string) : 'short', }); @@ -62,13 +62,13 @@ export class SceneLabelValuePanel extends SceneObjectBase) { const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks - const { comparePanel, timeseriesPanel } = model.useState(); - const { compareTargetValue } = comparePanel.useState(); + const { statsPanel, timeseriesPanel } = model.useState(); + const { compareTargetValue } = statsPanel.useState(); return (
-
- +
+
@@ -93,8 +93,8 @@ const getStyles = (theme: GrafanaTheme2) => ({ min-height: ${GRID_AUTO_ROWS}; } `, - comparePanel: css` - width: ${SceneComparePanel.WIDTH_IN_PIXELS}px; + statsPanel: css` + width: ${SceneStatsPanel.WIDTH_IN_PIXELS}px; &.selected > div { border-top: 1px solid ${theme.colors.primary.main}; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx similarity index 75% rename from src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/SceneComparePanel.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx index 7f782c50..46c54516 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx @@ -2,21 +2,22 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana import React from 'react'; import { GridItemData } from '../../../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; -import { EventSelectForCompare } from '../../../../../../domain/events/EventSelectForCompare'; -import { ComparePanel, CompareTarget } from './ui/ComparePanel'; +import { EventSelectForCompare } from '../../../../domain/events/EventSelectForCompare'; +import { CompareTarget } from '../../domain/types'; +import { StatsPanel } from './ui/StatsPanel'; export type ItemStats = { allValuesSum: number; unit: string; }; -interface SceneComparePanelState extends SceneObjectState { +interface SceneStatsPanelState extends SceneObjectState { item: GridItemData; itemStats?: ItemStats; compareTargetValue?: CompareTarget; } -export class SceneComparePanel extends SceneObjectBase { +export class SceneStatsPanel extends SceneObjectBase { static WIDTH_IN_PIXELS = 180; constructor({ item, compareTargetValue }: { item: GridItemData; compareTargetValue?: CompareTarget }) { @@ -31,7 +32,7 @@ export class SceneComparePanel extends SceneObjectBase { this.setState({ compareTargetValue }); } - getItemStats() { + getStats() { return this.state.itemStats; } @@ -44,11 +45,11 @@ export class SceneComparePanel extends SceneObjectBase { this.publishEvent(new EventSelectForCompare({ compareTarget, item }), true); }; - static Component({ model }: SceneComponentProps) { + static Component({ model }: SceneComponentProps) { const { item, itemStats, compareTargetValue } = model.useState(); return ( - void; }; -export enum CompareTarget { - BASELINE = 'baseline', - COMPARISON = 'comparison', -} - -export function ComparePanel({ item, itemStats, compareTargetValue, onChangeCompareTarget }: ComparePanelProps) { +export function StatsPanel({ item, itemStats, compareTargetValue, onChangeCompareTarget }: StatsPanelProps) { const styles = useStyles2(getStyles); const { index, value } = item; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types.ts b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types.ts new file mode 100644 index 00000000..0d7fd5a1 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types.ts @@ -0,0 +1,4 @@ +export enum CompareTarget { + BASELINE = 'baseline', + COMPARISON = 'comparison', +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx index 9f771861..06bca52e 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx @@ -5,8 +5,8 @@ import { noOp } from '@shared/domain/noOp'; import React from 'react'; import { SceneGroupByLabelsState } from '../../../SceneGroupByLabels'; -import { SceneComparePanel } from '../components/SceneComparePanel/SceneComparePanel'; -import { CompareTarget } from '../components/SceneComparePanel/ui/ComparePanel'; +import { SceneStatsPanel } from '../components/SceneStatsPanel/SceneStatsPanel'; +import { CompareTarget } from '../domain/types'; type CompareButtonProps = { compare: SceneGroupByLabelsState['compare']; @@ -53,10 +53,10 @@ const getStyles = (theme: GrafanaTheme2) => ({ container: css` display: flex; align-items: center; - width: ${SceneComparePanel.WIDTH_IN_PIXELS}px; + width: ${SceneStatsPanel.WIDTH_IN_PIXELS}px; `, compareButton: css` - width: ${SceneComparePanel.WIDTH_IN_PIXELS - 32}px; + width: ${SceneStatsPanel.WIDTH_IN_PIXELS - 32}px; border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/domain/events/EventSelectForCompare.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/domain/events/EventSelectForCompare.tsx similarity index 56% rename from src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/domain/events/EventSelectForCompare.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/domain/events/EventSelectForCompare.tsx index 4c16d799..fe4aae0c 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/domain/events/EventSelectForCompare.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/domain/events/EventSelectForCompare.tsx @@ -1,7 +1,7 @@ import { BusEventWithPayload } from '@grafana/data'; -import { GridItemData } from '../../../SceneByVariableRepeaterGrid/types/GridItemData'; -import { CompareTarget } from '../../components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneComparePanel/ui/ComparePanel'; +import { GridItemData } from '../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; +import { CompareTarget } from '../../components/SceneLabelValuesGrid/domain/types'; export interface EventSelectForComparePayload { compareTarget: CompareTarget; From d9bf0ee0db185d8cde42badd3441a67a8f937983 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 9 Aug 2024 13:54:29 +0200 Subject: [PATCH 17/76] feat(*): WiP --- .../infrastructure/useFetchDiffProfile.ts | 10 + .../SceneExploreDiffFlameGraphs.tsx | 165 +++++++++++ .../components/SceneComparePanel.tsx | 257 ++++++++++++++++++ .../SceneTimerangeSelectionTypeSwitcher.tsx | 101 +++++++ .../EventSwitchTimerangeSelectionType.ts | 11 + .../SceneProfilesExplorer.tsx | 35 ++- .../ui/ExplorationTypeSelector.tsx | 34 ++- .../FiltersVariable/FiltersVariable.tsx | 5 +- 8 files changed, 604 insertions(+), 14 deletions(-) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel.tsx create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneTimerangeSelectionTypeSwitcher.tsx create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/events/EventSwitchTimerangeSelectionType.ts diff --git a/src/pages/ComparisonView/components/FlameGraphContainer/infrastructure/useFetchDiffProfile.ts b/src/pages/ComparisonView/components/FlameGraphContainer/infrastructure/useFetchDiffProfile.ts index 7a110836..69ebfe39 100644 --- a/src/pages/ComparisonView/components/FlameGraphContainer/infrastructure/useFetchDiffProfile.ts +++ b/src/pages/ComparisonView/components/FlameGraphContainer/infrastructure/useFetchDiffProfile.ts @@ -12,6 +12,16 @@ export function useFetchDiffProfile({ disabled }: FetchParams) { const [maxNodes] = useMaxNodesFromUrl(); const { left, right } = useLeftRightParamsFromUrl(); + // console.log( + // '*** useFetchDiffProfile', + // left.query, + // right.query, + // left.timeRange.raw.from.valueOf(), + // left.timeRange.raw.to.valueOf(), + // right.timeRange.raw.from.valueOf(), + // right.timeRange.raw.to.valueOf() + // ); + const { isFetching, error, data, refetch } = useQuery({ // for UX: keep previous data while fetching -> profile does not re-render with empty panels when refreshing placeholderData: (previousData) => previousData, diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx new file mode 100644 index 00000000..37285838 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx @@ -0,0 +1,165 @@ +import { css } from '@emotion/css'; +import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data'; +import { behaviors, SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { Spinner, useStyles2 } from '@grafana/ui'; +import { AiPanel } from '@shared/components/AiPanel/AiPanel'; +import { AIButton } from '@shared/components/AiPanel/components/AIButton'; +import { FlameGraph } from '@shared/components/FlameGraph/FlameGraph'; +import { useToggleSidePanel } from '@shared/domain/useToggleSidePanel'; +import { useFetchPluginSettings } from '@shared/infrastructure/settings/useFetchPluginSettings'; +import { FlamebearerProfile } from '@shared/types/FlamebearerProfile'; +import React, { useEffect } from 'react'; + +import { useFetchDiffProfile } from '../../../../pages/ComparisonView/components/FlameGraphContainer/infrastructure/useFetchDiffProfile'; +import { useDefaultComparisonParamsFromUrl } from '../../../../pages/ComparisonView/domain/useDefaultComparisonParamsFromUrl'; +import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; +import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; +import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; +import { CompareTarget } from '../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; +import { SceneComparePanel } from './components/SceneComparePanel'; + +interface SceneExploreDiffFlameGraphsState extends SceneObjectState { + baselinePanel: SceneComparePanel; + comparisonPanel: SceneComparePanel; +} + +export class SceneExploreDiffFlameGraphs extends SceneObjectBase { + constructor() { + super({ + key: 'explore-diff-flame-graphs', + baselinePanel: new SceneComparePanel({ + target: CompareTarget.BASELINE, + }), + comparisonPanel: new SceneComparePanel({ + target: CompareTarget.COMPARISON, + }), + $behaviors: [ + new behaviors.CursorSync({ + key: 'metricCrosshairSync', + sync: DashboardCursorSync.Crosshair, + }), + ], + }); + + this.addActivationHandler(this.onActivate.bind(this)); + } + + onActivate() {} + + // see SceneProfilesExplorer + getVariablesAndGridControls() { + return { + variables: [ + findSceneObjectByClass(this, ServiceNameVariable) as ServiceNameVariable, + findSceneObjectByClass(this, ProfileMetricVariable) as ProfileMetricVariable, + ], + gridControls: [], + }; + } + + static Component({ model }: SceneComponentProps) { + const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks + + const { baselinePanel, comparisonPanel } = model.useState(); + + useDefaultComparisonParamsFromUrl(); // eslint-disable-line react-hooks/rules-of-hooks + const { isFetching, error, profile } = useFetchDiffProfile({}); // eslint-disable-line react-hooks/rules-of-hooks + const noProfileDataAvailable = !error && profile?.flamebearer.numTicks === 0; + const shouldDisplayFlamegraph = Boolean(!error && !noProfileDataAvailable && profile); + + const { settings /*, error: isFetchingSettingsError*/ } = useFetchPluginSettings(); // eslint-disable-line react-hooks/rules-of-hooks + + const sidePanel = useToggleSidePanel(); // eslint-disable-line react-hooks/rules-of-hooks + + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (isFetching) { + sidePanel.close(); + } + }, [isFetching, sidePanel]); + + // console.log('*** Component', left, right, isFetching, error, profile); + + return ( +
+
+ + +
+ +
+
+ {isFetching && } + + {shouldDisplayFlamegraph && ( + <> +
+ { + sidePanel.open('ai'); + }} + disabled={isFetching || noProfileDataAvailable || sidePanel.isOpen('ai')} + interactionName="g_pyroscope_app_explain_flamegraph_clicked" + > + Explain Flame Graph + +
+ + + + )} +
+ + {sidePanel.isOpen('ai') && } +
+
+ ); + } +} + +const getStyles = (theme: GrafanaTheme2) => ({ + flex: css` + display: flex; + `, + container: css` + width: 100%; + display: flex; + flex-direction: column; + `, + columns: css` + display: flex; + flex-direction: row; + gap: ${theme.spacing(1)}; + margin-bottom: ${theme.spacing(1)}; + + & > div { + flex: 1 1 0; + } + `, + flameGraphPanel: css` + min-width: 0; + flex-grow: 1; + width: 100%; + padding: ${theme.spacing(1)}; + border: 1px solid ${theme.colors.border.weak}; + border-radius: 2px; + `, + flameGraphHeaderActions: css` + display: flex; + align-items: flex-end; + + & > button { + margin-left: auto; + } + `, + sidePanel: css` + flex: 1 0 50%; + margin-left: 8px; + max-width: calc(50% - 4px); + `, +}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel.tsx new file mode 100644 index 00000000..0a365dd0 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel.tsx @@ -0,0 +1,257 @@ +import { css } from '@emotion/css'; +import { dateTime, FieldType, GrafanaTheme2, MutableDataFrame } from '@grafana/data'; +import { + PanelBuilders, + SceneComponentProps, + sceneGraph, + SceneObjectBase, + SceneObjectState, + SceneQueryRunner, + SceneTimePicker, + SceneTimeRange, + SceneTimeRangeState, + VariableDependencyConfig, + VizPanel, +} from '@grafana/scenes'; +import { GraphGradientMode, InlineLabel, useStyles2 } from '@grafana/ui'; +import { getProfileMetric, ProfileMetricId } from '@shared/infrastructure/profile-metrics/getProfileMetric'; +import React from 'react'; +import { BASELINE_COLORS, COMPARISON_COLORS } from 'src/pages/ComparisonView/ui/colors'; +import { FiltersVariable } from 'src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable'; +import { findSceneObjectByClass } from 'src/pages/ProfilesExplorerView/helpers/findSceneObjectByClass'; +import { getSceneVariableValue } from 'src/pages/ProfilesExplorerView/helpers/getSceneVariableValue'; +import { PYROSCOPE_DATA_SOURCE } from 'src/pages/ProfilesExplorerView/infrastructure/pyroscope-data-sources'; +import { getProfileMetricLabel } from 'src/pages/ProfilesExplorerView/infrastructure/series/helpers/getProfileMetricLabel'; + +import { CompareTarget } from '../../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; +import { EventSwitchTimerangeSelectionType } from '../domain/events/EventSwitchTimerangeSelectionType'; +import { SceneTimerangeSelectionTypeSwitcher } from './SceneTimerangeSelectionTypeSwitcher'; + +export interface SceneComparePanelState extends SceneObjectState { + target: CompareTarget; + title: string; + filterKey: 'filtersBaseline' | 'filtersComparison'; + color: string; + annotationColor: string; + timePicker: SceneTimePicker; + timeseries?: VizPanel; +} + +function buildCompareTimeSeriesQueryRunner({ filterKey }: { filterKey: 'filtersBaseline' | 'filtersComparison' }) { + return new SceneQueryRunner({ + datasource: PYROSCOPE_DATA_SOURCE, + queries: [ + { + refId: `$profileMetricId-$serviceName-${filterKey}}`, + queryType: 'metrics', + profileTypeId: '$profileMetricId', + labelSelector: `{service_name="$serviceName",$${filterKey}}`, + }, + ], + }); +} + +const getDefaultTimeRange = (): SceneTimeRangeState => { + const now = dateTime(); + + return { + from: 'now-5m', + to: 'now', + value: { + from: dateTime(now).subtract(5, 'minutes'), + to: now, + raw: { from: 'now-5m', to: 'now' }, + }, + }; +}; + +class RangeAnnotation extends MutableDataFrame { + constructor() { + super(); + [ + { + name: 'time', + type: FieldType.time, + }, + { + name: 'timeEnd', + type: FieldType.time, + }, + { + name: 'isRegion', + type: FieldType.boolean, + }, + { + name: 'color', + type: FieldType.other, + }, + { + name: 'text', + type: FieldType.string, + }, + ].forEach((field) => this.addField(field)); + } + + addRange(entry: { time: number; timeEnd: number; color?: string; text: string }) { + this.add({ ...entry, isRegion: true }); + } +} + +export class SceneComparePanel extends SceneObjectBase { + protected _variableDependency = new VariableDependencyConfig(this, { + variableNames: ['profileMetricId'], + onVariableUpdateCompleted: () => { + this.state.timeseries?.setState({ + title: this.buildTimeseriesTitle(), + }); + }, + }); + + constructor({ target }: { target: SceneComparePanelState['target'] }) { + super({ + key: `diff-panel-${target}`, + target, + title: target === CompareTarget.BASELINE ? 'Baseline' : 'Comparison', + filterKey: target === CompareTarget.BASELINE ? 'filtersBaseline' : 'filtersComparison', + color: target === CompareTarget.BASELINE ? BASELINE_COLORS.COLOR.toString() : COMPARISON_COLORS.COLOR.toString(), + annotationColor: + target === CompareTarget.BASELINE ? BASELINE_COLORS.OVERLAY.toString() : COMPARISON_COLORS.OVERLAY.toString(), + $timeRange: new SceneTimeRange(getDefaultTimeRange()), + timePicker: new SceneTimePicker({ isOnCanvas: true }), + timeseries: undefined, + }); + + this.addActivationHandler(this.onActivate.bind(this)); + } + + onActivate() { + const { filterKey, color, title, annotationColor } = this.state; + + this.subscribeToEvent(EventSwitchTimerangeSelectionType, (event) => { + const { type } = event.payload; + console.log('*** EventSwitchTimerangeSelectionType', event, type); + }); + + const timeseries = PanelBuilders.timeseries() + .setTitle(this.buildTimeseriesTitle()) + .setData(buildCompareTimeSeriesQueryRunner({ filterKey })) + .setHeaderActions([new SceneTimerangeSelectionTypeSwitcher()]) + .setColor({ mode: 'fixed', fixedColor: color }) + .setCustomFieldConfig('fillOpacity', 9) + .setCustomFieldConfig('gradientMode', GraphGradientMode.Opacity) + .setBehaviors([ + (vizPanel: VizPanel) => { + const timeRange = findSceneObjectByClass(vizPanel, SceneTimeRange) as SceneTimeRange; + console.log('*** behaviors', timeRange.state); + }, + ]) + .build(); + + this.setState({ + timeseries, + }); + + timeseries.state.$data?.subscribeToState((newState, prevState) => { + console.log('*** newState', newState); + if (!newState.data) { + return; + } + + if (!newState.data?.annotations?.length && !prevState.data?.annotations?.length) { + const data = timeseries.state.$data?.state.data; + if (!data) { + return; + } + + // Make new annotations, for the first time + const annotation = new RangeAnnotation(); + const timeRange = (findSceneObjectByClass(this, SceneTimeRange) as SceneTimeRange).state.value; + + console.log('*** timeRange.from.unix()', timeRange.from.unix() * 1000); + + annotation.addRange({ + text: `${title} flame graph time range`, + color: annotationColor, + time: timeRange.from.unix() * 1000 + 30 * 1000, + timeEnd: timeRange.to.unix() * 1000 - 30 * 1000, + }); + + timeseries.state.$data?.setState({ + data: { + ...data, + annotations: [annotation], + }, + }); + } else if (!newState.data?.annotations?.length && prevState.data?.annotations?.length) { + // We can just ensure we retain the old annotations if they exist + newState.data.annotations = prevState.data.annotations; + } + }); + } + + buildTimeseriesTitle() { + const profileMetricId = getSceneVariableValue(this, 'profileMetricId'); + const { description } = getProfileMetric(profileMetricId as ProfileMetricId); + return description || getProfileMetricLabel(profileMetricId); + } + + public static Component = ({ model }: SceneComponentProps) => { + const styles = useStyles2(getStyles); + const { title, timeseries, timePicker, filterKey } = model.useState(); + + const filtersVariable = sceneGraph.findByKey(model, filterKey) as FiltersVariable; + + return ( +
+
+
{title}
+ +
+ +
+
+ +
+ {filtersVariable.state.label} + +
+ +
{timeseries && }
+
+ ); + }; +} + +const getStyles = (theme: GrafanaTheme2) => ({ + panel: css` + background-color: ${theme.colors.background.primary}; + padding: ${theme.spacing(1)} ${theme.spacing(1)} 0 ${theme.spacing(1)}; + border: 1px solid ${theme.colors.border.weak}; + border-radius: 2px; + `, + panelHeader: css` + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: ${theme.spacing(2)}; + + & > h6 { + margin-top: -2px; + } + `, + timePicker: css` + display: flex; + justify-content: flex-end; + `, + filter: css` + display: flex; + margin-bottom: ${theme.spacing(3)}; + `, + timeseries: css` + height: 200px; + + & [data-viz-panel-key] > div { + border: 0 none; + } + `, +}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneTimerangeSelectionTypeSwitcher.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneTimerangeSelectionTypeSwitcher.tsx new file mode 100644 index 00000000..636d1450 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneTimerangeSelectionTypeSwitcher.tsx @@ -0,0 +1,101 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { Icon, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui'; +import React from 'react'; + +import { EventSwitchTimerangeSelectionType } from '../domain/events/EventSwitchTimerangeSelectionType'; + +export enum TimerangeSelectionType { + TIMEPICKER = 'timepicker', + FLAMEGRAPH = 'flame-graph', +} + +export interface SceneTimerangeSelectionTypeSwitcherState extends SceneObjectState { + type: TimerangeSelectionType; +} + +export class SceneTimerangeSelectionTypeSwitcher extends SceneObjectBase { + static OPTIONS = [ + { label: 'Timepicker', value: TimerangeSelectionType.TIMEPICKER }, + { label: 'Flame graph', value: TimerangeSelectionType.FLAMEGRAPH }, + ]; + + constructor() { + super({ + type: TimerangeSelectionType.FLAMEGRAPH, + }); + } + + public onChange = (newType: TimerangeSelectionType) => { + this.setState({ type: newType }); + + const { type } = this.state; + + this.publishEvent(new EventSwitchTimerangeSelectionType({ type }), true); + }; + + public static Component = ({ model }: SceneComponentProps) => { + const styles = useStyles2(getStyles); + const { type } = model.useState(); + + return ( +
+
+ } + placement="top" + > + + + + +
+ ); + }; +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css` + display: flex; + flex-direction: column; + `, + tooltip: css` + padding: ${theme.spacing(1)}; + & dl { + margin-top: ${theme.spacing(2)}; + display: grid; + grid-gap: ${theme.spacing(1)} ${theme.spacing(2)}; + grid-template-columns: max-content; + } + & dt { + font-weight: bold; + } + & dd { + margin: 0; + grid-column-start: 2; + } + `, + label: css` + font-size: 12px; + text-align: right; + margin-bottom: 2px; + color: ${theme.colors.text.secondary}; + `, +}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/events/EventSwitchTimerangeSelectionType.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/events/EventSwitchTimerangeSelectionType.ts new file mode 100644 index 00000000..a9b15cb4 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/events/EventSwitchTimerangeSelectionType.ts @@ -0,0 +1,11 @@ +import { BusEventWithPayload } from '@grafana/data'; + +import { TimerangeSelectionType } from '../../components/SceneTimerangeSelectionTypeSwitcher'; + +export interface EventSwitchTimerangeSelectionTypePayload { + type: TimerangeSelectionType; +} + +export class EventSwitchTimerangeSelectionType extends BusEventWithPayload { + public static type = 'switch-timerange-selection-type'; +} diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index f235f81c..2389b5e0 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -43,6 +43,7 @@ import { SceneNoDataSwitcher } from '../SceneByVariableRepeaterGrid/components/S import { ScenePanelTypeSwitcher } from '../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; import { SceneQuickFilter } from '../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; import { GridItemData } from '../SceneByVariableRepeaterGrid/types/GridItemData'; +import { SceneExploreDiffFlameGraphs } from '../SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs'; import { SceneExploreServiceFlameGraph } from '../SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph'; import { ExplorationTypeSelector } from './ui/ExplorationTypeSelector'; @@ -57,6 +58,7 @@ export enum ExplorationType { PROFILE_TYPES = 'profiles', LABELS = 'labels', FLAME_GRAPH = 'flame-graph', + DIFF_FLAME_GRAPH = 'diff-flame-graph', FAVORITES = 'favorites', } @@ -82,6 +84,11 @@ export class SceneProfilesExplorer extends SceneObjectBase; }; @@ -360,8 +382,13 @@ export class SceneProfilesExplorer extends SceneObjectBase
- - + {timePickerControl && ( + + )} + {refreshPickerControl && ( + + )} + Exploration
- {options.map((option, i) => { + {options.slice(0, options.length - 2).map((option, i) => { const isActive = value === option.value; return ( <> @@ -35,7 +35,29 @@ export function ExplorationTypeSelector({ options, value, onChange }: Exploratio {option.label} - {i < options.length - 2 && } + {i < options.length - 3 && } + + ); + })} +
    
+ {/* eslint-disable-next-line sonarjs/cognitive-complexity */} + {options.slice(-2).map((option) => { + const isActive = value === option.value; + return ( + <> + +
  
); })} @@ -44,7 +66,7 @@ export function ExplorationTypeSelector({ options, value, onChange }: Exploratio ); } -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = () => ({ explorationTypeContainer: css` display: flex; align-items: center; @@ -54,10 +76,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ line-height: 32px; display: flex; align-items: center; - - & > button:last-child { - margin-left: ${theme.spacing(2)}; - } `, button: css` height: 30px; diff --git a/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx b/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx index cb442373..19fa5044 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx @@ -15,9 +15,10 @@ import { convertPyroscopeToVariableFilter, expressionBuilder } from './filters-o export class FiltersVariable extends AdHocFiltersVariable { static DEFAULT_VALUE = []; - constructor() { + constructor({ key }: { key: string }) { super({ - name: 'filters', + key, + name: key, label: 'Filters', filters: FiltersVariable.DEFAULT_VALUE, }); From 6eea7455e520091a50f2de303a2131f1a4eeb685 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 9 Aug 2024 15:39:28 +0200 Subject: [PATCH 18/76] feat(*): WiP --- .../SceneExploreDiffFlameGraphs.tsx | 14 +- .../SceneComparePanel.tsx | 162 ++++++++---------- .../domain/RangeAnnotation.ts | 33 ++++ .../SwitchTimeRangeSelectionTypeAction.tsx} | 10 +- .../EventSwitchTimerangeSelectionType.ts | 2 +- .../buildCompareTimeSeriesQueryRunner.ts | 20 +++ .../SceneLabelValuesTimeseries.tsx | 27 +-- 7 files changed, 154 insertions(+), 114 deletions(-) rename src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/{ => SceneComparePanel}/SceneComparePanel.tsx (56%) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/RangeAnnotation.ts rename src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/{SceneTimerangeSelectionTypeSwitcher.tsx => SceneComparePanel/domain/actions/SwitchTimeRangeSelectionTypeAction.tsx} (83%) rename src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/{ => components/SceneComparePanel}/domain/events/EventSwitchTimerangeSelectionType.ts (77%) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/infrastructure/buildCompareTimeSeriesQueryRunner.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx index 37285838..7a9a9726 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx @@ -16,7 +16,7 @@ import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVaria import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { CompareTarget } from '../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; -import { SceneComparePanel } from './components/SceneComparePanel'; +import { SceneComparePanel } from './components/SceneComparePanel/SceneComparePanel'; interface SceneExploreDiffFlameGraphsState extends SceneObjectState { baselinePanel: SceneComparePanel; @@ -44,7 +44,17 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase { + profileMetricVariable.setState({ query: ProfileMetricVariable.QUERY_DEFAULT }); + profileMetricVariable.update(true); + }; + } // see SceneProfilesExplorer getVariablesAndGridControls() { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx similarity index 56% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index 0a365dd0..78979f5e 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -1,31 +1,33 @@ import { css } from '@emotion/css'; -import { dateTime, FieldType, GrafanaTheme2, MutableDataFrame } from '@grafana/data'; +import { dateTime, FieldMatcherID, GrafanaTheme2 } from '@grafana/data'; import { - PanelBuilders, SceneComponentProps, + SceneDataTransformer, sceneGraph, SceneObjectBase, SceneObjectState, - SceneQueryRunner, + SceneRefreshPicker, SceneTimePicker, SceneTimeRange, SceneTimeRangeState, VariableDependencyConfig, - VizPanel, } from '@grafana/scenes'; -import { GraphGradientMode, InlineLabel, useStyles2 } from '@grafana/ui'; +import { InlineLabel, useStyles2 } from '@grafana/ui'; import { getProfileMetric, ProfileMetricId } from '@shared/infrastructure/profile-metrics/getProfileMetric'; import React from 'react'; -import { BASELINE_COLORS, COMPARISON_COLORS } from 'src/pages/ComparisonView/ui/colors'; -import { FiltersVariable } from 'src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable'; -import { findSceneObjectByClass } from 'src/pages/ProfilesExplorerView/helpers/findSceneObjectByClass'; -import { getSceneVariableValue } from 'src/pages/ProfilesExplorerView/helpers/getSceneVariableValue'; -import { PYROSCOPE_DATA_SOURCE } from 'src/pages/ProfilesExplorerView/infrastructure/pyroscope-data-sources'; -import { getProfileMetricLabel } from 'src/pages/ProfilesExplorerView/infrastructure/series/helpers/getProfileMetricLabel'; -import { CompareTarget } from '../../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; -import { EventSwitchTimerangeSelectionType } from '../domain/events/EventSwitchTimerangeSelectionType'; -import { SceneTimerangeSelectionTypeSwitcher } from './SceneTimerangeSelectionTypeSwitcher'; +import { BASELINE_COLORS, COMPARISON_COLORS } from '../../../../../../pages/ComparisonView/ui/colors'; +import { FiltersVariable } from '../../../..//domain/variables/FiltersVariable/FiltersVariable'; +import { findSceneObjectByClass } from '../../../../helpers/findSceneObjectByClass'; +import { getSceneVariableValue } from '../../../../helpers/getSceneVariableValue'; +import { getProfileMetricLabel } from '../../../../infrastructure/series/helpers/getProfileMetricLabel'; +import { addRefId, addStats } from '../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; +import { CompareTarget } from '../../../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; +import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; +import { SwitchTimeRangeSelectionTypeAction } from './domain/actions/SwitchTimeRangeSelectionTypeAction'; +import { EventSwitchTimerangeSelectionType } from './domain/events/EventSwitchTimerangeSelectionType'; +import { RangeAnnotation } from './domain/RangeAnnotation'; +import { buildCompareTimeSeriesQueryRunner } from './infrastructure/buildCompareTimeSeriesQueryRunner'; export interface SceneComparePanelState extends SceneObjectState { target: CompareTarget; @@ -34,21 +36,8 @@ export interface SceneComparePanelState extends SceneObjectState { color: string; annotationColor: string; timePicker: SceneTimePicker; - timeseries?: VizPanel; -} - -function buildCompareTimeSeriesQueryRunner({ filterKey }: { filterKey: 'filtersBaseline' | 'filtersComparison' }) { - return new SceneQueryRunner({ - datasource: PYROSCOPE_DATA_SOURCE, - queries: [ - { - refId: `$profileMetricId-$serviceName-${filterKey}}`, - queryType: 'metrics', - profileTypeId: '$profileMetricId', - labelSelector: `{service_name="$serviceName",$${filterKey}}`, - }, - ], - }); + refreshPicker: SceneRefreshPicker; + timeseries?: SceneLabelValuesTimeseries; } const getDefaultTimeRange = (): SceneTimeRangeState => { @@ -65,45 +54,11 @@ const getDefaultTimeRange = (): SceneTimeRangeState => { }; }; -class RangeAnnotation extends MutableDataFrame { - constructor() { - super(); - [ - { - name: 'time', - type: FieldType.time, - }, - { - name: 'timeEnd', - type: FieldType.time, - }, - { - name: 'isRegion', - type: FieldType.boolean, - }, - { - name: 'color', - type: FieldType.other, - }, - { - name: 'text', - type: FieldType.string, - }, - ].forEach((field) => this.addField(field)); - } - - addRange(entry: { time: number; timeEnd: number; color?: string; text: string }) { - this.add({ ...entry, isRegion: true }); - } -} - export class SceneComparePanel extends SceneObjectBase { protected _variableDependency = new VariableDependencyConfig(this, { variableNames: ['profileMetricId'], onVariableUpdateCompleted: () => { - this.state.timeseries?.setState({ - title: this.buildTimeseriesTitle(), - }); + this.state.timeseries?.updateTitle(this.buildTimeseriesTitle()); }, }); @@ -118,6 +73,7 @@ export class SceneComparePanel extends SceneObjectBase { target === CompareTarget.BASELINE ? BASELINE_COLORS.OVERLAY.toString() : COMPARISON_COLORS.OVERLAY.toString(), $timeRange: new SceneTimeRange(getDefaultTimeRange()), timePicker: new SceneTimePicker({ isOnCanvas: true }), + refreshPicker: new SceneRefreshPicker({ isOnCanvas: true }), timeseries: undefined, }); @@ -125,40 +81,27 @@ export class SceneComparePanel extends SceneObjectBase { } onActivate() { - const { filterKey, color, title, annotationColor } = this.state; + const { title, annotationColor } = this.state; this.subscribeToEvent(EventSwitchTimerangeSelectionType, (event) => { const { type } = event.payload; console.log('*** EventSwitchTimerangeSelectionType', event, type); }); - const timeseries = PanelBuilders.timeseries() - .setTitle(this.buildTimeseriesTitle()) - .setData(buildCompareTimeSeriesQueryRunner({ filterKey })) - .setHeaderActions([new SceneTimerangeSelectionTypeSwitcher()]) - .setColor({ mode: 'fixed', fixedColor: color }) - .setCustomFieldConfig('fillOpacity', 9) - .setCustomFieldConfig('gradientMode', GraphGradientMode.Opacity) - .setBehaviors([ - (vizPanel: VizPanel) => { - const timeRange = findSceneObjectByClass(vizPanel, SceneTimeRange) as SceneTimeRange; - console.log('*** behaviors', timeRange.state); - }, - ]) - .build(); + const timeseries = this.buildTimeSeries(); - this.setState({ - timeseries, - }); + this.setState({ timeseries }); + + const { $data } = timeseries.state.body.state; - timeseries.state.$data?.subscribeToState((newState, prevState) => { + $data?.subscribeToState((newState, prevState) => { console.log('*** newState', newState); if (!newState.data) { return; } if (!newState.data?.annotations?.length && !prevState.data?.annotations?.length) { - const data = timeseries.state.$data?.state.data; + const data = $data?.state.data; if (!data) { return; } @@ -167,20 +110,15 @@ export class SceneComparePanel extends SceneObjectBase { const annotation = new RangeAnnotation(); const timeRange = (findSceneObjectByClass(this, SceneTimeRange) as SceneTimeRange).state.value; - console.log('*** timeRange.from.unix()', timeRange.from.unix() * 1000); - annotation.addRange({ - text: `${title} flame graph time range`, + text: `${title} time range for the flame graph`, color: annotationColor, - time: timeRange.from.unix() * 1000 + 30 * 1000, - timeEnd: timeRange.to.unix() * 1000 - 30 * 1000, + time: timeRange.from.unix() * 1000 + Math.random() * 480 * 1000, + timeEnd: timeRange.to.unix() * 1000 - Math.random() * 360 * 1000, }); - timeseries.state.$data?.setState({ - data: { - ...data, - annotations: [annotation], - }, + $data?.setState({ + data: { ...data, annotations: [annotation] }, }); } else if (!newState.data?.annotations?.length && prevState.data?.annotations?.length) { // We can just ensure we retain the old annotations if they exist @@ -189,6 +127,38 @@ export class SceneComparePanel extends SceneObjectBase { }); } + buildTimeSeries() { + const { target, filterKey, title, color } = this.state; + + return new SceneLabelValuesTimeseries({ + item: { + index: 0, + value: target, + label: this.buildTimeseriesTitle(), + queryRunnerParams: {}, + }, + data: new SceneDataTransformer({ + $data: buildCompareTimeSeriesQueryRunner({ filterKey }), + transformations: [addRefId, addStats], + }), + overrides: (series) => + series.map((s) => ({ + matcher: { id: FieldMatcherID.byFrameRefID, options: s.refId }, + properties: [ + { + id: 'displayName', + value: `${title} getLabelFieldName(s.fields[1], '')`, + }, + { + id: 'color', + value: { mode: 'fixed', fixedColor: color }, + }, + ], + })), + headerActions: () => [new SwitchTimeRangeSelectionTypeAction()], + }); + } + buildTimeseriesTitle() { const profileMetricId = getSceneVariableValue(this, 'profileMetricId'); const { description } = getProfileMetric(profileMetricId as ProfileMetricId); @@ -197,7 +167,7 @@ export class SceneComparePanel extends SceneObjectBase { public static Component = ({ model }: SceneComponentProps) => { const styles = useStyles2(getStyles); - const { title, timeseries, timePicker, filterKey } = model.useState(); + const { title, timeseries, timePicker, refreshPicker, filterKey } = model.useState(); const filtersVariable = sceneGraph.findByKey(model, filterKey) as FiltersVariable; @@ -208,6 +178,7 @@ export class SceneComparePanel extends SceneObjectBase {
+
@@ -242,6 +213,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ timePicker: css` display: flex; justify-content: flex-end; + gap: ${theme.spacing(1)}; `, filter: css` display: flex; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/RangeAnnotation.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/RangeAnnotation.ts new file mode 100644 index 00000000..4cc71e3b --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/RangeAnnotation.ts @@ -0,0 +1,33 @@ +import { FieldType, MutableDataFrame } from '@grafana/data'; + +export class RangeAnnotation extends MutableDataFrame { + constructor() { + super(); + [ + { + name: 'time', + type: FieldType.time, + }, + { + name: 'timeEnd', + type: FieldType.time, + }, + { + name: 'isRegion', + type: FieldType.boolean, + }, + { + name: 'color', + type: FieldType.other, + }, + { + name: 'text', + type: FieldType.string, + }, + ].forEach((field) => this.addField(field)); + } + + addRange(entry: { time: number; timeEnd: number; color?: string; text: string }) { + this.add({ ...entry, isRegion: true }); + } +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneTimerangeSelectionTypeSwitcher.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionTypeAction.tsx similarity index 83% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneTimerangeSelectionTypeSwitcher.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionTypeAction.tsx index 636d1450..5a89cb3b 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneTimerangeSelectionTypeSwitcher.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionTypeAction.tsx @@ -4,18 +4,18 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana import { Icon, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui'; import React from 'react'; -import { EventSwitchTimerangeSelectionType } from '../domain/events/EventSwitchTimerangeSelectionType'; +import { EventSwitchTimerangeSelectionType } from '../events/EventSwitchTimerangeSelectionType'; export enum TimerangeSelectionType { TIMEPICKER = 'timepicker', FLAMEGRAPH = 'flame-graph', } -export interface SceneTimerangeSelectionTypeSwitcherState extends SceneObjectState { +export interface SwitchTimeRangeSelectionTypeActionState extends SceneObjectState { type: TimerangeSelectionType; } -export class SceneTimerangeSelectionTypeSwitcher extends SceneObjectBase { +export class SwitchTimeRangeSelectionTypeAction extends SceneObjectBase { static OPTIONS = [ { label: 'Timepicker', value: TimerangeSelectionType.TIMEPICKER }, { label: 'Flame graph', value: TimerangeSelectionType.FLAMEGRAPH }, @@ -35,7 +35,7 @@ export class SceneTimerangeSelectionTypeSwitcher extends SceneObjectBase) => { + public static Component = ({ model }: SceneComponentProps) => { const styles = useStyles2(getStyles); const { type } = model.useState(); @@ -62,7 +62,7 @@ export class SceneTimerangeSelectionTypeSwitcher extends SceneObjectBase diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/events/EventSwitchTimerangeSelectionType.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionType.ts similarity index 77% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/events/EventSwitchTimerangeSelectionType.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionType.ts index a9b15cb4..50a20226 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/events/EventSwitchTimerangeSelectionType.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionType.ts @@ -1,6 +1,6 @@ import { BusEventWithPayload } from '@grafana/data'; -import { TimerangeSelectionType } from '../../components/SceneTimerangeSelectionTypeSwitcher'; +import { TimerangeSelectionType } from '../actions/SwitchTimeRangeSelectionTypeAction'; export interface EventSwitchTimerangeSelectionTypePayload { type: TimerangeSelectionType; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/infrastructure/buildCompareTimeSeriesQueryRunner.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/infrastructure/buildCompareTimeSeriesQueryRunner.ts new file mode 100644 index 00000000..049bae8b --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/infrastructure/buildCompareTimeSeriesQueryRunner.ts @@ -0,0 +1,20 @@ +import { SceneQueryRunner } from '@grafana/scenes'; +import { PYROSCOPE_DATA_SOURCE } from 'src/pages/ProfilesExplorerView/infrastructure/pyroscope-data-sources'; + +export function buildCompareTimeSeriesQueryRunner({ + filterKey, +}: { + filterKey: 'filtersBaseline' | 'filtersComparison'; +}) { + return new SceneQueryRunner({ + datasource: PYROSCOPE_DATA_SOURCE, + queries: [ + { + refId: `$profileMetricId-$serviceName-${filterKey}}`, + queryType: 'metrics', + profileTypeId: '$profileMetricId', + labelSelector: `{service_name="$serviceName",$${filterKey}}`, + }, + ], + }); +} diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx index 64b8614c..3263f1e1 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx @@ -2,6 +2,7 @@ import { DataFrame, FieldMatcherID, getValueFormat, LoadingState } from '@grafan import { PanelBuilders, SceneComponentProps, + SceneDataProvider, SceneDataTransformer, SceneObjectBase, SceneObjectState, @@ -30,6 +31,7 @@ interface SceneLabelValuesTimeseriesState extends SceneObjectState { headerActions: (item: GridItemData) => VizPanelState['headerActions']; displayAllValues: boolean; body: VizPanel; + overrides: (series: DataFrame[]) => VizPanelState['fieldConfig']['overrides']; } export class SceneLabelValuesTimeseries extends SceneObjectBase { @@ -37,25 +39,31 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase []), body: PanelBuilders.timeseries() .setTitle(item.label) .setData( - new SceneDataTransformer({ - $data: buildTimeSeriesQueryRunner(item.queryRunnerParams), - transformations: displayAllValues - ? [addRefId, addStats, sortSeries] - : [addRefId, addStats, sortSeries, limitNumberOfSeries], - }) + data || + new SceneDataTransformer({ + $data: buildTimeSeriesQueryRunner(item.queryRunnerParams), + transformations: displayAllValues + ? [addRefId, addStats, sortSeries] + : [addRefId, addStats, sortSeries, limitNumberOfSeries], + }) ) .setHeaderActions(headerActions(item)) .build(), @@ -140,7 +148,7 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { + const defaultOverrides = series.map((s, i) => { const metricField = s.fields[1]; let displayName = getLabelFieldName(metricField, groupByLabel); @@ -165,11 +173,8 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase getLabelFieldName(s.fields[1], groupByLabel)); + return [...defaultOverrides, ...this.state.overrides(series)]; } updateTitle(newTitle: string) { From dab2dec5853ab1968cf78318c29e076ad0864dc1 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 9 Aug 2024 15:40:03 +0200 Subject: [PATCH 19/76] chore(*): Fix imports --- .../SceneByVariableRepeaterGrid/domain/sortFavGridItems.tsx | 5 ++--- .../components/SceneStatsPanel/ui/StatsPanel.tsx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.tsx index 86d677a6..70f7c6e2 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.tsx @@ -1,6 +1,5 @@ -import { FavAction } from 'src/pages/ProfilesExplorerView/domain/actions/FavAction'; -import { FavoritesDataSource } from 'src/pages/ProfilesExplorerView/infrastructure/favorites/FavoritesDataSource'; - +import { FavAction } from '../../..//domain/actions/FavAction'; +import { FavoritesDataSource } from '../../../infrastructure/favorites/FavoritesDataSource'; import { GridItemData } from '../types/GridItemData'; export const sortFavGridItems: (a: GridItemData, b: GridItemData) => number = function (a, b) { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx index 24bd27c7..bf30a6f3 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx @@ -2,8 +2,8 @@ import { css } from '@emotion/css'; import { getValueFormat, GrafanaTheme2 } from '@grafana/data'; import { RadioButtonGroup, Spinner, useStyles2 } from '@grafana/ui'; import React, { useMemo } from 'react'; -import { GridItemData } from 'src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/types/GridItemData'; +import { GridItemData } from '../../../../../../../../../components/SceneByVariableRepeaterGrid/types/GridItemData'; import { getColorByIndex } from '../../../../../../../../../helpers/getColorByIndex'; import { CompareTarget } from '../../../domain/types'; import { ItemStats } from '../SceneStatsPanel'; From 90d2b8de62c36f7c79a25970f74251342c47abf1 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 9 Aug 2024 15:46:37 +0200 Subject: [PATCH 20/76] refactor(*): Better naming --- .../SceneComparePanel/SceneComparePanel.tsx | 18 +++++++---- ...=> SwitchTimeRangeSelectionModeAction.tsx} | 32 +++++++++---------- .../EventSwitchTimerangeSelectionMode.ts | 11 +++++++ .../EventSwitchTimerangeSelectionType.ts | 11 ------- 4 files changed, 38 insertions(+), 34 deletions(-) rename src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/{SwitchTimeRangeSelectionTypeAction.tsx => SwitchTimeRangeSelectionModeAction.tsx} (71%) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts delete mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionType.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index 78979f5e..aa1393eb 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -24,8 +24,11 @@ import { getProfileMetricLabel } from '../../../../infrastructure/series/helpers import { addRefId, addStats } from '../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; import { CompareTarget } from '../../../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; -import { SwitchTimeRangeSelectionTypeAction } from './domain/actions/SwitchTimeRangeSelectionTypeAction'; -import { EventSwitchTimerangeSelectionType } from './domain/events/EventSwitchTimerangeSelectionType'; +import { + SwitchTimeRangeSelectionModeAction, + TimerangeSelectionMode, +} from './domain/actions/SwitchTimeRangeSelectionModeAction'; +import { EventSwitchTimerangeSelectionMode } from './domain/events/EventSwitchTimerangeSelectionMode'; import { RangeAnnotation } from './domain/RangeAnnotation'; import { buildCompareTimeSeriesQueryRunner } from './infrastructure/buildCompareTimeSeriesQueryRunner'; @@ -83,9 +86,8 @@ export class SceneComparePanel extends SceneObjectBase { onActivate() { const { title, annotationColor } = this.state; - this.subscribeToEvent(EventSwitchTimerangeSelectionType, (event) => { - const { type } = event.payload; - console.log('*** EventSwitchTimerangeSelectionType', event, type); + this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { + this.switchSelectionMode(event.payload); }); const timeseries = this.buildTimeSeries(); @@ -155,7 +157,7 @@ export class SceneComparePanel extends SceneObjectBase { }, ], })), - headerActions: () => [new SwitchTimeRangeSelectionTypeAction()], + headerActions: () => [new SwitchTimeRangeSelectionModeAction()], }); } @@ -165,6 +167,10 @@ export class SceneComparePanel extends SceneObjectBase { return description || getProfileMetricLabel(profileMetricId); } + switchSelectionMode({ mode }: { mode: TimerangeSelectionMode }) { + console.log('*** switchSelectionMode', mode); + } + public static Component = ({ model }: SceneComponentProps) => { const styles = useStyles2(getStyles); const { title, timeseries, timePicker, refreshPicker, filterKey } = model.useState(); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionTypeAction.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx similarity index 71% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionTypeAction.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx index 5a89cb3b..b2a4a6ed 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionTypeAction.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx @@ -4,45 +4,43 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana import { Icon, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui'; import React from 'react'; -import { EventSwitchTimerangeSelectionType } from '../events/EventSwitchTimerangeSelectionType'; +import { EventSwitchTimerangeSelectionMode } from '../events/EventSwitchTimerangeSelectionMode'; -export enum TimerangeSelectionType { +export enum TimerangeSelectionMode { TIMEPICKER = 'timepicker', FLAMEGRAPH = 'flame-graph', } export interface SwitchTimeRangeSelectionTypeActionState extends SceneObjectState { - type: TimerangeSelectionType; + mode: TimerangeSelectionMode; } -export class SwitchTimeRangeSelectionTypeAction extends SceneObjectBase { +export class SwitchTimeRangeSelectionModeAction extends SceneObjectBase { static OPTIONS = [ - { label: 'Timepicker', value: TimerangeSelectionType.TIMEPICKER }, - { label: 'Flame graph', value: TimerangeSelectionType.FLAMEGRAPH }, + { label: 'Timepicker', value: TimerangeSelectionMode.TIMEPICKER }, + { label: 'Flame graph', value: TimerangeSelectionMode.FLAMEGRAPH }, ]; constructor() { super({ - type: TimerangeSelectionType.FLAMEGRAPH, + mode: TimerangeSelectionMode.FLAMEGRAPH, }); } - public onChange = (newType: TimerangeSelectionType) => { - this.setState({ type: newType }); + public onChange = (newMode: TimerangeSelectionMode) => { + this.setState({ mode: newMode }); - const { type } = this.state; - - this.publishEvent(new EventSwitchTimerangeSelectionType({ type }), true); + this.publishEvent(new EventSwitchTimerangeSelectionMode({ mode: newMode }), true); }; - public static Component = ({ model }: SceneComponentProps) => { + public static Component = ({ model }: SceneComponentProps) => { const styles = useStyles2(getStyles); - const { type } = model.useState(); + const { mode } = model.useState(); return (
diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts new file mode 100644 index 00000000..8c391091 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts @@ -0,0 +1,11 @@ +import { BusEventWithPayload } from '@grafana/data'; + +import { TimerangeSelectionMode } from '../actions/SwitchTimeRangeSelectionModeAction'; + +export interface EventSwitchTimerangeSelectionTypePayload { + mode: TimerangeSelectionMode; +} + +export class EventSwitchTimerangeSelectionMode extends BusEventWithPayload { + public static type = 'switch-timerange-selection-mode'; +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionType.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionType.ts deleted file mode 100644 index 50a20226..00000000 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionType.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BusEventWithPayload } from '@grafana/data'; - -import { TimerangeSelectionType } from '../actions/SwitchTimeRangeSelectionTypeAction'; - -export interface EventSwitchTimerangeSelectionTypePayload { - type: TimerangeSelectionType; -} - -export class EventSwitchTimerangeSelectionType extends BusEventWithPayload { - public static type = 'switch-timerange-selection-type'; -} From 25ff03d9bccd92c1b00acf1cbc373834541b793c Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 9 Aug 2024 15:55:59 +0200 Subject: [PATCH 21/76] fix(SceneComparePanel): Fix legend bug --- .../SceneComparePanel/SceneComparePanel.tsx | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index aa1393eb..9aa30ec5 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { dateTime, FieldMatcherID, GrafanaTheme2 } from '@grafana/data'; +import { dateTime, FieldMatcherID, getValueFormat, GrafanaTheme2 } from '@grafana/data'; import { SceneComponentProps, SceneDataTransformer, @@ -22,6 +22,7 @@ import { findSceneObjectByClass } from '../../../../helpers/findSceneObjectByCla import { getSceneVariableValue } from '../../../../helpers/getSceneVariableValue'; import { getProfileMetricLabel } from '../../../../infrastructure/series/helpers/getProfileMetricLabel'; import { addRefId, addStats } from '../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; +import { getSeriesStatsValue } from '../../../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/getSeriesStatsValue'; import { CompareTarget } from '../../../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; import { @@ -144,19 +145,26 @@ export class SceneComparePanel extends SceneObjectBase { transformations: [addRefId, addStats], }), overrides: (series) => - series.map((s) => ({ - matcher: { id: FieldMatcherID.byFrameRefID, options: s.refId }, - properties: [ - { - id: 'displayName', - value: `${title} getLabelFieldName(s.fields[1], '')`, - }, - { - id: 'color', - value: { mode: 'fixed', fixedColor: color }, - }, - ], - })), + series.map((s) => { + const metricField = s.fields[1]; + const allValuesSum = getSeriesStatsValue(s, 'allValuesSum') || 0; + const formattedValue = getValueFormat(metricField.config.unit)(allValuesSum); + const displayName = `${title} total = ${formattedValue.text}${formattedValue.suffix}`; + + return { + matcher: { id: FieldMatcherID.byFrameRefID, options: s.refId }, + properties: [ + { + id: 'displayName', + value: displayName, + }, + { + id: 'color', + value: { mode: 'fixed', fixedColor: color }, + }, + ], + }; + }), headerActions: () => [new SwitchTimeRangeSelectionModeAction()], }); } From 0c655e7d0bed8723c97135ca0184d7fbfca1f591 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 9 Aug 2024 19:28:52 +0200 Subject: [PATCH 22/76] fix: Small fixes --- .../actions/SwitchTimeRangeSelectionModeAction.tsx | 9 ++++++--- .../SceneLabelValuesTimeseries.tsx | 12 +++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx index b2a4a6ed..1e1d905d 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx @@ -17,7 +17,7 @@ export interface SwitchTimeRangeSelectionTypeActionState extends SceneObjectStat export class SwitchTimeRangeSelectionModeAction extends SceneObjectBase { static OPTIONS = [ - { label: 'Timepicker', value: TimerangeSelectionMode.TIMEPICKER }, + { label: 'Time picker', value: TimerangeSelectionMode.TIMEPICKER }, { label: 'Flame graph', value: TimerangeSelectionMode.FLAMEGRAPH }, ]; @@ -46,10 +46,13 @@ export class SwitchTimeRangeSelectionModeAction extends SceneObjectBase
Change the behaviour when selecting a time range on the panel:
-
Timepicker
+
Time picker
Time range zoom in (default behaviour)
Flame graph
-
Time range for building the flame graph
+
+ Time range for building the flame graph (the stack traces will be retrieved only for the selected + area) +
} diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx index 3263f1e1..b261c324 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries/SceneLabelValuesTimeseries.tsx @@ -31,7 +31,7 @@ interface SceneLabelValuesTimeseriesState extends SceneObjectState { headerActions: (item: GridItemData) => VizPanelState['headerActions']; displayAllValues: boolean; body: VizPanel; - overrides: (series: DataFrame[]) => VizPanelState['fieldConfig']['overrides']; + overrides?: (series: DataFrame[]) => VizPanelState['fieldConfig']['overrides']; } export class SceneLabelValuesTimeseries extends SceneObjectBase { @@ -53,7 +53,7 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase []), + overrides, body: PanelBuilders.timeseries() .setTitle(item.label) .setData( @@ -145,10 +145,14 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { + return series.map((s, i) => { const metricField = s.fields[1]; let displayName = getLabelFieldName(metricField, groupByLabel); @@ -173,8 +177,6 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase Date: Fri, 9 Aug 2024 20:01:31 +0200 Subject: [PATCH 23/76] feat: Make selection work --- .../SceneComparePanel/SceneComparePanel.tsx | 62 ++++++++++++------- .../SceneTimeRangeWithAnnotations.ts | 61 ++++++++++++++++++ 2 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index 9aa30ec5..8e1cd4bf 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { dateTime, FieldMatcherID, getValueFormat, GrafanaTheme2 } from '@grafana/data'; +import { dateTime, FieldMatcherID, getValueFormat, GrafanaTheme2, PanelData, TimeRange } from '@grafana/data'; import { SceneComponentProps, SceneDataTransformer, @@ -18,7 +18,6 @@ import React from 'react'; import { BASELINE_COLORS, COMPARISON_COLORS } from '../../../../../../pages/ComparisonView/ui/colors'; import { FiltersVariable } from '../../../..//domain/variables/FiltersVariable/FiltersVariable'; -import { findSceneObjectByClass } from '../../../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../../../helpers/getSceneVariableValue'; import { getProfileMetricLabel } from '../../../../infrastructure/series/helpers/getProfileMetricLabel'; import { addRefId, addStats } from '../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; @@ -32,6 +31,7 @@ import { import { EventSwitchTimerangeSelectionMode } from './domain/events/EventSwitchTimerangeSelectionMode'; import { RangeAnnotation } from './domain/RangeAnnotation'; import { buildCompareTimeSeriesQueryRunner } from './infrastructure/buildCompareTimeSeriesQueryRunner'; +import { SceneTimeRangeWithAnnotations } from './SceneTimeRangeWithAnnotations'; export interface SceneComparePanelState extends SceneObjectState { target: CompareTarget; @@ -42,6 +42,7 @@ export interface SceneComparePanelState extends SceneObjectState { timePicker: SceneTimePicker; refreshPicker: SceneRefreshPicker; timeseries?: SceneLabelValuesTimeseries; + $timeRange: SceneTimeRange; } const getDefaultTimeRange = (): SceneTimeRangeState => { @@ -93,38 +94,51 @@ export class SceneComparePanel extends SceneObjectBase { const timeseries = this.buildTimeSeries(); + function updateAnnotation(timeRange: TimeRange, data?: PanelData) { + if (!data) { + return; + } + + const annotation = new RangeAnnotation(); + + annotation.addRange({ + text: `${title} time range`, + color: annotationColor, + time: timeRange.from.unix() * 1000, + timeEnd: timeRange.to.unix() * 1000, + }); + + $data?.setState({ + data: { + ...data, + annotations: [annotation], + }, + }); + } + + timeseries.setState({ + $timeRange: new SceneTimeRangeWithAnnotations({ + onTimeRangeChange(timeRange) { + updateAnnotation(timeRange, timeseries.state.body.state.$data?.state.data); + }, + }), + }); + this.setState({ timeseries }); const { $data } = timeseries.state.body.state; $data?.subscribeToState((newState, prevState) => { - console.log('*** newState', newState); if (!newState.data) { return; } if (!newState.data?.annotations?.length && !prevState.data?.annotations?.length) { - const data = $data?.state.data; - if (!data) { - return; - } - - // Make new annotations, for the first time - const annotation = new RangeAnnotation(); - const timeRange = (findSceneObjectByClass(this, SceneTimeRange) as SceneTimeRange).state.value; - - annotation.addRange({ - text: `${title} time range for the flame graph`, - color: annotationColor, - time: timeRange.from.unix() * 1000 + Math.random() * 480 * 1000, - timeEnd: timeRange.to.unix() * 1000 - Math.random() * 360 * 1000, - }); - - $data?.setState({ - data: { ...data, annotations: [annotation] }, - }); - } else if (!newState.data?.annotations?.length && prevState.data?.annotations?.length) { - // We can just ensure we retain the old annotations if they exist + // updateAnnotation(this.state.$timeRange.state.value, newState.data); + return; + } + + if (!newState.data?.annotations?.length && prevState.data?.annotations?.length) { newState.data.annotations = prevState.data.annotations; } }); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts new file mode 100644 index 00000000..bd882b29 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts @@ -0,0 +1,61 @@ +import { getDefaultTimeRange, TimeRange } from '@grafana/data'; +import { sceneGraph, SceneObjectBase, SceneTimeRangeLike, SceneTimeRangeState } from '@grafana/scenes'; + +interface SceneTimeRangeWithAnnotationsState extends SceneTimeRangeState { + alternateTimeRange: TimeRange; + onTimeRangeChange?: (timeRange: TimeRange) => void; +} + +export class SceneTimeRangeWithAnnotations + extends SceneObjectBase + implements SceneTimeRangeLike +{ + constructor( + state: Omit = {} + ) { + super({ + ...state, + // We set a default time range here. It will be overwritten on activation based on ancestor time range. + from: 'now-6h', + to: 'now', + value: getDefaultTimeRange(), + alternateTimeRange: getDefaultTimeRange(), + }); + + this.addActivationHandler(() => { + const timeRange = this.realTimeRange; + + this.setState({ + ...timeRange.state, + alternateTimeRange: timeRange.state.value, + }); + + this._subs.add(timeRange.subscribeToState((newState) => this.setState(newState))); + }); + } + + private get realTimeRange() { + const parentsceneObject = this.parent; + if (!parentsceneObject?.parent) { + throw Error('A time range change override will not function if it is on a scene with no parent.'); + } + return sceneGraph.getTimeRange(parentsceneObject?.parent); + } + + onTimeRangeChange(timeRange: TimeRange): void { + this.setState({ alternateTimeRange: timeRange }); + this.state.onTimeRangeChange?.(timeRange); + } + + onTimeZoneChange(timeZone: string): void { + this.realTimeRange.onTimeZoneChange(timeZone); + } + + getTimeZone(): string { + return this.realTimeRange.getTimeZone(); + } + + onRefresh(): void { + this.realTimeRange.onRefresh(); + } +} From 4ce83a3409611528f0fac552bf5abb088a023184 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 9 Aug 2024 20:09:42 +0200 Subject: [PATCH 24/76] feat(*): Make toggle range selection mode work --- .../SceneComparePanel/SceneComparePanel.tsx | 9 ++++++-- .../SceneTimeRangeWithAnnotations.ts | 21 ++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index 8e1cd4bf..2ecbb3e1 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -31,7 +31,7 @@ import { import { EventSwitchTimerangeSelectionMode } from './domain/events/EventSwitchTimerangeSelectionMode'; import { RangeAnnotation } from './domain/RangeAnnotation'; import { buildCompareTimeSeriesQueryRunner } from './infrastructure/buildCompareTimeSeriesQueryRunner'; -import { SceneTimeRangeWithAnnotations } from './SceneTimeRangeWithAnnotations'; +import { SceneTimeRangeWithAnnotations, TimeRangeWithAnnotationsMode } from './SceneTimeRangeWithAnnotations'; export interface SceneComparePanelState extends SceneObjectState { target: CompareTarget; @@ -190,7 +190,12 @@ export class SceneComparePanel extends SceneObjectBase { } switchSelectionMode({ mode }: { mode: TimerangeSelectionMode }) { - console.log('*** switchSelectionMode', mode); + const newMode = + mode === TimerangeSelectionMode.FLAMEGRAPH + ? TimeRangeWithAnnotationsMode.ANNOTATIONS + : TimeRangeWithAnnotationsMode.DEFAULT; + + (this.state.timeseries?.state.$timeRange as SceneTimeRangeWithAnnotations).changeMode(newMode); } public static Component = ({ model }: SceneComponentProps) => { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts index bd882b29..258e9edb 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts @@ -1,9 +1,15 @@ import { getDefaultTimeRange, TimeRange } from '@grafana/data'; import { sceneGraph, SceneObjectBase, SceneTimeRangeLike, SceneTimeRangeState } from '@grafana/scenes'; +export enum TimeRangeWithAnnotationsMode { + ANNOTATIONS = 'annotations', + DEFAULT = 'default', +} + interface SceneTimeRangeWithAnnotationsState extends SceneTimeRangeState { alternateTimeRange: TimeRange; onTimeRangeChange?: (timeRange: TimeRange) => void; + mode: TimeRangeWithAnnotationsMode; } export class SceneTimeRangeWithAnnotations @@ -11,7 +17,10 @@ export class SceneTimeRangeWithAnnotations implements SceneTimeRangeLike { constructor( - state: Omit = {} + state: Omit< + SceneTimeRangeWithAnnotationsState, + 'mode' | 'from' | 'to' | 'value' | 'timeZone' | 'alternateTimeRange' + > = {} ) { super({ ...state, @@ -20,6 +29,7 @@ export class SceneTimeRangeWithAnnotations to: 'now', value: getDefaultTimeRange(), alternateTimeRange: getDefaultTimeRange(), + mode: TimeRangeWithAnnotationsMode.ANNOTATIONS, }); this.addActivationHandler(() => { @@ -34,6 +44,10 @@ export class SceneTimeRangeWithAnnotations }); } + changeMode(newMode: TimeRangeWithAnnotationsMode) { + this.setState({ mode: newMode }); + } + private get realTimeRange() { const parentsceneObject = this.parent; if (!parentsceneObject?.parent) { @@ -43,6 +57,11 @@ export class SceneTimeRangeWithAnnotations } onTimeRangeChange(timeRange: TimeRange): void { + if (this.state.mode === TimeRangeWithAnnotationsMode.DEFAULT) { + this.realTimeRange.onTimeRangeChange(timeRange); + return; + } + this.setState({ alternateTimeRange: timeRange }); this.state.onTimeRangeChange?.(timeRange); } From d7d7b621148cd560ef911a484af0bda02000166b Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Mon, 12 Aug 2024 11:38:23 +0200 Subject: [PATCH 25/76] chore(StatsPanel): Small code improvement --- .../components/SceneStatsPanel/ui/StatsPanel.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx index bf30a6f3..3ce74ff2 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx @@ -12,7 +12,7 @@ export type StatsPanelProps = { item: GridItemData; itemStats?: ItemStats; compareTargetValue?: CompareTarget; - onChangeCompareTarget: (compareTarget: CompareTarget, item: GridItemData) => void; + onChangeCompareTarget: (compareTarget: CompareTarget) => void; }; export function StatsPanel({ item, itemStats, compareTargetValue, onChangeCompareTarget }: StatsPanelProps) { @@ -62,9 +62,7 @@ export function StatsPanel({ item, itemStats, compareTargetValue, onChangeCompar className={styles.radioButtonsGroup} size="sm" options={options} - onChange={(newValue) => { - onChangeCompareTarget(newValue as CompareTarget, item); - }} + onChange={onChangeCompareTarget} value={compareTargetValue} />
From 83c315f6806d734a8a758ae9e969d9e25f539b23 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Mon, 12 Aug 2024 12:09:13 +0200 Subject: [PATCH 26/76] refactor(SceneStatsPanel): Better cohesion --- .../SceneGroupByLabels/SceneGroupByLabels.tsx | 14 +------ .../SceneLabelValuesGrid.tsx | 21 +--------- .../components/SceneLabelValuePanel.tsx | 5 +-- .../SceneStatsPanel/SceneStatsPanel.tsx | 40 +++++++++++++++---- 4 files changed, 36 insertions(+), 44 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index 357c47ed..d3bdfc36 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -320,19 +320,7 @@ export class SceneGroupByLabels extends SceneObjectBase // TODO: optimize if needed // we can remove the loop if we clear the current selection in the UI before updating the compare map (see selectForCompare() and onClickClearCompareButton()) for (const panel of statsPanels) { - const { item } = panel.state; - - if (baselineItem?.value === item.value) { - panel.updateCompareTargetValue(CompareTarget.BASELINE); - continue; - } - - if (comparisonItem?.value === item.value) { - panel.updateCompareTargetValue(CompareTarget.COMPARISON); - continue; - } - - panel.updateCompareTargetValue(undefined); + panel.setCompareTargetValue(baselineItem, comparisonItem); } } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index 8f247d88..13e43504 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -44,9 +44,7 @@ import { } from '../../../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; import { GridItemData } from '../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; import { EventDataReceived } from '../../../../../SceneLabelValuesTimeseries/domain/events/EventDataReceived'; -import { SceneGroupByLabels } from '../../SceneGroupByLabels'; import { SceneLabelValuePanel } from './components/SceneLabelValuePanel'; -import { CompareTarget } from './domain/types'; export interface SceneLabelValuesGridState extends EmbeddedSceneState { $data: SceneDataProvider; @@ -322,12 +320,10 @@ export class SceneLabelValuesGrid extends SceneObjectBase { return new SceneCSSGridItem({ key: SceneLabelValuesGrid.buildGridItemKey(item), - body: this.buildVizPanel(item, compare), + body: this.buildVizPanel(item), }); }); @@ -337,11 +333,10 @@ export class SceneLabelValuesGrid extends SceneObjectBase) { + buildVizPanel(item: GridItemData) { const vizPanel = new SceneLabelValuePanel({ item, headerActions: this.state.headerActions.bind(null, item, this.state.items), - compareTargetValue: this.getItemCompareTargetValue(item, compare), }); const sub = vizPanel.subscribeToEvent(EventDataReceived, (event) => { @@ -371,18 +366,6 @@ export class SceneLabelValuesGrid extends SceneObjectBase) { - if (compare.get(CompareTarget.BASELINE)?.value === item.value) { - return CompareTarget.BASELINE; - } - - if (compare.get(CompareTarget.COMPARISON)?.value === item.value) { - return CompareTarget.COMPARISON; - } - - return undefined; - } - filterItems(items: SceneLabelValuesGridState['items']) { const quickFilterScene = findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter; const { searchText } = quickFilterScene.state; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx index d97139b7..a2fef65b 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx @@ -8,7 +8,6 @@ import { GridItemData } from '../../../../../../SceneByVariableRepeaterGrid/type import { EventDataReceived } from '../../../../../../SceneLabelValuesTimeseries/domain/events/EventDataReceived'; import { SceneLabelValuesTimeseries } from '../../../../../../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; import { getSeriesStatsValue } from '../domain/getSeriesStatsValue'; -import { CompareTarget } from '../domain/types'; import { GRID_AUTO_ROWS } from '../SceneLabelValuesGrid'; import { SceneStatsPanel } from './SceneStatsPanel/SceneStatsPanel'; @@ -25,15 +24,13 @@ export class SceneLabelValuePanel extends SceneObjectBase VizPanelState['headerActions']; - compareTargetValue?: CompareTarget; }) { super({ key: 'label-value-panel', - statsPanel: new SceneStatsPanel({ item, compareTargetValue }), + statsPanel: new SceneStatsPanel({ item }), timeseriesPanel: new SceneLabelValuesTimeseries({ item, headerActions }), }); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx index 46c54516..9671db8f 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx @@ -1,8 +1,10 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import React from 'react'; +import { findSceneObjectByClass } from 'src/pages/ProfilesExplorerView/helpers/findSceneObjectByClass'; import { GridItemData } from '../../../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; import { EventSelectForCompare } from '../../../../domain/events/EventSelectForCompare'; +import { SceneGroupByLabels } from '../../../../SceneGroupByLabels'; import { CompareTarget } from '../../domain/types'; import { StatsPanel } from './ui/StatsPanel'; @@ -20,18 +22,45 @@ interface SceneStatsPanelState extends SceneObjectState { export class SceneStatsPanel extends SceneObjectBase { static WIDTH_IN_PIXELS = 180; - constructor({ item, compareTargetValue }: { item: GridItemData; compareTargetValue?: CompareTarget }) { + constructor({ item }: { item: GridItemData }) { super({ item, itemStats: undefined, - compareTargetValue, + compareTargetValue: undefined, }); + + this.addActivationHandler(this.onActivate.bind(this)); + } + + onActivate() { + const compare = (findSceneObjectByClass(this, SceneGroupByLabels) as SceneGroupByLabels).getCompare(); + + this.setCompareTargetValue(compare.get(CompareTarget.BASELINE), compare.get(CompareTarget.COMPARISON)); } - updateCompareTargetValue(compareTargetValue?: CompareTarget) { + setCompareTargetValue(baselineItem?: GridItemData, comparisonItem?: GridItemData) { + const { item } = this.state; + let compareTargetValue; + + if (baselineItem?.value === item.value) { + compareTargetValue = CompareTarget.BASELINE; + } else if (comparisonItem?.value === item.value) { + compareTargetValue = CompareTarget.COMPARISON; + } + this.setState({ compareTargetValue }); } + onChangeCompareTarget = (compareTarget: CompareTarget) => { + this.publishEvent( + new EventSelectForCompare({ + compareTarget, + item: this.state.item, + }), + true + ); + }; + getStats() { return this.state.itemStats; } @@ -40,11 +69,6 @@ export class SceneStatsPanel extends SceneObjectBase { this.setState({ itemStats }); } - onChangeCompareTarget = (compareTarget: CompareTarget) => { - const { item } = this.state; - this.publishEvent(new EventSelectForCompare({ compareTarget, item }), true); - }; - static Component({ model }: SceneComponentProps) { const { item, itemStats, compareTargetValue } = model.useState(); From 0c25e6d8cecb13481b75e308dba65591d227e27b Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Mon, 12 Aug 2024 12:49:50 +0200 Subject: [PATCH 27/76] refactor(*): Prepare comparison flow improvement --- .../SceneByVariableRepeaterGrid.tsx | 52 ++++---------- .../components/SceneLayoutSwitcher.tsx | 2 +- .../components/ScenePanelTypeSwitcher.tsx | 2 +- .../components/SceneQuickFilter.tsx | 2 +- .../domain/sortFavGridItems.ts | 22 ++++++ .../infrastructure/data-transformations.ts | 5 +- .../SceneExploreFavorites.tsx | 4 +- .../SceneExploreServiceFlameGraph.tsx | 1 + .../SceneExploreServiceLabels.tsx | 1 + .../components/SceneLabelValuesGrid.tsx | 24 +------ .../components/SceneLabelValuesBarGauge.tsx | 24 ++++--- .../components/SceneLabelValuesTimeseries.tsx | 67 ++++++++++++------- .../components/SceneMainServiceTimeseries.tsx | 21 +++++- .../SceneProfilesExplorer.tsx | 4 +- .../domain/events/EventDataReceived.ts | 9 +++ .../GroupByVariable/GroupBySelector.tsx | 8 ++- .../GroupByVariable/GroupByVariable.tsx | 1 + .../helpers/getSeriesLabelFieldName.ts | 4 ++ .../helpers/getSeriesStatsValue.ts | 4 ++ .../infrastructure/labels/LabelsDataSource.ts | 2 + .../infrastructure/labels/labelsRepository.ts | 3 +- 21 files changed, 156 insertions(+), 106 deletions(-) create mode 100644 src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.ts create mode 100644 src/pages/ProfilesExplorerView/domain/events/EventDataReceived.ts create mode 100644 src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesLabelFieldName.ts create mode 100644 src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesStatsValue.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx index db9711ea..e1ef9d9d 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx @@ -1,4 +1,4 @@ -import { DashboardCursorSync, LoadingState, VariableRefresh } from '@grafana/data'; +import { DashboardCursorSync, VariableRefresh } from '@grafana/data'; import { behaviors, EmbeddedSceneState, @@ -8,7 +8,6 @@ import { SceneCSSGridLayout, sceneGraph, SceneObjectBase, - SceneQueryRunner, VariableValueOption, VizPanelState, } from '@grafana/scenes'; @@ -17,19 +16,19 @@ import { noOp } from '@shared/domain/noOp'; import { debounce, isEqual } from 'lodash'; import React from 'react'; -import { FavAction } from '../../domain/actions/FavAction'; +import { EventDataReceived } from '../../domain/events/EventDataReceived'; import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../helpers/getSceneVariableValue'; -import { FavoritesDataSource } from '../../infrastructure/favorites/FavoritesDataSource'; import { SceneLabelValuesBarGauge } from '../SceneLabelValuesBarGauge'; import { SceneLabelValueStat } from '../SceneLabelValueStat'; import { SceneLabelValuesTimeseries } from '../SceneLabelValuesTimeseries'; import { SceneEmptyState } from './components/SceneEmptyState/SceneEmptyState'; import { SceneErrorState } from './components/SceneErrorState/SceneErrorState'; -import { LayoutType, SceneLayoutSwitcher } from './components/SceneLayoutSwitcher'; -import { SceneNoDataSwitcher } from './components/SceneNoDataSwitcher'; +import { LayoutType, SceneLayoutSwitcher, SceneLayoutSwitcherState } from './components/SceneLayoutSwitcher'; +import { SceneNoDataSwitcher, SceneNoDataSwitcherState } from './components/SceneNoDataSwitcher'; import { PanelType, ScenePanelTypeSwitcher } from './components/ScenePanelTypeSwitcher'; -import { SceneQuickFilter } from './components/SceneQuickFilter'; +import { SceneQuickFilter, SceneQuickFilterState } from './components/SceneQuickFilter'; +import { sortFavGridItems } from './domain/sortFavGridItems'; import { GridItemData } from './types/GridItemData'; interface SceneByVariableRepeaterGridState extends EmbeddedSceneState { @@ -49,25 +48,6 @@ const GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(400px, 1fr))'; const GRID_TEMPLATE_ROWS = '1fr'; const GRID_AUTO_ROWS = '240px'; -const DEFAULT_SORT_ITEMS_FN: SceneByVariableRepeaterGridState['sortItemsFn'] = function (a, b) { - const aIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(a)); - const bIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(b)); - - if (aIsFav && bIsFav) { - return a.label.localeCompare(b.label); - } - - if (bIsFav) { - return +1; - } - - if (aIsFav) { - return -1; - } - - return 0; -}; - export class SceneByVariableRepeaterGrid extends SceneObjectBase { static buildGridItemKey(item: GridItemData) { return `grid-item-${item.index}-${item.value}`; @@ -96,7 +76,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { + const onChangeState = (newState: SceneQuickFilterState, prevState?: SceneQuickFilterState) => { if (newState.searchText !== prevState?.searchText) { this.renderGridItems(); } @@ -195,7 +175,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { + const onChangeState = (newState: SceneLayoutSwitcherState, prevState?: SceneLayoutSwitcherState) => { if (newState.layout !== prevState?.layout) { body.setState({ templateColumns: SceneByVariableRepeaterGrid.getGridColumnsTemplate(newState.layout), @@ -219,7 +199,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { + const onChangeState = (newState: SceneNoDataSwitcherState, prevState?: SceneNoDataSwitcherState) => { if (newState.hideNoData !== prevState?.hideNoData) { this.setState({ hideNoData: newState.hideNoData === 'on' }); @@ -327,8 +307,8 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { - if (state.data?.state !== LoadingState.Done || state.data.series.length > 0) { + const sub = vizPanel.subscribeToEvent(EventDataReceived, (event) => { + if (event.payload.series.length > 0) { return; } @@ -377,9 +357,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase void; } diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher.tsx index 9d626252..4a8b351f 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher.tsx @@ -15,7 +15,7 @@ export enum PanelType { STATS = 'stats', } -interface ScenePanelTypeSwitcherState extends SceneObjectState { +export interface ScenePanelTypeSwitcherState extends SceneObjectState { panelType: PanelType; onChange?: (panelType: PanelType) => void; } diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneQuickFilter.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneQuickFilter.tsx index b140b9ba..f37fd24f 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneQuickFilter.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneQuickFilter.tsx @@ -10,7 +10,7 @@ import { Icon, IconButton, Input, useStyles2 } from '@grafana/ui'; import { reportInteraction } from '@shared/domain/reportInteraction'; import React from 'react'; -interface SceneQuickFilterState extends SceneObjectState { +export interface SceneQuickFilterState extends SceneObjectState { placeholder: string; searchText: string; onChange?: (searchText: string) => void; diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.ts b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.ts new file mode 100644 index 00000000..70f7c6e2 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/domain/sortFavGridItems.ts @@ -0,0 +1,22 @@ +import { FavAction } from '../../..//domain/actions/FavAction'; +import { FavoritesDataSource } from '../../../infrastructure/favorites/FavoritesDataSource'; +import { GridItemData } from '../types/GridItemData'; + +export const sortFavGridItems: (a: GridItemData, b: GridItemData) => number = function (a, b) { + const aIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(a)); + const bIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(b)); + + if (aIsFav && bIsFav) { + return a.label.localeCompare(b.label); + } + + if (bIsFav) { + return +1; + } + + if (aIsFav) { + return -1; + } + + return 0; +}; diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/infrastructure/data-transformations.ts b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/infrastructure/data-transformations.ts index 4054ff88..53566188 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/infrastructure/data-transformations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/infrastructure/data-transformations.ts @@ -2,6 +2,7 @@ import { DataFrame } from '@grafana/data'; import { merge } from 'lodash'; import { map, Observable } from 'rxjs'; +import { getSeriesStatsValue } from '../../../infrastructure/helpers/getSeriesStatsValue'; import { LabelsDataSource } from '../../../infrastructure/labels/LabelsDataSource'; // General note: because (e.g.) SceneLabelValuesTimeseries sets the data provider in its constructor, data can come as undefined, hence all the optional chaining operators @@ -43,8 +44,8 @@ export const sortSeries = () => (source: Observable) => source.pipe( map((data: DataFrame[]) => data?.sort((d1, d2) => { - const d1Sum = d1.meta?.stats?.find(({ displayName }) => displayName === 'allValuesSum')?.value || 0; - const d2Sum = d2.meta?.stats?.find(({ displayName }) => displayName === 'allValuesSum')?.value || 0; + const d1Sum = getSeriesStatsValue(d1, 'allValuesSum') || 0; + const d2Sum = getSeriesStatsValue(d2, 'allValuesSum') || 0; return d2Sum - d1Sum; }) ) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreFavorites/SceneExploreFavorites.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreFavorites/SceneExploreFavorites.tsx index 8eb34bcf..5fcaacbc 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreFavorites/SceneExploreFavorites.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreFavorites/SceneExploreFavorites.tsx @@ -5,8 +5,6 @@ import { PanelType } from '../../components/SceneByVariableRepeaterGrid/componen import { SceneByVariableRepeaterGrid } from '../../components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid'; import { GridItemData } from '../../components/SceneByVariableRepeaterGrid/types/GridItemData'; import { SceneDrawer } from '../../components/SceneDrawer'; -import { SceneLabelValuesBarGauge } from '../../components/SceneLabelValuesBarGauge'; -import { SceneLabelValuesTimeseries } from '../../components/SceneLabelValuesTimeseries'; import { FavAction } from '../../domain/actions/FavAction'; import { SelectAction } from '../../domain/actions/SelectAction'; import { EventExpandPanel } from '../../domain/events/EventExpandPanel'; @@ -17,6 +15,8 @@ import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { SceneLayoutSwitcher } from '../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; import { SceneNoDataSwitcher } from '../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; import { SceneQuickFilter } from '../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; +import { SceneLabelValuesBarGauge } from '../SceneLabelValuesBarGauge'; +import { SceneLabelValuesTimeseries } from '../SceneLabelValuesTimeseries'; interface SceneExploreFavoritesState extends EmbeddedSceneState { drawer: SceneDrawer; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx index 30602272..a393dcee 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph.tsx @@ -26,6 +26,7 @@ export class SceneExploreServiceFlameGraph extends SceneObjectBase [ new SelectAction({ EventClass: EventViewServiceProfiles, item }), new SelectAction({ EventClass: EventViewServiceLabels, item }), diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx index 263a20e8..4261e652 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/SceneExploreServiceLabels.tsx @@ -41,6 +41,7 @@ export class SceneExploreServiceLabels extends SceneObjectBase [ new SelectAction({ EventClass: EventViewServiceProfiles, item }), new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx index ce79764c..593127fa 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx @@ -16,16 +16,15 @@ import { noOp } from '@shared/domain/noOp'; import { debounce, isEqual } from 'lodash'; import React from 'react'; -import { FavAction } from '../../../domain/actions/FavAction'; import { findSceneObjectByClass } from '../../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../../helpers/getSceneVariableValue'; -import { FavoritesDataSource } from '../../../infrastructure/favorites/FavoritesDataSource'; import { SceneEmptyState } from '../../SceneByVariableRepeaterGrid/components/SceneEmptyState/SceneEmptyState'; import { SceneErrorState } from '../../SceneByVariableRepeaterGrid/components/SceneErrorState/SceneErrorState'; import { LayoutType, SceneLayoutSwitcher } from '../../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; import { SceneNoDataSwitcher } from '../../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; import { PanelType, ScenePanelTypeSwitcher } from '../../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; import { SceneQuickFilter } from '../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; +import { sortFavGridItems } from '../../SceneByVariableRepeaterGrid/domain/sortFavGridItems'; import { GridItemData } from '../../SceneByVariableRepeaterGrid/types/GridItemData'; import { SceneLabelValuesBarGauge } from '../../SceneLabelValuesBarGauge'; import { SceneLabelValueStat } from '../../SceneLabelValueStat'; @@ -44,25 +43,6 @@ const GRID_TEMPLATE_ROWS = '1fr'; const GRID_AUTO_ROWS = '240px'; const GRID_AUTO_ROWS_SMALL = '76px'; -const DEFAULT_SORT_ITEMS_FN: SceneLabelValuesGridState['sortItemsFn'] = function (a, b) { - const aIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(a)); - const bIsFav = FavoritesDataSource.exists(FavAction.buildFavorite(b)); - - if (aIsFav && bIsFav) { - return a.label.localeCompare(b.label); - } - - if (bIsFav) { - return +1; - } - - if (aIsFav) { - return -1; - } - - return 0; -}; - export class SceneLabelValuesGrid extends SceneObjectBase { static buildGridItemKey(item: GridItemData) { return `grid-item-${item.index}-${item.value}`; @@ -85,7 +65,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase { @@ -64,8 +71,8 @@ export class SceneLabelValuesBarGauge extends SceneObjectBase displayName === 'allValuesSum')?.value || 0; + for (const s of series) { + const allValuesSum = getSeriesStatsValue(s, 'allValuesSum') || 0; if (allValuesSum > max) { max = allValuesSum; @@ -111,18 +118,19 @@ export class SceneLabelValuesBarGauge extends SceneObjectBase ({ - matcher: { id: FieldMatcherID.byFrameRefID, options: serie.refId }, + return series.map((s, i) => ({ + matcher: { id: FieldMatcherID.byFrameRefID, options: s.refId }, properties: [ { id: 'displayName', - value: groupByLabel ? serie.fields[1].labels?.[groupByLabel] : serie.fields[1].name, + value: getSeriesLabelFieldName(s.fields[1], groupByLabel), }, { id: 'color', - value: { mode: 'fixed', fixedColor: getColorByIndex(item.index + i) }, + value: { mode: 'fixed', fixedColor: getColorByIndex(startColorIndex + i) }, }, ], })); diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx index aa377dba..46b5f6ca 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx @@ -11,7 +11,10 @@ import { import { GraphGradientMode } from '@grafana/schema'; import React from 'react'; +import { EventDataReceived } from '../domain/events/EventDataReceived'; import { getColorByIndex } from '../helpers/getColorByIndex'; +import { getSeriesLabelFieldName } from '../infrastructure/helpers/getSeriesLabelFieldName'; +import { getSeriesStatsValue } from '../infrastructure/helpers/getSeriesStatsValue'; import { LabelsDataSource } from '../infrastructure/labels/LabelsDataSource'; import { buildTimeSeriesQueryRunner } from '../infrastructure/timeseries/buildTimeSeriesQueryRunner'; import { @@ -23,6 +26,9 @@ import { import { GridItemData } from './SceneByVariableRepeaterGrid/types/GridItemData'; interface SceneLabelValuesTimeseriesState extends SceneObjectState { + item: GridItemData; + headerActions: (item: GridItemData) => VizPanelState['headerActions']; + displayAllValues: boolean; body: VizPanel; } @@ -32,12 +38,15 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase VizPanelState['headerActions']; - displayAllValues?: boolean; + item: SceneLabelValuesTimeseriesState['item']; + headerActions: SceneLabelValuesTimeseriesState['headerActions']; + displayAllValues?: SceneLabelValuesTimeseriesState['displayAllValues']; }) { super({ key: 'timeseries-label-values', + item, + headerActions, + displayAllValues: Boolean(displayAllValues), body: PanelBuilders.timeseries() .setTitle(item.label) .setData( @@ -52,20 +61,26 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { - if (state.data?.state !== LoadingState.Done || !state.data.series.length) { + if (state.data?.state !== LoadingState.Done) { return; } - const config = displayAllValues - ? this.getAllValuesConfig(item, state.data.series) - : this.getConfig(item, state.data.series); + const { series } = state.data; + + this.publishEvent(new EventDataReceived({ series }), true); + + if (!series.length) { + return; + } + + const config = this.state.displayAllValues ? this.getAllValuesConfig(series) : this.getConfig(series); body.setState(config); }); @@ -75,15 +90,15 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase 1 ? `${item.label} (${series.length})` : item.label; - const totalSeriesCount = - series[0].meta?.stats?.find(({ displayName }) => displayName === 'totalSeriesCount')?.value || 0; + const totalSeriesCount = getSeriesStatsValue(series[0], 'totalSeriesCount') || 0; const hasTooManySeries = totalSeriesCount > LabelsDataSource.MAX_TIMESERIES_LABEL_VALUES; description = hasTooManySeries @@ -102,12 +117,12 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { - let displayName = groupByLabel ? serie.fields[1].labels?.[groupByLabel] : serie.fields[1].name; + return series.map((s, i) => { + const metricField = s.fields[1]; + let displayName = getSeriesLabelFieldName(metricField, groupByLabel); if (series.length === 1) { - const allValuesSum = serie.meta?.stats?.find(({ displayName }) => displayName === 'allValuesSum')?.value || 0; - const { unit } = serie.fields[1].config; - const formattedValue = getValueFormat(unit)(allValuesSum); + const allValuesSum = getSeriesStatsValue(s, 'allValuesSum') || 0; + const formattedValue = getValueFormat(metricField.config.unit)(allValuesSum); - displayName = `${displayName} · total = ${formattedValue.text}${formattedValue.suffix}`; + displayName = `${displayName} / total = ${formattedValue.text}${formattedValue.suffix}`; } return { - matcher: { id: FieldMatcherID.byFrameRefID, options: serie.refId }, + matcher: { id: FieldMatcherID.byFrameRefID, options: s.refId }, properties: [ { id: 'displayName', @@ -151,6 +167,11 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase getSeriesLabelFieldName(s.fields[1], groupByLabel)); + } + updateTitle(newTitle: string) { this.state.body.setState({ title: newTitle }); } diff --git a/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx index cd53661e..648f16ce 100644 --- a/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx @@ -15,6 +15,8 @@ import { GridItemData } from './SceneByVariableRepeaterGrid/types/GridItemData'; import { SceneLabelValuesTimeseries } from './SceneLabelValuesTimeseries'; interface SceneMainServiceTimeseriesState extends SceneObjectState { + item?: GridItemData; + headerActions: (item: GridItemData) => VizPanelState['headerActions']; body?: SceneLabelValuesTimeseries; } @@ -28,18 +30,31 @@ export class SceneMainServiceTimeseries extends SceneObjectBase VizPanelState['headerActions'] }) { + constructor({ + item, + headerActions, + }: { + item: SceneMainServiceTimeseriesState['item']; + headerActions: SceneMainServiceTimeseriesState['headerActions']; + }) { super({ + item, + headerActions, body: undefined, }); - this.addActivationHandler(this.onActivate.bind(this, headerActions)); + this.addActivationHandler(this.onActivate.bind(this)); } - onActivate(headerActions: (item: GridItemData) => VizPanelState['headerActions']) { + onActivate() { + const { headerActions } = this.state; + this.setState({ body: new SceneLabelValuesTimeseries({ item: { + // we should test with users first but... + // ...uncomment to preserve the color of the item that was clicked (coming from "All services", "Favorites", etc.) + // index: item ? item.index : 0, index: 0, value: '', label: this.buildTitle(), diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 1b9fbf31..2b98e4ce 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -308,7 +308,7 @@ export class SceneProfilesExplorer extends SceneObjectBase; + gridControls: SceneObject[]; }; return { @@ -380,7 +380,7 @@ export class SceneProfilesExplorer extends SceneObjectBase ( - + ))}
diff --git a/src/pages/ProfilesExplorerView/domain/events/EventDataReceived.ts b/src/pages/ProfilesExplorerView/domain/events/EventDataReceived.ts new file mode 100644 index 00000000..782ab841 --- /dev/null +++ b/src/pages/ProfilesExplorerView/domain/events/EventDataReceived.ts @@ -0,0 +1,9 @@ +import { BusEventWithPayload, DataFrame } from '@grafana/data'; + +export interface EventDataReceivedPayload { + series: DataFrame[]; +} + +export class EventDataReceived extends BusEventWithPayload { + public static type = 'data-received'; +} diff --git a/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupBySelector.tsx b/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupBySelector.tsx index 215e43f2..f9bbc0c1 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupBySelector.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupBySelector.tsx @@ -72,7 +72,13 @@ export function GroupBySelector({ options, mainLabels, value, onChange, onRefres isClearable /> )} - + ); diff --git a/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx b/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx index 87468335..e1a9653e 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx @@ -22,6 +22,7 @@ export class GroupByVariable extends QueryVariable { datasource: PYROSCOPE_LABELS_DATA_SOURCE, // "hack": we want to subscribe to changes of dataSource, serviceName and profileMetricId // we could also add filters, but the Service labels exploration type would reload all labels each time they are modified + // which wouldn't be great UX query: '$dataSource and $profileMetricId{service_name="$serviceName"}', loading: true, }); diff --git a/src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesLabelFieldName.ts b/src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesLabelFieldName.ts new file mode 100644 index 00000000..0eb785bd --- /dev/null +++ b/src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesLabelFieldName.ts @@ -0,0 +1,4 @@ +import { Field } from '@grafana/data'; + +export const getSeriesLabelFieldName = (metricField: Field, label?: string) => + metricField.labels?.[label as string] || metricField.name; diff --git a/src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesStatsValue.ts b/src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesStatsValue.ts new file mode 100644 index 00000000..7c008652 --- /dev/null +++ b/src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesStatsValue.ts @@ -0,0 +1,4 @@ +import { DataFrame } from '@grafana/data'; + +export const getSeriesStatsValue = (series: DataFrame, displayName: string) => + series.meta?.stats?.find((s) => s.displayName === displayName)?.value; diff --git a/src/pages/ProfilesExplorerView/infrastructure/labels/LabelsDataSource.ts b/src/pages/ProfilesExplorerView/infrastructure/labels/LabelsDataSource.ts index 7688f7b3..76c8ec0a 100644 --- a/src/pages/ProfilesExplorerView/infrastructure/labels/LabelsDataSource.ts +++ b/src/pages/ProfilesExplorerView/infrastructure/labels/LabelsDataSource.ts @@ -72,6 +72,8 @@ export class LabelsDataSource extends RuntimeDataSource { const sceneObject = options.scopedVars?.__sceneObject?.value as GroupByVariable; // save bandwidth + // TODO: remove this when we can declare the GroupByVariable in the Scene it's used + // without messing up the variable URL sync if (!sceneObject.isActive) { return []; } diff --git a/src/shared/infrastructure/labels/labelsRepository.ts b/src/shared/infrastructure/labels/labelsRepository.ts index 9042d513..13ec7ce2 100644 --- a/src/shared/infrastructure/labels/labelsRepository.ts +++ b/src/shared/infrastructure/labels/labelsRepository.ts @@ -48,8 +48,7 @@ class LabelsRepository extends AbstractRepository 0, 'Invalid "from" parameter!'); - invariant(to > 0 && to > from, 'Invalid "to" parameter!'); + invariant(from > 0 && to > 0 && to > from, 'Invalid timerange!'); } async listLabels({ query, from, to }: ListLabelsOptions): Promise { From 1220b7f87b56c70cf419c7dbad116627f5475165 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Mon, 12 Aug 2024 13:00:51 +0200 Subject: [PATCH 28/76] chore(SceneLabelValuesTimeseries): Remove unused code --- .../components/SceneLabelValuesTimeseries.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx index 46b5f6ca..a262ce3e 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx @@ -167,11 +167,6 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase getSeriesLabelFieldName(s.fields[1], groupByLabel)); - } - updateTitle(newTitle: string) { this.state.body.setState({ title: newTitle }); } From 082c2111e94c3d993b7d39f9b72c5017a14aba3c Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Mon, 12 Aug 2024 13:19:08 +0200 Subject: [PATCH 29/76] chore(docs): Revert unwanted change --- docs/sources/get-started/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sources/get-started/index.md b/docs/sources/get-started/index.md index e1d39b31..0c1331e0 100644 --- a/docs/sources/get-started/index.md +++ b/docs/sources/get-started/index.md @@ -11,6 +11,10 @@ weight: 300 # Get started with Explore Profiles +{{% admonition type="note" %}} +Expand your observability journey and learn about [Explore Traces](https://grafana.com/docs/grafana-cloud/visualizations/simplified-exploration/traces/). +{{% /admonition %}} + Profiles can help you identify errors in your apps and services. Using this information, you can optimize and streamline your apps. From 5fdf90d1ffbdeda161c834213cc33d0ba0a26f39 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Mon, 12 Aug 2024 13:36:32 +0200 Subject: [PATCH 30/76] chore: Remove unused code --- .../components/SceneLabelValuesGrid.tsx | 404 ------------------ 1 file changed, 404 deletions(-) delete mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx deleted file mode 100644 index bed17bb3..00000000 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx +++ /dev/null @@ -1,404 +0,0 @@ -import { DashboardCursorSync, LoadingState, VariableRefresh } from '@grafana/data'; -import { - behaviors, - EmbeddedSceneState, - QueryVariable, - SceneComponentProps, - SceneCSSGridItem, - SceneCSSGridLayout, - sceneGraph, - SceneObjectBase, - SceneQueryRunner, - VizPanelState, -} from '@grafana/scenes'; -import { Spinner } from '@grafana/ui'; -import { noOp } from '@shared/domain/noOp'; -import { debounce, isEqual } from 'lodash'; -import React from 'react'; - -import { findSceneObjectByClass } from '../../../helpers/findSceneObjectByClass'; -import { getSceneVariableValue } from '../../../helpers/getSceneVariableValue'; -import { SceneEmptyState } from '../../SceneByVariableRepeaterGrid/components/SceneEmptyState/SceneEmptyState'; -import { SceneErrorState } from '../../SceneByVariableRepeaterGrid/components/SceneErrorState/SceneErrorState'; -import { LayoutType, SceneLayoutSwitcher } from '../../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; -import { SceneNoDataSwitcher } from '../../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; -import { PanelType, ScenePanelTypeSwitcher } from '../../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; -import { SceneQuickFilter } from '../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; -import { sortFavGridItems } from '../../SceneByVariableRepeaterGrid/domain/sortFavGridItems'; -import { GridItemData } from '../../SceneByVariableRepeaterGrid/types/GridItemData'; -import { SceneLabelValuesBarGauge } from '../../SceneLabelValuesBarGauge'; -import { SceneLabelValuesTimeseries } from '../../SceneLabelValuesTimeseries'; - -interface SceneLabelValuesGridState extends EmbeddedSceneState { - items: GridItemData[]; - headerActions: (item: GridItemData, items: GridItemData[]) => VizPanelState['headerActions']; - sortItemsFn: (a: GridItemData, b: GridItemData) => number; - hideNoData: boolean; -} - -const GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(400px, 1fr))'; - -const GRID_TEMPLATE_ROWS = '1fr'; -const GRID_AUTO_ROWS = '240px'; -const GRID_AUTO_ROWS_SMALL = '76px'; - -export class SceneLabelValuesGrid extends SceneObjectBase { - static buildGridItemKey(item: GridItemData) { - return `grid-item-${item.index}-${item.value}`; - } - - static getGridColumnsTemplate(layout: LayoutType) { - return layout === LayoutType.ROWS ? GRID_TEMPLATE_ROWS : GRID_TEMPLATE_COLUMNS; - } - - constructor({ - key, - headerActions, - sortItemsFn, - }: { - key: string; - headerActions: SceneLabelValuesGridState['headerActions']; - sortItemsFn?: SceneLabelValuesGridState['sortItemsFn']; - }) { - super({ - key, - items: [], - headerActions, - sortItemsFn: sortItemsFn || sortFavGridItems, - hideNoData: false, - body: new SceneCSSGridLayout({ - templateColumns: SceneLabelValuesGrid.getGridColumnsTemplate(SceneLayoutSwitcher.DEFAULT_LAYOUT), - autoRows: GRID_AUTO_ROWS, - isLazy: true, - $behaviors: [ - new behaviors.CursorSync({ - key: 'metricCrosshairSync', - sync: DashboardCursorSync.Crosshair, - }), - ], - children: [], - }), - }); - - this.addActivationHandler(this.onActivate.bind(this)); - } - - // eslint-disable-next-line sonarjs/cognitive-complexity - onActivate() { - // here we try to emulate VariableDependencyConfig.onVariableUpdateCompleted - const variable = sceneGraph.lookupVariable('groupBy', this) as QueryVariable & { update: () => void }; - - const variableSub = variable.subscribeToState((newState, prevState) => { - if (!newState.loading && prevState.loading) { - this.renderGridItems(); - } - }); - - // the "groupBy" variable data source will not fetch values if the variable is inactive - // (see src/pages/ProfilesExplorerView/data/labels/LabelsDataSource.ts) - // so we force an update here to be sure we have the latest values - variable.update(); - - const refreshSub = this.subscribeToRefreshClick(); - const quickFilterSub = this.subscribeToQuickFilterChange(); - const layoutChangeSub = this.subscribeToLayoutChange(); - const hideNoDataSub = this.subscribeToHideNoDataChange(); - - return () => { - hideNoDataSub.unsubscribe(); - layoutChangeSub.unsubscribe(); - quickFilterSub.unsubscribe(); - refreshSub.unsubscribe(); - - variableSub.unsubscribe(); - }; - } - - subscribeToRefreshClick() { - const variable = sceneGraph.lookupVariable('groupBy', this) as QueryVariable & { update: () => void }; - const originalRefresh = variable.state.refresh; - - variable.setState({ refresh: VariableRefresh.never }); - - const onClickRefresh = () => { - variable.update(); - }; - - // start of hack, for a better UX: we disable the variable "refresh" option and we allow the user to reload the list only by clicking on the "Refresh" button - // if we don't do this, every time the time range changes (even with auto-refresh on), - // all the timeseries present on the screen would be re-created, resulting in blinking and a poor UX - const refreshButton = document.querySelector( - '[data-testid="data-testid RefreshPicker run button"]' - ) as HTMLButtonElement; - - if (!refreshButton) { - console.error('SceneLabelValuesGrid: Refresh button not found! The list of items will never be updated.'); - } - - refreshButton?.addEventListener('click', onClickRefresh); - refreshButton?.setAttribute('title', 'Click to completely refresh all the panels present on the screen'); - // end of hack - - return { - unsubscribe() { - refreshButton?.removeAttribute('title'); - refreshButton?.removeEventListener('click', onClickRefresh); - variable.setState({ refresh: originalRefresh }); - }, - }; - } - - subscribeToQuickFilterChange() { - const quickFilter = findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter; - - const onChangeState = (newState: typeof quickFilter.state, prevState?: typeof quickFilter.state) => { - if (newState.searchText !== prevState?.searchText) { - this.renderGridItems(); - } - }; - - return quickFilter.subscribeToState(debounce(onChangeState, 250)); - } - - subscribeToLayoutChange() { - const layoutSwitcher = findSceneObjectByClass(this, SceneLayoutSwitcher) as SceneLayoutSwitcher; - - const body = this.state.body as SceneCSSGridLayout; - - const onChangeState = (newState: typeof layoutSwitcher.state, prevState?: typeof layoutSwitcher.state) => { - if (newState.layout !== prevState?.layout) { - body.setState({ - templateColumns: SceneLabelValuesGrid.getGridColumnsTemplate(newState.layout), - }); - } - }; - - onChangeState(layoutSwitcher.state); - - return layoutSwitcher.subscribeToState(onChangeState); - } - - subscribeToHideNoDataChange() { - const noDataSwitcher = findSceneObjectByClass(this, SceneNoDataSwitcher) as SceneNoDataSwitcher; - - if (!noDataSwitcher.isActive) { - this.setState({ hideNoData: false }); - - return { - unsubscribe: noOp, - }; - } - - const onChangeState = (newState: typeof noDataSwitcher.state, prevState?: typeof noDataSwitcher.state) => { - if (newState.hideNoData !== prevState?.hideNoData) { - this.setState({ hideNoData: newState.hideNoData === 'on' }); - - // we force render because this.state.items certainly have not changed but we want to update the UI panels anyway - this.renderGridItems(true); - } - }; - - onChangeState(noDataSwitcher.state); - - return noDataSwitcher.subscribeToState(onChangeState); - } - - buildItemsData(variable: QueryVariable) { - const { value: variableValue, options } = variable.state; - - const currentOption = options - .filter((o) => o.value !== 'all') - .find((o) => variableValue === JSON.parse(o.value as string).value); - - if (!currentOption) { - console.error('Cannot find the "%s" groupBy value among all the variable option!', variableValue); - return []; - } - - const serviceName = getSceneVariableValue(this, 'serviceName'); - const profileMetricId = getSceneVariableValue(this, 'profileMetricId'); - const panelTypeFromSwitcher = (findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher).state - .panelType; - - const panelType = panelTypeFromSwitcher; - - const items = JSON.parse(currentOption.value as string).groupBy.values.map((value: string, index: number) => { - return { - index, - value: value, - label: value, - queryRunnerParams: { - serviceName, - profileMetricId, - filters: [{ key: variableValue, operator: '=', value }], - }, - panelType, - }; - }); - - return this.filterItems(items).sort(this.state.sortItemsFn); - } - - shouldRenderItems(newItems: SceneLabelValuesGridState['items']) { - const { items } = this.state; - - if (!newItems.length || items.length !== newItems.length) { - return true; - } - - return !isEqual(items, newItems); - } - - renderGridItems(forceRender = false) { - const variable = sceneGraph.lookupVariable('groupBy', this) as QueryVariable; - - if (variable.state.loading) { - return; - } - - if (variable.state.error) { - this.renderErrorState(variable.state.error); - return; - } - - const newItems = this.buildItemsData(variable); - - if (!forceRender && !this.shouldRenderItems(newItems)) { - return; - } - - this.setState({ items: newItems }); - - if (!this.state.items.length) { - this.renderEmptyState(); - return; - } - - const gridItems = this.state.items.map((item) => { - const vizPanel = this.buildVizPanel(item); - - if (this.state.hideNoData) { - this.setupHideNoData(vizPanel); - } - - return new SceneCSSGridItem({ - key: SceneLabelValuesGrid.buildGridItemKey(item), - body: vizPanel, - }); - }); - - (this.state.body as SceneCSSGridLayout).setState({ - autoRows: this.getAutoRows(), // required to have the correct grid items height - children: gridItems, - }); - } - - buildVizPanel(item: GridItemData) { - switch (item.panelType) { - case PanelType.BARGAUGE: - return new SceneLabelValuesBarGauge({ - item, - headerActions: this.state.headerActions.bind(null, item, this.state.items), - }); - - case PanelType.TIMESERIES: - default: - return new SceneLabelValuesTimeseries({ - item, - headerActions: this.state.headerActions.bind(null, item, this.state.items), - }); - } - } - - setupHideNoData(vizPanel: SceneLabelValuesTimeseries | SceneLabelValuesBarGauge) { - const sub = (vizPanel.state.body.state.$data as SceneQueryRunner)!.subscribeToState((state) => { - if (state.data?.state !== LoadingState.Done || state.data.series.length > 0) { - return; - } - - const gridItem = sceneGraph.getAncestor(vizPanel, SceneCSSGridItem); - const { key: gridItemKey } = gridItem.state; - const grid = sceneGraph.getAncestor(gridItem, SceneCSSGridLayout); - - const filteredChildren = grid.state.children.filter((c) => c.state.key !== gridItemKey); - - if (!filteredChildren.length) { - this.renderEmptyState(); - } else { - grid.setState({ children: filteredChildren }); - } - }); - - vizPanel.addActivationHandler(() => { - return () => { - sub.unsubscribe(); - }; - }); - } - - getAutoRows() { - const { panelType } = (findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher).state; - return panelType === PanelType.BARGAUGE ? GRID_AUTO_ROWS_SMALL : GRID_AUTO_ROWS; - } - - filterItems(items: SceneLabelValuesGridState['items']) { - const quickFilterScene = findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter; - const { searchText } = quickFilterScene.state; - - if (!searchText) { - return items; - } - - const regexes = searchText - .split(',') - .map((t) => t.trim()) - .filter(Boolean) - .map((r) => { - try { - return new RegExp(r); - } catch { - return null; - } - }) - .filter(Boolean) as RegExp[]; - - return items.filter(({ label }) => regexes.some((r) => r.test(label))); - } - - renderEmptyState() { - const body = this.state.body as SceneCSSGridLayout; - - body.setState({ - autoRows: '480px', - children: [ - new SceneCSSGridItem({ - body: new SceneEmptyState({ - message: 'No results', - }), - }), - ], - }); - } - - renderErrorState(error: Error) { - const body = this.state.body as SceneCSSGridLayout; - - body.setState({ - autoRows: '480px', - children: [ - new SceneCSSGridItem({ - body: new SceneErrorState({ - message: error.toString(), - }), - }), - ], - }); - } - - static Component({ model }: SceneComponentProps) { - const { body } = model.useState(); - const { loading } = (sceneGraph.lookupVariable('groupBy', model) as QueryVariable)?.useState(); - - return loading ? : ; - } -} From 5ead9e7c00f0dc2eb21851e46507e06a873fad2a Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Mon, 12 Aug 2024 13:38:16 +0200 Subject: [PATCH 31/76] chore: Remove unused code --- .../components/SceneLabelValuesTimeseries.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx index ebe9b8f0..a262ce3e 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx @@ -13,7 +13,6 @@ import React from 'react'; import { EventDataReceived } from '../domain/events/EventDataReceived'; import { getColorByIndex } from '../helpers/getColorByIndex'; -import { getLabelFieldName } from '../helpers/getLabelFieldName'; import { getSeriesLabelFieldName } from '../infrastructure/helpers/getSeriesLabelFieldName'; import { getSeriesStatsValue } from '../infrastructure/helpers/getSeriesStatsValue'; import { LabelsDataSource } from '../infrastructure/labels/LabelsDataSource'; @@ -168,11 +167,6 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase getLabelFieldName(s.fields[1], groupByLabel)); - } - updateTitle(newTitle: string) { this.state.body.setState({ title: newTitle }); } From 7aaee70e4622ad1d6b40a0574bc0b57d0aa487a7 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Mon, 12 Aug 2024 13:38:54 +0200 Subject: [PATCH 32/76] chore: Remove unused code --- src/pages/ProfilesExplorerView/helpers/getLabelFieldName.ts | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 src/pages/ProfilesExplorerView/helpers/getLabelFieldName.ts diff --git a/src/pages/ProfilesExplorerView/helpers/getLabelFieldName.ts b/src/pages/ProfilesExplorerView/helpers/getLabelFieldName.ts deleted file mode 100644 index 8c1dfa2b..00000000 --- a/src/pages/ProfilesExplorerView/helpers/getLabelFieldName.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Field } from '@grafana/data'; - -export const getLabelFieldName = (metricField: Field, label?: string) => - metricField.labels?.[label as string] || metricField.name; From 5e6d09d856ac2a1604e1ab28e5b29ea3280aa382 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Mon, 12 Aug 2024 13:50:00 +0200 Subject: [PATCH 33/76] chore(*): Small code improvements --- .../components/SceneGroupByLabels/SceneGroupByLabels.tsx | 6 +++--- .../SceneLabelValuesGrid => }/ui/CompareActions.tsx | 6 +++--- .../infrastructure/helpers/getSeriesLabelFieldName.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/{components/SceneLabelValuesGrid => }/ui/CompareActions.tsx (90%) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index f0d8f765..bf9d19de 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -46,8 +46,8 @@ import { SceneProfilesExplorer } from '../../../SceneProfilesExplorer/SceneProfi import { SceneStatsPanel } from './components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel'; import { CompareTarget } from './components/SceneLabelValuesGrid/domain/types'; import { SceneLabelValuesGrid } from './components/SceneLabelValuesGrid/SceneLabelValuesGrid'; -import { CompareActions } from './components/SceneLabelValuesGrid/ui/CompareActions'; import { EventSelectForCompare } from './domain/events/EventSelectForCompare'; +import { CompareActions } from './ui/CompareActions'; export interface SceneGroupByLabelsState extends SceneObjectState { body?: SceneObject; @@ -224,6 +224,8 @@ export class SceneGroupByLabels extends SceneObjectBase 'Search label values (comma-separated regexes are supported)' ); + this.clearCompare(); + const { value, options } = groupByVariableState; const index = options @@ -235,8 +237,6 @@ export class SceneGroupByLabels extends SceneObjectBase this.setState({ body: this.buildSceneLabelValuesGrid(value as string, startColorIndex), }); - - this.clearCompare(); } buildSceneLabelValuesGrid(label: string, startColorIndex: number) { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx similarity index 90% rename from src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx index 06bca52e..3c6fda75 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/ui/CompareActions.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx @@ -4,9 +4,9 @@ import { Button, useStyles2 } from '@grafana/ui'; import { noOp } from '@shared/domain/noOp'; import React from 'react'; -import { SceneGroupByLabelsState } from '../../../SceneGroupByLabels'; -import { SceneStatsPanel } from '../components/SceneStatsPanel/SceneStatsPanel'; -import { CompareTarget } from '../domain/types'; +import { SceneStatsPanel } from '../components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel'; +import { CompareTarget } from '../components/SceneLabelValuesGrid/domain/types'; +import { SceneGroupByLabelsState } from '../SceneGroupByLabels'; type CompareButtonProps = { compare: SceneGroupByLabelsState['compare']; diff --git a/src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesLabelFieldName.ts b/src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesLabelFieldName.ts index 0eb785bd..917272e0 100644 --- a/src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesLabelFieldName.ts +++ b/src/pages/ProfilesExplorerView/infrastructure/helpers/getSeriesLabelFieldName.ts @@ -1,4 +1,4 @@ import { Field } from '@grafana/data'; export const getSeriesLabelFieldName = (metricField: Field, label?: string) => - metricField.labels?.[label as string] || metricField.name; + metricField.labels?.[label as string] || metricField.name; // metricField.labels can be empty when the ingested profiles do not have a label value set From 365644b2c7ebf7c7fa1e2f59b6a986a1ea1e42e1 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Mon, 12 Aug 2024 15:29:13 +0200 Subject: [PATCH 34/76] fix(*): Fix some imports --- .../components/SceneComparePanel/SceneComparePanel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index 2ecbb3e1..10fa7b41 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -17,13 +17,13 @@ import { getProfileMetric, ProfileMetricId } from '@shared/infrastructure/profil import React from 'react'; import { BASELINE_COLORS, COMPARISON_COLORS } from '../../../../../../pages/ComparisonView/ui/colors'; -import { FiltersVariable } from '../../../..//domain/variables/FiltersVariable/FiltersVariable'; +import { FiltersVariable } from '../../../../domain/variables/FiltersVariable/FiltersVariable'; import { getSceneVariableValue } from '../../../../helpers/getSceneVariableValue'; +import { getSeriesStatsValue } from '../../../../infrastructure/helpers/getSeriesStatsValue'; import { getProfileMetricLabel } from '../../../../infrastructure/series/helpers/getProfileMetricLabel'; import { addRefId, addStats } from '../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; -import { getSeriesStatsValue } from '../../../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/getSeriesStatsValue'; import { CompareTarget } from '../../../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; -import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries/SceneLabelValuesTimeseries'; +import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries'; import { SwitchTimeRangeSelectionModeAction, TimerangeSelectionMode, From 28aea562bcfb0e6f67c939fbf8f3e58f9b8137b0 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 13 Aug 2024 12:41:48 +0200 Subject: [PATCH 35/76] refactor(*): Use native methods to retrieve Scene objects --- .../SceneByVariableRepeaterGrid.tsx | 11 ++++---- .../SceneExploreAllServices.tsx | 17 +++++++---- .../SceneExploreFavorites.tsx | 17 +++++++---- .../SceneExploreServiceFlameGraph.tsx | 17 ++++++----- .../SceneExploreServiceLabels.tsx | 20 ++++++------- .../components/SceneGroupByLabels.tsx | 23 ++++++++------- .../components/SceneLabelValuesGrid.tsx | 13 ++++----- .../SceneExploreServiceProfileTypes.tsx | 19 ++++++++----- .../SceneProfilesExplorer.tsx | 10 +++---- .../domain/actions/CompareAction.tsx | 3 +- .../FiltersVariable/FiltersVariable.tsx | 28 +++++++++---------- .../GroupByVariable/GroupByVariable.tsx | 1 + .../variables/ProfileMetricVariable.tsx | 1 + .../variables/ProfilesDataSourceVariable.ts | 1 + .../domain/variables/ServiceNameVariable.tsx | 12 ++++++-- .../helpers/findSceneObjectByClass.ts | 13 --------- .../helpers/findSceneObjectByKey.ts | 13 --------- 17 files changed, 105 insertions(+), 114 deletions(-) delete mode 100644 src/pages/ProfilesExplorerView/helpers/findSceneObjectByClass.ts delete mode 100644 src/pages/ProfilesExplorerView/helpers/findSceneObjectByKey.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx index e1ef9d9d..cc079aed 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx @@ -17,7 +17,6 @@ import { debounce, isEqual } from 'lodash'; import React from 'react'; import { EventDataReceived } from '../../domain/events/EventDataReceived'; -import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../helpers/getSceneVariableValue'; import { SceneLabelValuesBarGauge } from '../SceneLabelValuesBarGauge'; import { SceneLabelValueStat } from '../SceneLabelValueStat'; @@ -159,7 +158,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { if (newState.searchText !== prevState?.searchText) { @@ -171,7 +170,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { @@ -89,9 +94,9 @@ export class SceneExploreFavorites extends SceneObjectBase } subscribeToGroupByChange() { - const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; + const groupByVariable = sceneGraph.findByKeyAndType(this, 'groupBy', GroupByVariable); const onChangeState = (newState: typeof groupByVariable.state, prevState?: typeof groupByVariable.state) => { if (newState.value !== prevState?.value) { - const quickFilter = findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter; + const quickFilter = sceneGraph.findByKeyAndType(this, 'quick-filter', SceneQuickFilter); quickFilter.clear(); if (newState.value === 'all') { @@ -185,7 +184,7 @@ export class SceneGroupByLabels extends SceneObjectBase } subscribeToPanelTypeChange() { - const panelTypeSwitcher = findSceneObjectByClass(this, ScenePanelTypeSwitcher) as ScenePanelTypeSwitcher; + const panelTypeSwitcher = sceneGraph.findByKeyAndType(this, 'panel-type-switcher', ScenePanelTypeSwitcher); return panelTypeSwitcher.subscribeToState( (newState: typeof panelTypeSwitcher.state, prevState?: typeof panelTypeSwitcher.state) => { @@ -197,8 +196,8 @@ export class SceneGroupByLabels extends SceneObjectBase } subscribeToFiltersChange() { - const filtersVariable = findSceneObjectByClass(this, FiltersVariable) as FiltersVariable; - const noDataSwitcher = findSceneObjectByClass(this, SceneNoDataSwitcher) as SceneNoDataSwitcher; + const filtersVariable = sceneGraph.findByKeyAndType(this, 'filters', FiltersVariable); + const noDataSwitcher = sceneGraph.findByKeyAndType(this, 'no-data-switcher', SceneNoDataSwitcher); // the handler will be called each time a filter is added/removed/modified return filtersVariable.subscribeToState(() => { @@ -233,7 +232,7 @@ export class SceneGroupByLabels extends SceneObjectBase selectLabel({ queryRunnerParams }: GridItemData) { const labelValue = queryRunnerParams!.groupBy!.label; - const groupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; + const groupByVariable = sceneGraph.findByKeyAndType(this, 'groupBy', GroupByVariable); groupByVariable.changeValueTo(labelValue); @@ -242,7 +241,7 @@ export class SceneGroupByLabels extends SceneObjectBase } addLabelValueToFilters(item: GridItemData) { - const filterByVariable = findSceneObjectByClass(this, FiltersVariable) as FiltersVariable; + const filterByVariable = sceneGraph.findByKeyAndType(this, 'filters', FiltersVariable); let filterToAdd: AdHocVariableFilter; const { filters, groupBy } = item.queryRunnerParams; @@ -260,7 +259,7 @@ export class SceneGroupByLabels extends SceneObjectBase addFilter(filterByVariable, filterToAdd); - const goupByVariable = findSceneObjectByClass(this, GroupByVariable) as GroupByVariable; + const goupByVariable = sceneGraph.findByKeyAndType(this, 'groupBy', GroupByVariable); goupByVariable.changeValueTo(GroupByVariable.DEFAULT_VALUE); } @@ -292,8 +291,8 @@ export class SceneGroupByLabels extends SceneObjectBase const { body, drawer } = model.useState(); - const groupByVariable = findSceneObjectByClass(model, GroupByVariable); - const { gridControls } = (findSceneObjectByClass(model, SceneProfilesExplorer) as SceneProfilesExplorer).state; + const groupByVariable = sceneGraph.findByKeyAndType(model, 'groupBy', GroupByVariable); + const { gridControls } = sceneGraph.findByKeyAndType(model, 'profiles-explorer', SceneProfilesExplorer).state; return (
diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx index 593127fa..ea55bf95 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx @@ -16,7 +16,6 @@ import { noOp } from '@shared/domain/noOp'; import { debounce, isEqual } from 'lodash'; import React from 'react'; -import { findSceneObjectByClass } from '../../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../../helpers/getSceneVariableValue'; import { SceneEmptyState } from '../../SceneByVariableRepeaterGrid/components/SceneEmptyState/SceneEmptyState'; import { SceneErrorState } from '../../SceneByVariableRepeaterGrid/components/SceneErrorState/SceneErrorState'; @@ -150,7 +149,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase { if (newState.searchText !== prevState?.searchText) { @@ -162,7 +161,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase { diff --git a/src/pages/ProfilesExplorerView/domain/actions/CompareAction.tsx b/src/pages/ProfilesExplorerView/domain/actions/CompareAction.tsx index 86f963b2..e25dbcb6 100644 --- a/src/pages/ProfilesExplorerView/domain/actions/CompareAction.tsx +++ b/src/pages/ProfilesExplorerView/domain/actions/CompareAction.tsx @@ -16,7 +16,6 @@ import React, { useMemo } from 'react'; import { GridItemData } from '../../components/SceneByVariableRepeaterGrid/types/GridItemData'; import { computeRoundedTimeRange } from '../../helpers/computeRoundedTimeRange'; -import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { getSceneVariableValue } from '../../helpers/getSceneVariableValue'; import { interpolateQueryRunnerVariables } from '../../infrastructure/helpers/interpolateQueryRunnerVariables'; import { FiltersVariable } from '../variables/FiltersVariable/FiltersVariable'; @@ -120,7 +119,7 @@ export class CompareAction extends SceneObjectBase { diffUrl.searchParams.set('from', from.toString()); diffUrl.searchParams.set('to', to.toString()); - const { filters: queryFilters } = (findSceneObjectByClass(this, FiltersVariable) as FiltersVariable).state; + const { filters: queryFilters } = sceneGraph.findByKeyAndType(this, 'filters', FiltersVariable).state; const { serviceName: serviceId, profileMetricId } = interpolateQueryRunnerVariables(this, this.state.item); // query - just in case diff --git a/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx b/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx index cb442373..17de35f7 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx @@ -6,7 +6,6 @@ import { QueryBuilder } from '@shared/components/QueryBuilder/QueryBuilder'; import { useQueryFromUrl } from '@shared/domain/url-params/useQueryFromUrl'; import React, { useEffect, useMemo } from 'react'; -import { findSceneObjectByClass } from '../../../helpers/findSceneObjectByClass'; import { ProfileMetricVariable } from '../ProfileMetricVariable'; import { ProfilesDataSourceVariable } from '../ProfilesDataSourceVariable'; import { ServiceNameVariable } from '../ServiceNameVariable'; @@ -17,6 +16,7 @@ export class FiltersVariable extends AdHocFiltersVariable { constructor() { super({ + key: 'filters', name: 'filters', label: 'Filters', filters: FiltersVariable.DEFAULT_VALUE, @@ -24,11 +24,11 @@ export class FiltersVariable extends AdHocFiltersVariable { this.addActivationHandler(() => { // VariableDependencyConfig does not work :man_shrug: (never called) - const dataSourceSub = ( - findSceneObjectByClass(this, ProfilesDataSourceVariable) as ProfilesDataSourceVariable - ).subscribeToState(() => { - this.setState({ filters: [] }); - }); + const dataSourceSub = sceneGraph + .findByKeyAndType(this, 'dataSource', ProfilesDataSourceVariable) + .subscribeToState(() => { + this.setState({ filters: [] }); + }); return () => { dataSourceSub.unsubscribe(); @@ -47,17 +47,15 @@ export class FiltersVariable extends AdHocFiltersVariable { const { filters } = model.useState(); const [, setQuery] = useQueryFromUrl(); - const { value: dataSourceUid } = ( - findSceneObjectByClass(model, ProfilesDataSourceVariable) as ProfilesDataSourceVariable - ).useState(); + const { value: dataSourceUid } = sceneGraph + .findByKeyAndType(model, 'dataSource', ProfilesDataSourceVariable) + .useState(); - const { value: serviceName } = ( - findSceneObjectByClass(model, ServiceNameVariable) as ServiceNameVariable - ).useState(); + const { value: serviceName } = sceneGraph.findByKeyAndType(model, 'serviceName', ServiceNameVariable).useState(); - const { value: profileMetricId } = ( - findSceneObjectByClass(model, ProfileMetricVariable) as ProfileMetricVariable - ).useState(); + const { value: profileMetricId } = sceneGraph + .findByKeyAndType(model, 'profileMetricId', ProfileMetricVariable) + .useState(); const filterExpression = useMemo( () => expressionBuilder(serviceName as string, profileMetricId as string, filters), diff --git a/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx b/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx index e1a9653e..44b79711 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/GroupByVariable/GroupByVariable.tsx @@ -17,6 +17,7 @@ export class GroupByVariable extends QueryVariable { constructor() { super({ + key: 'groupBy', name: 'groupBy', label: 'Group by labels', datasource: PYROSCOPE_LABELS_DATA_SOURCE, diff --git a/src/pages/ProfilesExplorerView/domain/variables/ProfileMetricVariable.tsx b/src/pages/ProfilesExplorerView/domain/variables/ProfileMetricVariable.tsx index e0011a20..e15c2193 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/ProfileMetricVariable.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/ProfileMetricVariable.tsx @@ -32,6 +32,7 @@ export class ProfileMetricVariable extends QueryVariable { constructor(state?: ProfileMetricVariableState) { super({ + key: 'profileMetricId', name: 'profileMetricId', label: '🔥 Profile type', datasource: PYROSCOPE_SERIES_DATA_SOURCE, diff --git a/src/pages/ProfilesExplorerView/domain/variables/ProfilesDataSourceVariable.ts b/src/pages/ProfilesExplorerView/domain/variables/ProfilesDataSourceVariable.ts index 6fd3a0c6..676c400d 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/ProfilesDataSourceVariable.ts +++ b/src/pages/ProfilesExplorerView/domain/variables/ProfilesDataSourceVariable.ts @@ -6,6 +6,7 @@ export class ProfilesDataSourceVariable extends DataSourceVariable { constructor() { super({ pluginId: 'grafana-pyroscope-datasource', + key: 'dataSource', name: 'dataSource', label: 'Data source', skipUrlSync: true, diff --git a/src/pages/ProfilesExplorerView/domain/variables/ServiceNameVariable.tsx b/src/pages/ProfilesExplorerView/domain/variables/ServiceNameVariable.tsx index 10793385..e9db73fd 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/ServiceNameVariable.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/ServiceNameVariable.tsx @@ -1,13 +1,18 @@ import { css } from '@emotion/css'; import { GrafanaTheme2, VariableRefresh } from '@grafana/data'; -import { MultiValueVariable, QueryVariable, SceneComponentProps, VariableValueOption } from '@grafana/scenes'; +import { + MultiValueVariable, + QueryVariable, + SceneComponentProps, + sceneGraph, + VariableValueOption, +} from '@grafana/scenes'; import { Cascader, Icon, Tooltip, useStyles2 } from '@grafana/ui'; import { buildServiceNameCascaderOptions } from '@shared/components/Toolbar/domain/useBuildServiceNameOptions'; import { reportInteraction } from '@shared/domain/reportInteraction'; import React, { useMemo } from 'react'; import { lastValueFrom } from 'rxjs'; -import { findSceneObjectByClass } from '../../helpers/findSceneObjectByClass'; import { PYROSCOPE_SERIES_DATA_SOURCE } from '../../infrastructure/pyroscope-data-sources'; import { FiltersVariable } from './FiltersVariable/FiltersVariable'; @@ -25,6 +30,7 @@ export class ServiceNameVariable extends QueryVariable { constructor(state?: ServiceNameVariableState) { super({ + key: 'serviceName', name: 'serviceName', label: '🚀 Service', datasource: PYROSCOPE_SERIES_DATA_SOURCE, @@ -69,7 +75,7 @@ export class ServiceNameVariable extends QueryVariable { // manually reset filters - the "Scenes way" would be to listen to the variable changes but it leads to errors // see comments in src/pages/ProfilesExplorerView/variables/FiltersVariable/FiltersVariable.tsx - const filtersVariable = findSceneObjectByClass(this, FiltersVariable) as FiltersVariable; + const filtersVariable = sceneGraph.findByKeyAndType(this, 'filters', FiltersVariable); filtersVariable.setState({ filters: [] }); }; diff --git a/src/pages/ProfilesExplorerView/helpers/findSceneObjectByClass.ts b/src/pages/ProfilesExplorerView/helpers/findSceneObjectByClass.ts deleted file mode 100644 index cb231c39..00000000 --- a/src/pages/ProfilesExplorerView/helpers/findSceneObjectByClass.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { sceneGraph, SceneObject } from '@grafana/scenes'; - -export function findSceneObjectByClass(sceneObject: SceneObject, Class: Function): SceneObject { - const objectFound = sceneGraph.findObject(sceneObject, (o) => o instanceof Class); - - if (!objectFound) { - const error = new Error(`Unable to find any scene object for class "${Class.name}"!`); - console.error(error); - throw error; - } - - return objectFound; -} diff --git a/src/pages/ProfilesExplorerView/helpers/findSceneObjectByKey.ts b/src/pages/ProfilesExplorerView/helpers/findSceneObjectByKey.ts deleted file mode 100644 index b3975694..00000000 --- a/src/pages/ProfilesExplorerView/helpers/findSceneObjectByKey.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { sceneGraph, SceneObject } from '@grafana/scenes'; - -export function findSceneObjectByKey(sceneObject: SceneObject, key: string): SceneObject { - const objectFound = sceneGraph.findObject(sceneObject, (o) => o.state.key === key); - - if (!objectFound) { - const error = new Error(`Unable to find any scene object with key="${key}"!`); - console.error(error); - throw error; - } - - return objectFound; -} From d71d0e5f9af8501bb1c1224df2b07fcda7aa4af3 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 13 Aug 2024 13:06:34 +0200 Subject: [PATCH 36/76] chore: ... --- .../components/SceneGroupByLabels.tsx | 325 -------------- .../SceneLabelValuesGrid.tsx | 6 +- .../components/SceneLabelValuesGrid.tsx | 410 ------------------ 3 files changed, 1 insertion(+), 740 deletions(-) delete mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels.tsx delete mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels.tsx deleted file mode 100644 index 1ec51e06..00000000 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import { css } from '@emotion/css'; -import { AdHocVariableFilter, GrafanaTheme2 } from '@grafana/data'; -import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; -import { Stack, useStyles2 } from '@grafana/ui'; -import React from 'react'; - -import { CompareAction } from '../../../domain/actions/CompareAction'; -import { FavAction } from '../../../domain/actions/FavAction'; -import { SelectAction } from '../../../domain/actions/SelectAction'; -import { EventAddLabelToFilters } from '../../../domain/events/EventAddLabelToFilters'; -import { EventExpandPanel } from '../../../domain/events/EventExpandPanel'; -import { EventSelectLabel } from '../../../domain/events/EventSelectLabel'; -import { EventViewServiceFlameGraph } from '../../../domain/events/EventViewServiceFlameGraph'; -import { addFilter } from '../../../domain/variables/FiltersVariable/filters-ops'; -import { FiltersVariable } from '../../../domain/variables/FiltersVariable/FiltersVariable'; -import { GroupByVariable } from '../../../domain/variables/GroupByVariable/GroupByVariable'; -import { getSceneVariableValue } from '../../../helpers/getSceneVariableValue'; -import { getProfileMetricLabel } from '../../../infrastructure/series/helpers/getProfileMetricLabel'; -import { SceneNoDataSwitcher } from '../../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; -import { PanelType, ScenePanelTypeSwitcher } from '../../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; -import { SceneQuickFilter } from '../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; -import { SceneByVariableRepeaterGrid } from '../../SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid'; -import { GridItemData } from '../../SceneByVariableRepeaterGrid/types/GridItemData'; -import { SceneDrawer } from '../../SceneDrawer'; -import { SceneLabelValuesBarGauge } from '../../SceneLabelValuesBarGauge'; -import { SceneLabelValuesTimeseries } from '../../SceneLabelValuesTimeseries'; -import { SceneProfilesExplorer } from '../../SceneProfilesExplorer/SceneProfilesExplorer'; -import { SceneLabelValuesGrid } from './SceneLabelValuesGrid'; - -interface SceneGroupByLabelsState extends SceneObjectState { - body?: SceneObject; - drawer: SceneDrawer; -} - -export class SceneGroupByLabels extends SceneObjectBase { - constructor() { - super({ - key: 'group-by-labels', - body: undefined, - drawer: new SceneDrawer(), - }); - - this.addActivationHandler(this.onActivate.bind(this)); - } - - onActivate() { - const groupBySub = this.subscribeToGroupByChange(); - const panelTypeChangeSub = this.subscribeToPanelTypeChange(); - const filtersSub = this.subscribeToFiltersChange(); - const panelEventsSub = this.subscribeToPanelEvents(); - - return () => { - panelTypeChangeSub.unsubscribe(); - filtersSub.unsubscribe(); - panelEventsSub.unsubscribe(); - groupBySub.unsubscribe(); - }; - } - - subscribeToGroupByChange() { - const groupByVariable = sceneGraph.findByKeyAndType(this, 'groupBy', GroupByVariable); - - const onChangeState = (newState: typeof groupByVariable.state, prevState?: typeof groupByVariable.state) => { - if (newState.value !== prevState?.value) { - const quickFilter = sceneGraph.findByKeyAndType(this, 'quick-filter', SceneQuickFilter); - quickFilter.clear(); - - if (newState.value === 'all') { - quickFilter.setPlaceholder('Search labels (comma-separated regexes are supported)'); - - this.setState({ - body: this.buildSceneByVariableRepeaterGrid(), - }); - } else { - quickFilter.setPlaceholder('Search label values (comma-separated regexes are supported)'); - - this.setState({ - body: this.buildSceneLabelValuesGrid(), - }); - } - } - }; - - onChangeState(groupByVariable.state); - - return groupByVariable.subscribeToState(onChangeState); - } - - buildSceneByVariableRepeaterGrid() { - return new SceneByVariableRepeaterGrid({ - key: 'service-labels-grid', - variableName: 'groupBy', - mapOptionToItem: (option, index, { serviceName, profileMetricId, panelType }) => { - if (option.value === 'all') { - return null; - } - - // see LabelsDataSource.ts - const { value, groupBy } = JSON.parse(option.value as string); - - return { - index, - value, - // remove the count in parenthesis that exists in option.label - // it'll be set by SceneLabelValuesTimeseries or SceneLabelValuesBarGauge - label: value, - queryRunnerParams: { - serviceName, - profileMetricId, - groupBy, - filters: [], - }, - panelType: panelType as PanelType, - }; - }, - headerActions: (item, items) => { - const { queryRunnerParams } = item; - - if (!queryRunnerParams.groupBy) { - if (items.length > 1) { - return [ - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), - new SelectAction({ EventClass: EventAddLabelToFilters, item }), - new CompareAction({ item }), - new FavAction({ item }), - ]; - } - - return [ - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), - new SelectAction({ EventClass: EventAddLabelToFilters, item }), - new FavAction({ item }), - ]; - } - - // FIXME: should be based on how many series are displayed -> re-render header actions after each data load - if (queryRunnerParams.groupBy.values.length > 1) { - return [ - new SelectAction({ EventClass: EventSelectLabel, item }), - new SelectAction({ EventClass: EventExpandPanel, item }), - new FavAction({ item }), - ]; - } - - return [new FavAction({ item })]; - }, - }); - } - - buildSceneLabelValuesGrid() { - return new SceneLabelValuesGrid({ - key: 'service-label-values-grid', - headerActions: (item, items) => { - const { queryRunnerParams } = item; - - if (!queryRunnerParams.groupBy) { - if (items.length > 1) { - return [ - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), - new SelectAction({ EventClass: EventAddLabelToFilters, item }), - new CompareAction({ item }), - new FavAction({ item }), - ]; - } - - return [ - new SelectAction({ EventClass: EventViewServiceFlameGraph, item }), - new SelectAction({ EventClass: EventAddLabelToFilters, item }), - new FavAction({ item }), - ]; - } - - if (queryRunnerParams.groupBy.values.length > 1) { - return [ - new SelectAction({ EventClass: EventSelectLabel, item }), - new SelectAction({ EventClass: EventExpandPanel, item }), - new FavAction({ item }), - ]; - } - - return [new FavAction({ item })]; - }, - }); - } - - subscribeToPanelTypeChange() { - const panelTypeSwitcher = sceneGraph.findByKeyAndType(this, 'panel-type-switcher', ScenePanelTypeSwitcher); - - return panelTypeSwitcher.subscribeToState( - (newState: typeof panelTypeSwitcher.state, prevState?: typeof panelTypeSwitcher.state) => { - if (newState.panelType !== prevState?.panelType) { - (this.state.body as SceneByVariableRepeaterGrid | SceneLabelValuesGrid)?.renderGridItems(); - } - } - ); - } - - subscribeToFiltersChange() { - const filtersVariable = sceneGraph.findByKeyAndType(this, 'filters', FiltersVariable); - const noDataSwitcher = sceneGraph.findByKeyAndType(this, 'no-data-switcher', SceneNoDataSwitcher); - - // the handler will be called each time a filter is added/removed/modified - return filtersVariable.subscribeToState(() => { - if (noDataSwitcher.state.hideNoData === 'on') { - // we force render because the filters only influence the query made in each panel, not the list of items to render (which come from the groupBy options) - (this.state.body as SceneByVariableRepeaterGrid | SceneLabelValuesGrid)?.renderGridItems(true); - } - }); - } - - subscribeToPanelEvents() { - const selectLabelSub = this.subscribeToEvent(EventSelectLabel, (event) => { - this.selectLabel(event.payload.item); - }); - - const addToFiltersSub = this.subscribeToEvent(EventAddLabelToFilters, (event) => { - this.addLabelValueToFilters(event.payload.item); - }); - - const expandPanelSub = this.subscribeToEvent(EventExpandPanel, async (event) => { - this.openExpandedPanelDrawer(event.payload.item); - }); - - return { - unsubscribe() { - expandPanelSub.unsubscribe(); - addToFiltersSub.unsubscribe(); - selectLabelSub.unsubscribe(); - }, - }; - } - - selectLabel({ queryRunnerParams }: GridItemData) { - const labelValue = queryRunnerParams!.groupBy!.label; - const groupByVariable = sceneGraph.findByKeyAndType(this, 'groupBy', GroupByVariable); - - groupByVariable.changeValueTo(labelValue); - - // the event may be published from an expanded panel in the drawer - this.state.drawer.close(); - } - - addLabelValueToFilters(item: GridItemData) { - const filterByVariable = sceneGraph.findByKeyAndType(this, 'filters', FiltersVariable); - - let filterToAdd: AdHocVariableFilter; - const { filters, groupBy } = item.queryRunnerParams; - - if (filters?.[0]) { - filterToAdd = filters?.[0]; - } else if (groupBy?.values.length === 1) { - filterToAdd = { key: groupBy.label, operator: '=', value: groupBy.values[0] }; - } else { - const error = new Error('Cannot build filter! Missing "filters" and "groupBy" value.'); - console.error(error); - console.info(item); - throw error; - } - - addFilter(filterByVariable, filterToAdd); - - const goupByVariable = sceneGraph.findByKeyAndType(this, 'groupBy', GroupByVariable); - goupByVariable.changeValueTo(GroupByVariable.DEFAULT_VALUE); - } - - openExpandedPanelDrawer(item: GridItemData) { - this.state.drawer.open({ - title: this.buildtimeSeriesPanelTitle(item), - body: - item.panelType === PanelType.BARGAUGE - ? new SceneLabelValuesBarGauge({ - item, - headerActions: () => [new SelectAction({ EventClass: EventSelectLabel, item }), new FavAction({ item })], - }) - : new SceneLabelValuesTimeseries({ - displayAllValues: true, - item, - headerActions: () => [new SelectAction({ EventClass: EventSelectLabel, item }), new FavAction({ item })], - }), - }); - } - - buildtimeSeriesPanelTitle(item: GridItemData) { - const serviceName = getSceneVariableValue(this, 'serviceName'); - const profileMetricId = getSceneVariableValue(this, 'profileMetricId'); - return `${serviceName} · ${getProfileMetricLabel(profileMetricId)} · ${item.label}`; - } - - static Component = ({ model }: SceneComponentProps) => { - const styles = useStyles2(getStyles); - - const { body, drawer } = model.useState(); - - const groupByVariable = sceneGraph.findByKeyAndType(model, 'groupBy', GroupByVariable); - const { gridControls } = sceneGraph.findByKeyAndType(model, 'profiles-explorer', SceneProfilesExplorer).state; - - return ( -
- - -
- {gridControls.length ? ( - - {gridControls.map((control) => ( - - ))} - - ) : null} -
- - {body && } - {} -
- ); - }; -} - -const getStyles = (theme: GrafanaTheme2) => ({ - container: css` - margin-top: ${theme.spacing(1)}; - `, - sceneControls: css` - margin-bottom: ${theme.spacing(1)}; - `, -}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index 614f36ea..d2cfddeb 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -192,11 +192,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx deleted file mode 100644 index ea55bf95..00000000 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneLabelValuesGrid.tsx +++ /dev/null @@ -1,410 +0,0 @@ -import { DashboardCursorSync, LoadingState, VariableRefresh } from '@grafana/data'; -import { - behaviors, - EmbeddedSceneState, - QueryVariable, - SceneComponentProps, - SceneCSSGridItem, - SceneCSSGridLayout, - sceneGraph, - SceneObjectBase, - SceneQueryRunner, - VizPanelState, -} from '@grafana/scenes'; -import { Spinner } from '@grafana/ui'; -import { noOp } from '@shared/domain/noOp'; -import { debounce, isEqual } from 'lodash'; -import React from 'react'; - -import { getSceneVariableValue } from '../../../helpers/getSceneVariableValue'; -import { SceneEmptyState } from '../../SceneByVariableRepeaterGrid/components/SceneEmptyState/SceneEmptyState'; -import { SceneErrorState } from '../../SceneByVariableRepeaterGrid/components/SceneErrorState/SceneErrorState'; -import { LayoutType, SceneLayoutSwitcher } from '../../SceneByVariableRepeaterGrid/components/SceneLayoutSwitcher'; -import { SceneNoDataSwitcher } from '../../SceneByVariableRepeaterGrid/components/SceneNoDataSwitcher'; -import { PanelType, ScenePanelTypeSwitcher } from '../../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; -import { SceneQuickFilter } from '../../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; -import { sortFavGridItems } from '../../SceneByVariableRepeaterGrid/domain/sortFavGridItems'; -import { GridItemData } from '../../SceneByVariableRepeaterGrid/types/GridItemData'; -import { SceneLabelValuesBarGauge } from '../../SceneLabelValuesBarGauge'; -import { SceneLabelValueStat } from '../../SceneLabelValueStat'; -import { SceneLabelValuesTimeseries } from '../../SceneLabelValuesTimeseries'; - -interface SceneLabelValuesGridState extends EmbeddedSceneState { - items: GridItemData[]; - headerActions: (item: GridItemData, items: GridItemData[]) => VizPanelState['headerActions']; - sortItemsFn: (a: GridItemData, b: GridItemData) => number; - hideNoData: boolean; -} - -const GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(400px, 1fr))'; - -const GRID_TEMPLATE_ROWS = '1fr'; -const GRID_AUTO_ROWS = '240px'; -const GRID_AUTO_ROWS_SMALL = '76px'; - -export class SceneLabelValuesGrid extends SceneObjectBase { - static buildGridItemKey(item: GridItemData) { - return `grid-item-${item.index}-${item.value}`; - } - - static getGridColumnsTemplate(layout: LayoutType) { - return layout === LayoutType.ROWS ? GRID_TEMPLATE_ROWS : GRID_TEMPLATE_COLUMNS; - } - - constructor({ - key, - headerActions, - sortItemsFn, - }: { - key: string; - headerActions: SceneLabelValuesGridState['headerActions']; - sortItemsFn?: SceneLabelValuesGridState['sortItemsFn']; - }) { - super({ - key, - items: [], - headerActions, - sortItemsFn: sortItemsFn || sortFavGridItems, - hideNoData: false, - body: new SceneCSSGridLayout({ - templateColumns: SceneLabelValuesGrid.getGridColumnsTemplate(SceneLayoutSwitcher.DEFAULT_LAYOUT), - autoRows: GRID_AUTO_ROWS, - isLazy: true, - $behaviors: [ - new behaviors.CursorSync({ - key: 'metricCrosshairSync', - sync: DashboardCursorSync.Crosshair, - }), - ], - children: [], - }), - }); - - this.addActivationHandler(this.onActivate.bind(this)); - } - - // eslint-disable-next-line sonarjs/cognitive-complexity - onActivate() { - // here we try to emulate VariableDependencyConfig.onVariableUpdateCompleted - const variable = sceneGraph.lookupVariable('groupBy', this) as QueryVariable & { update: () => void }; - - const variableSub = variable.subscribeToState((newState, prevState) => { - if (!newState.loading && prevState.loading) { - this.renderGridItems(); - } - }); - - // the "groupBy" variable data source will not fetch values if the variable is inactive - // (see src/pages/ProfilesExplorerView/data/labels/LabelsDataSource.ts) - // so we force an update here to be sure we have the latest values - variable.update(); - - const refreshSub = this.subscribeToRefreshClick(); - const quickFilterSub = this.subscribeToQuickFilterChange(); - const layoutChangeSub = this.subscribeToLayoutChange(); - const hideNoDataSub = this.subscribeToHideNoDataChange(); - - return () => { - hideNoDataSub.unsubscribe(); - layoutChangeSub.unsubscribe(); - quickFilterSub.unsubscribe(); - refreshSub.unsubscribe(); - - variableSub.unsubscribe(); - }; - } - - subscribeToRefreshClick() { - const variable = sceneGraph.lookupVariable('groupBy', this) as QueryVariable & { update: () => void }; - const originalRefresh = variable.state.refresh; - - variable.setState({ refresh: VariableRefresh.never }); - - const onClickRefresh = () => { - variable.update(); - }; - - // start of hack, for a better UX: we disable the variable "refresh" option and we allow the user to reload the list only by clicking on the "Refresh" button - // if we don't do this, every time the time range changes (even with auto-refresh on), - // all the timeseries present on the screen would be re-created, resulting in blinking and a poor UX - const refreshButton = document.querySelector( - '[data-testid="data-testid RefreshPicker run button"]' - ) as HTMLButtonElement; - - if (!refreshButton) { - console.error('SceneLabelValuesGrid: Refresh button not found! The list of items will never be updated.'); - } - - refreshButton?.addEventListener('click', onClickRefresh); - refreshButton?.setAttribute('title', 'Click to completely refresh all the panels present on the screen'); - // end of hack - - return { - unsubscribe() { - refreshButton?.removeAttribute('title'); - refreshButton?.removeEventListener('click', onClickRefresh); - variable.setState({ refresh: originalRefresh }); - }, - }; - } - - subscribeToQuickFilterChange() { - const quickFilter = sceneGraph.findByKeyAndType(this, 'quick-filter', SceneQuickFilter); - - const onChangeState = (newState: typeof quickFilter.state, prevState?: typeof quickFilter.state) => { - if (newState.searchText !== prevState?.searchText) { - this.renderGridItems(); - } - }; - - return quickFilter.subscribeToState(debounce(onChangeState, 250)); - } - - subscribeToLayoutChange() { - const layoutSwitcher = sceneGraph.findByKeyAndType(this, 'layout-switcher', SceneLayoutSwitcher); - - const body = this.state.body as SceneCSSGridLayout; - - const onChangeState = (newState: typeof layoutSwitcher.state, prevState?: typeof layoutSwitcher.state) => { - if (newState.layout !== prevState?.layout) { - body.setState({ - templateColumns: SceneLabelValuesGrid.getGridColumnsTemplate(newState.layout), - }); - } - }; - - onChangeState(layoutSwitcher.state); - - return layoutSwitcher.subscribeToState(onChangeState); - } - - subscribeToHideNoDataChange() { - const noDataSwitcher = sceneGraph.findByKeyAndType(this, 'no-data-switcher', SceneNoDataSwitcher); - - if (!noDataSwitcher.isActive) { - this.setState({ hideNoData: false }); - - return { - unsubscribe: noOp, - }; - } - - const onChangeState = (newState: typeof noDataSwitcher.state, prevState?: typeof noDataSwitcher.state) => { - if (newState.hideNoData !== prevState?.hideNoData) { - this.setState({ hideNoData: newState.hideNoData === 'on' }); - - // we force render because this.state.items certainly have not changed but we want to update the UI panels anyway - this.renderGridItems(true); - } - }; - - onChangeState(noDataSwitcher.state); - - return noDataSwitcher.subscribeToState(onChangeState); - } - - buildItemsData(variable: QueryVariable) { - const { value: variableValue, options } = variable.state; - - const currentOption = options - .filter((o) => o.value !== 'all') - .find((o) => variableValue === JSON.parse(o.value as string).value); - - if (!currentOption) { - console.error('Cannot find the "%s" groupBy value among all the variable option!', variableValue); - return []; - } - - const serviceName = getSceneVariableValue(this, 'serviceName'); - const profileMetricId = getSceneVariableValue(this, 'profileMetricId'); - const panelTypeFromSwitcher = sceneGraph.findByKeyAndType(this, 'panel-type-switcher', ScenePanelTypeSwitcher).state - .panelType; - - const panelType = panelTypeFromSwitcher === PanelType.BARGAUGE ? PanelType.STATS : panelTypeFromSwitcher; - - const items = JSON.parse(currentOption.value as string).groupBy.values.map((value: string, index: number) => { - return { - index, - value: value, - label: value, - queryRunnerParams: { - serviceName, - profileMetricId, - filters: [{ key: variableValue, operator: '=', value }], - }, - panelType, - }; - }); - - return this.filterItems(items).sort(this.state.sortItemsFn); - } - - shouldRenderItems(newItems: SceneLabelValuesGridState['items']) { - const { items } = this.state; - - if (!newItems.length || items.length !== newItems.length) { - return true; - } - - return !isEqual(items, newItems); - } - - renderGridItems(forceRender = false) { - const variable = sceneGraph.lookupVariable('groupBy', this) as QueryVariable; - - if (variable.state.loading) { - return; - } - - if (variable.state.error) { - this.renderErrorState(variable.state.error); - return; - } - - const newItems = this.buildItemsData(variable); - - if (!forceRender && !this.shouldRenderItems(newItems)) { - return; - } - - this.setState({ items: newItems }); - - if (!this.state.items.length) { - this.renderEmptyState(); - return; - } - - const gridItems = this.state.items.map((item) => { - const vizPanel = this.buildVizPanel(item); - - if (this.state.hideNoData) { - this.setupHideNoData(vizPanel); - } - - return new SceneCSSGridItem({ - key: SceneLabelValuesGrid.buildGridItemKey(item), - body: vizPanel, - }); - }); - - (this.state.body as SceneCSSGridLayout).setState({ - autoRows: this.getAutoRows(), // required to have the correct grid items height - children: gridItems, - }); - } - - buildVizPanel(item: GridItemData) { - switch (item.panelType) { - case PanelType.BARGAUGE: - return new SceneLabelValuesBarGauge({ - item, - headerActions: this.state.headerActions.bind(null, item, this.state.items), - }); - - case PanelType.STATS: - return new SceneLabelValueStat({ - item, - headerActions: this.state.headerActions.bind(null, item, this.state.items), - }); - - case PanelType.TIMESERIES: - default: - return new SceneLabelValuesTimeseries({ - item, - headerActions: this.state.headerActions.bind(null, item, this.state.items), - }); - } - } - - setupHideNoData(vizPanel: SceneLabelValuesTimeseries | SceneLabelValuesBarGauge | SceneLabelValueStat) { - const sub = (vizPanel.state.body.state.$data as SceneQueryRunner)!.subscribeToState((state) => { - if (state.data?.state !== LoadingState.Done || state.data.series.length > 0) { - return; - } - - const gridItem = sceneGraph.getAncestor(vizPanel, SceneCSSGridItem); - const { key: gridItemKey } = gridItem.state; - const grid = sceneGraph.getAncestor(gridItem, SceneCSSGridLayout); - - const filteredChildren = grid.state.children.filter((c) => c.state.key !== gridItemKey); - - if (!filteredChildren.length) { - this.renderEmptyState(); - } else { - grid.setState({ children: filteredChildren }); - } - }); - - vizPanel.addActivationHandler(() => { - return () => { - sub.unsubscribe(); - }; - }); - } - - getAutoRows() { - const { panelType } = sceneGraph.findByKeyAndType(this, 'panel-type-switcher', ScenePanelTypeSwitcher).state; - return panelType === PanelType.BARGAUGE ? GRID_AUTO_ROWS_SMALL : GRID_AUTO_ROWS; - } - - filterItems(items: SceneLabelValuesGridState['items']) { - const quickFilterScene = sceneGraph.findByKeyAndType(this, 'quick-filter', SceneQuickFilter); - const { searchText } = quickFilterScene.state; - - if (!searchText) { - return items; - } - - const regexes = searchText - .split(',') - .map((t) => t.trim()) - .filter(Boolean) - .map((r) => { - try { - return new RegExp(r); - } catch { - return null; - } - }) - .filter(Boolean) as RegExp[]; - - return items.filter(({ label }) => regexes.some((r) => r.test(label))); - } - - renderEmptyState() { - const body = this.state.body as SceneCSSGridLayout; - - body.setState({ - autoRows: '480px', - children: [ - new SceneCSSGridItem({ - body: new SceneEmptyState({ - message: 'No results', - }), - }), - ], - }); - } - - renderErrorState(error: Error) { - const body = this.state.body as SceneCSSGridLayout; - - body.setState({ - autoRows: '480px', - children: [ - new SceneCSSGridItem({ - body: new SceneErrorState({ - message: error.toString(), - }), - }), - ], - }); - } - - static Component({ model }: SceneComponentProps) { - const { body } = model.useState(); - const { loading } = (sceneGraph.lookupVariable('groupBy', model) as QueryVariable)?.useState(); - - return loading ? : ; - } -} From cc8e064230577cb483a1d05fe346310b7fe8f084 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 13 Aug 2024 13:17:37 +0200 Subject: [PATCH 37/76] refactor(SceneLabelValuePanel): Early return --- .../components/SceneLabelValuePanel.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx index 1abc0671..dc4b098b 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx @@ -42,12 +42,18 @@ export class SceneLabelValuePanel extends SceneObjectBase { const [s] = event.payload.series; - const allValuesSum = s ? getSeriesStatsValue(s, 'allValuesSum') || 0 : 0; + + if (!s) { + statsPanel.updateStats({ allValuesSum: 0, unit: 'short' }); + return; + } + + const allValuesSum = getSeriesStatsValue(s, 'allValuesSum') || 0; if (statsPanel.getStats()?.allValuesSum !== allValuesSum) { statsPanel.updateStats({ allValuesSum, - unit: s ? (s.fields[1].config.unit as string) : 'short', + unit: s.fields[1].config.unit || 'short', }); } }); From 5ae11150e4d7e91e7b4ff379f3227c98071ecfdf Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 13 Aug 2024 13:26:54 +0200 Subject: [PATCH 38/76] chore: Remove comment --- .../components/SceneProfilesExplorer/SceneProfilesExplorer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 02533687..298b3123 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -118,7 +118,6 @@ export class SceneProfilesExplorer extends SceneObjectBase Date: Tue, 13 Aug 2024 13:31:16 +0200 Subject: [PATCH 39/76] feat(SceneVariableName): Reset filters when a different service is selected --- .../domain/variables/ServiceNameVariable.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/ProfilesExplorerView/domain/variables/ServiceNameVariable.tsx b/src/pages/ProfilesExplorerView/domain/variables/ServiceNameVariable.tsx index e9db73fd..f24c1216 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/ServiceNameVariable.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/ServiceNameVariable.tsx @@ -73,10 +73,12 @@ export class ServiceNameVariable extends QueryVariable { this.changeValueTo(newValue); - // manually reset filters - the "Scenes way" would be to listen to the variable changes but it leads to errors - // see comments in src/pages/ProfilesExplorerView/variables/FiltersVariable/FiltersVariable.tsx - const filtersVariable = sceneGraph.findByKeyAndType(this, 'filters', FiltersVariable); - filtersVariable.setState({ filters: [] }); + // manually reset filters - we should listen to the variables changes but it leads to unwanted behaviour + // (filters set in the URL search parameters are resetted when the user lands on the page) + ['filters', 'filtersBaseline', 'filtersComparison'].forEach((filterKey) => { + const filtersVariable = sceneGraph.findByKeyAndType(this, filterKey, FiltersVariable); + filtersVariable.setState({ filters: [] }); + }); }; static Component = ({ model }: SceneComponentProps) => { From a4764f8a3ce8c13ec732c6eb03806ec61fa9b6e2 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 13 Aug 2024 15:17:41 +0200 Subject: [PATCH 40/76] chore(SwitchTimeRangeSelectionModeAction): Better tooltip --- .../domain/actions/SwitchTimeRangeSelectionModeAction.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx index 1e1d905d..490f3ebd 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx @@ -44,14 +44,14 @@ export class SwitchTimeRangeSelectionModeAction extends SceneObjectBase -
Change the behaviour when selecting a time range on the panel:
+
Use these buttons to change the behaviour when selecting a time range on the panel:
Time picker
Time range zoom in (default behaviour)
Flame graph
Time range for building the flame graph (the stack traces will be retrieved only for the selected - area) + range)
From 3861133c309ce076620b544e93ad508378253bb6 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 13 Aug 2024 15:31:59 +0200 Subject: [PATCH 41/76] chore(*): Small TS improvements --- .../SceneExploreServiceFlameGraph/SceneFlameGraph.tsx | 6 ++++-- .../SceneProfilesExplorer/SceneProfilesExplorer.tsx | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneFlameGraph.tsx index 4565909a..8f2b7719 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneFlameGraph.tsx @@ -22,7 +22,9 @@ import { getSceneVariableValue } from '../../helpers/getSceneVariableValue'; import { buildFlameGraphQueryRunner } from '../../infrastructure/flame-graph/buildFlameGraphQueryRunner'; import { PYROSCOPE_DATA_SOURCE } from '../../infrastructure/pyroscope-data-sources'; -interface SceneFlameGraphState extends SceneObjectState {} +interface SceneFlameGraphState extends SceneObjectState { + $data: SceneQueryRunner; +} // I've tried to use a SplitLayout for the body without any success (left: flame graph, right: explain flame graph content) // without success: the flame graph dimensions are set in runtime and do not change when the user resizes the layout @@ -87,7 +89,7 @@ export class SceneFlameGraph extends SceneObjectBase { } }, [maxNodes]); - const $dataState = $data!.useState(); + const $dataState = $data.useState(); const isFetchingProfileData = $dataState?.data?.state === LoadingState.Loading; const profileData = $dataState?.data?.series?.[0]; const hasProfileData = Number(profileData?.length) > 1; diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 9f2ae067..c3e8723c 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -47,9 +47,10 @@ import { SceneExploreServiceFlameGraph } from '../SceneExploreServiceFlameGraph/ import { ExplorationTypeSelector } from './ui/ExplorationTypeSelector'; export interface SceneProfilesExplorerState extends Partial { + $variables: SceneVariableSet; + gridControls: Array; explorationType?: ExplorationType; body?: SplitLayout; - gridControls: Array; } export enum ExplorationType { @@ -301,10 +302,10 @@ export class SceneProfilesExplorer extends SceneObjectBase { - const { explorationType, controls, body } = this.useState(); + const { explorationType, controls, body, $variables } = this.useState(); const [timePickerControl, refreshPickerControl] = controls as [SceneObject, SceneObject]; - const dataSourceVariable = this.state.$variables!.state!.variables[0] as ProfilesDataSourceVariable; + const dataSourceVariable = $variables.state.variables[0] as ProfilesDataSourceVariable; const { variables: sceneVariables, gridControls } = (body?.state.primary as any).getVariablesAndGridControls() as { variables: SceneVariable[]; From 0053dc56048f33ac8e0969e34968fff052db4b85 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 16 Aug 2024 18:18:41 +0200 Subject: [PATCH 42/76] feat(*): Fetch diff profile only when both annotations exist --- .../SceneExploreDiffFlameGraphs.tsx | 152 +++++++++++++----- .../SceneComparePanel/SceneComparePanel.tsx | 99 ++++-------- .../SceneTimeRangeWithAnnotations.ts | 80 --------- .../SceneTimeRangeWithAnnotations.ts | 152 ++++++++++++++++++ .../events/EventAnnotationTimeRangeChanged.ts | 9 ++ .../EventSwitchTimerangeSelectionMode.ts | 4 +- .../domain/getDefaultTimeRange.ts | 16 ++ .../infrastructure/diffProfileApiClient.ts | 44 +++++ .../infrastructure/useFetchDiffProfile.ts | 75 +++++++++ .../domain/useBuildPyroscopeQuery.ts | 23 +++ .../FiltersVariable/FiltersVariable.tsx | 70 ++++---- .../variables/FiltersVariable/filters-ops.tsx | 11 -- 12 files changed, 496 insertions(+), 239 deletions(-) delete mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/getDefaultTimeRange.ts create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/diffProfileApiClient.ts create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/useFetchDiffProfile.ts create mode 100644 src/pages/ProfilesExplorerView/domain/useBuildPyroscopeQuery.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx index 49122120..54e2b83c 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx @@ -1,21 +1,24 @@ import { css } from '@emotion/css'; -import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data'; +import { DashboardCursorSync, GrafanaTheme2, TimeRange } from '@grafana/data'; import { behaviors, SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { Spinner, useStyles2 } from '@grafana/ui'; import { AiPanel } from '@shared/components/AiPanel/AiPanel'; import { AIButton } from '@shared/components/AiPanel/components/AIButton'; import { FlameGraph } from '@shared/components/FlameGraph/FlameGraph'; +import { displayWarning } from '@shared/domain/displayStatus'; import { useToggleSidePanel } from '@shared/domain/useToggleSidePanel'; import { useFetchPluginSettings } from '@shared/infrastructure/settings/useFetchPluginSettings'; import { FlamebearerProfile } from '@shared/types/FlamebearerProfile'; +import { InlineBanner } from '@shared/ui/InlineBanner'; import React, { useEffect } from 'react'; -import { useFetchDiffProfile } from '../../../../pages/ComparisonView/components/FlameGraphContainer/infrastructure/useFetchDiffProfile'; -import { useDefaultComparisonParamsFromUrl } from '../../../../pages/ComparisonView/domain/useDefaultComparisonParamsFromUrl'; +import { useBuildPyroscopeQuery } from '../../domain/useBuildPyroscopeQuery'; import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; import { CompareTarget } from '../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; +import { EventAnnotationTimeRangeChanged } from './components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged'; import { SceneComparePanel } from './components/SceneComparePanel/SceneComparePanel'; +import { useFetchDiffProfile } from './infrastructure/useFetchDiffProfile'; interface SceneExploreDiffFlameGraphsState extends SceneObjectState { baselinePanel: SceneComparePanel; @@ -33,6 +36,7 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase { + eventSub.unsubscribe(); + profileMetricVariable.setState({ query: ProfileMetricVariable.QUERY_DEFAULT }); profileMetricVariable.update(true); }; @@ -66,28 +74,88 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase) { - const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks + subscribeToEvents() { + // we use the EventAnnotationTimeRangeChanged instead of React hooks to get the annotation time ranges + // because the timeseries are not directly built (see SceneComparePanel) and the values of the annotation time ranges + // are not determined directly neither (see SceneTimeRangeWithAnnotations) + // TODO: we really need a native Scenes diff flame graph panel + return this.subscribeToEvent(EventAnnotationTimeRangeChanged, () => { + this.forceRender(); + }); + } + + useSceneExploreDiffFlameGraphs = () => { + const { baselinePanel, comparisonPanel } = this.useState(); + + const baselineTimeRange = baselinePanel.getDiffTimeRange()?.state.annotationTimeRange as TimeRange; + const baselineQuery = useBuildPyroscopeQuery(this, 'filtersBaseline'); + + const comparisonTimeRange = comparisonPanel.getDiffTimeRange()?.state.annotationTimeRange as TimeRange; + const comparisonQuery = useBuildPyroscopeQuery(this, 'filtersComparison'); + + const { + isFetching, + error: fetchProfileError, + profile, + } = useFetchDiffProfile({ + baselineTimeRange, + baselineQuery, + comparisonTimeRange, + comparisonQuery, + }); - const { baselinePanel, comparisonPanel } = model.useState(); + const noProfileDataAvailable = !fetchProfileError && (!profile || profile?.flamebearer.numTicks === 0); + const shouldDisplayFlamegraph = Boolean(!fetchProfileError && !noProfileDataAvailable && profile); - useDefaultComparisonParamsFromUrl(); // eslint-disable-line react-hooks/rules-of-hooks - const { isFetching, error, profile } = useFetchDiffProfile({}); // eslint-disable-line react-hooks/rules-of-hooks - const noProfileDataAvailable = !error && profile?.flamebearer.numTicks === 0; - const shouldDisplayFlamegraph = Boolean(!error && !noProfileDataAvailable && profile); + const { settings, error: fetchSettingsError } = useFetchPluginSettings(); - const { settings /*, error: isFetchingSettingsError*/ } = useFetchPluginSettings(); // eslint-disable-line react-hooks/rules-of-hooks + return { + data: { + baselinePanel, + comparisonPanel, + isLoading: isFetching, + fetchProfileError, + noProfileDataAvailable, + shouldDisplayFlamegraph, + profile, + settings, + fetchSettingsError, + }, + actions: {}, + }; + }; + + static Component({ model }: SceneComponentProps) { + const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks + + const { data } = model.useSceneExploreDiffFlameGraphs(); + const { + baselinePanel, + comparisonPanel, + isLoading, + fetchProfileError, + noProfileDataAvailable, + shouldDisplayFlamegraph, + profile, + fetchSettingsError, + settings, + } = data; const sidePanel = useToggleSidePanel(); // eslint-disable-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { - if (isFetching) { + if (isLoading) { sidePanel.close(); } - }, [isFetching, sidePanel]); + }, [isLoading, sidePanel]); - // console.log('*** Component', left, right, isFetching, error, profile); + if (fetchSettingsError) { + displayWarning([ + 'Error while retrieving the plugin settings!', + 'Some features might not work as expected (e.g. flamegraph export options). Please try to reload the page, sorry for the inconvenience.', + ]); + } return (
@@ -98,29 +166,41 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase
- {isFetching && } + {fetchProfileError && ( + + )} + + {noProfileDataAvailable && ( + + )} + +
+ {isLoading && } + + {shouldDisplayFlamegraph && ( + { + sidePanel.open('ai'); + }} + disabled={isLoading || noProfileDataAvailable || sidePanel.isOpen('ai')} + interactionName="g_pyroscope_app_explain_flamegraph_clicked" + > + Explain Flame Graph + + )} +
{shouldDisplayFlamegraph && ( - <> -
- { - sidePanel.open('ai'); - }} - disabled={isFetching || noProfileDataAvailable || sidePanel.isOpen('ai')} - interactionName="g_pyroscope_app_explain_flamegraph_clicked" - > - Explain Flame Graph - -
- - - + )}
@@ -160,7 +240,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ `, flameGraphHeaderActions: css` display: flex; - align-items: flex-end; + align-items: flex-start; & > button { margin-left: auto; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index 10fa7b41..1933185f 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { dateTime, FieldMatcherID, getValueFormat, GrafanaTheme2, PanelData, TimeRange } from '@grafana/data'; +import { FieldMatcherID, getValueFormat, GrafanaTheme2 } from '@grafana/data'; import { SceneComponentProps, SceneDataTransformer, @@ -9,7 +9,6 @@ import { SceneRefreshPicker, SceneTimePicker, SceneTimeRange, - SceneTimeRangeState, VariableDependencyConfig, } from '@grafana/scenes'; import { InlineLabel, useStyles2 } from '@grafana/ui'; @@ -24,41 +23,29 @@ import { getProfileMetricLabel } from '../../../../infrastructure/series/helpers import { addRefId, addStats } from '../../../SceneByVariableRepeaterGrid/infrastructure/data-transformations'; import { CompareTarget } from '../../../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries'; +import { + SceneTimeRangeWithAnnotations, + TimeRangeWithAnnotationsMode, +} from './components/SceneTimeRangeWithAnnotations'; import { SwitchTimeRangeSelectionModeAction, TimerangeSelectionMode, } from './domain/actions/SwitchTimeRangeSelectionModeAction'; import { EventSwitchTimerangeSelectionMode } from './domain/events/EventSwitchTimerangeSelectionMode'; -import { RangeAnnotation } from './domain/RangeAnnotation'; +import { getDefaultTimeRange } from './domain/getDefaultTimeRange'; import { buildCompareTimeSeriesQueryRunner } from './infrastructure/buildCompareTimeSeriesQueryRunner'; -import { SceneTimeRangeWithAnnotations, TimeRangeWithAnnotationsMode } from './SceneTimeRangeWithAnnotations'; export interface SceneComparePanelState extends SceneObjectState { target: CompareTarget; title: string; filterKey: 'filtersBaseline' | 'filtersComparison'; color: string; - annotationColor: string; timePicker: SceneTimePicker; refreshPicker: SceneRefreshPicker; timeseries?: SceneLabelValuesTimeseries; $timeRange: SceneTimeRange; } -const getDefaultTimeRange = (): SceneTimeRangeState => { - const now = dateTime(); - - return { - from: 'now-5m', - to: 'now', - value: { - from: dateTime(now).subtract(5, 'minutes'), - to: now, - raw: { from: 'now-5m', to: 'now' }, - }, - }; -}; - export class SceneComparePanel extends SceneObjectBase { protected _variableDependency = new VariableDependencyConfig(this, { variableNames: ['profileMetricId'], @@ -74,8 +61,6 @@ export class SceneComparePanel extends SceneObjectBase { title: target === CompareTarget.BASELINE ? 'Baseline' : 'Comparison', filterKey: target === CompareTarget.BASELINE ? 'filtersBaseline' : 'filtersComparison', color: target === CompareTarget.BASELINE ? BASELINE_COLORS.COLOR.toString() : COMPARISON_COLORS.COLOR.toString(), - annotationColor: - target === CompareTarget.BASELINE ? BASELINE_COLORS.OVERLAY.toString() : COMPARISON_COLORS.OVERLAY.toString(), $timeRange: new SceneTimeRange(getDefaultTimeRange()), timePicker: new SceneTimePicker({ isOnCanvas: true }), refreshPicker: new SceneRefreshPicker({ isOnCanvas: true }), @@ -86,61 +71,36 @@ export class SceneComparePanel extends SceneObjectBase { } onActivate() { - const { title, annotationColor } = this.state; - - this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { - this.switchSelectionMode(event.payload); - }); + const { title, target } = this.state; const timeseries = this.buildTimeSeries(); - function updateAnnotation(timeRange: TimeRange, data?: PanelData) { - if (!data) { - return; - } - - const annotation = new RangeAnnotation(); - - annotation.addRange({ - text: `${title} time range`, - color: annotationColor, - time: timeRange.from.unix() * 1000, - timeEnd: timeRange.to.unix() * 1000, - }); - - $data?.setState({ - data: { - ...data, - annotations: [annotation], - }, - }); - } - - timeseries.setState({ + timeseries.state.body.setState({ $timeRange: new SceneTimeRangeWithAnnotations({ - onTimeRangeChange(timeRange) { - updateAnnotation(timeRange, timeseries.state.body.state.$data?.state.data); - }, + mode: TimeRangeWithAnnotationsMode.ANNOTATIONS, + annotationColor: + target === CompareTarget.BASELINE ? BASELINE_COLORS.OVERLAY.toString() : COMPARISON_COLORS.OVERLAY.toString(), + annotationTitle: `${title} time range`, }), }); this.setState({ timeseries }); - const { $data } = timeseries.state.body.state; + const eventSub = this.subscribeToEvents(); - $data?.subscribeToState((newState, prevState) => { - if (!newState.data) { - return; - } - - if (!newState.data?.annotations?.length && !prevState.data?.annotations?.length) { - // updateAnnotation(this.state.$timeRange.state.value, newState.data); - return; - } + return () => { + eventSub.unsubscribe(); + }; + } - if (!newState.data?.annotations?.length && prevState.data?.annotations?.length) { - newState.data.annotations = prevState.data.annotations; - } + subscribeToEvents() { + return this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { + (this.state.timeseries?.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).setState({ + mode: + event.payload.mode === TimerangeSelectionMode.FLAMEGRAPH + ? TimeRangeWithAnnotationsMode.ANNOTATIONS + : TimeRangeWithAnnotationsMode.DEFAULT, + }); }); } @@ -189,13 +149,8 @@ export class SceneComparePanel extends SceneObjectBase { return description || getProfileMetricLabel(profileMetricId); } - switchSelectionMode({ mode }: { mode: TimerangeSelectionMode }) { - const newMode = - mode === TimerangeSelectionMode.FLAMEGRAPH - ? TimeRangeWithAnnotationsMode.ANNOTATIONS - : TimeRangeWithAnnotationsMode.DEFAULT; - - (this.state.timeseries?.state.$timeRange as SceneTimeRangeWithAnnotations).changeMode(newMode); + getDiffTimeRange() { + return this.state.timeseries?.state.body.state.$timeRange as SceneTimeRangeWithAnnotations; } public static Component = ({ model }: SceneComponentProps) => { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts deleted file mode 100644 index 258e9edb..00000000 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneTimeRangeWithAnnotations.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { getDefaultTimeRange, TimeRange } from '@grafana/data'; -import { sceneGraph, SceneObjectBase, SceneTimeRangeLike, SceneTimeRangeState } from '@grafana/scenes'; - -export enum TimeRangeWithAnnotationsMode { - ANNOTATIONS = 'annotations', - DEFAULT = 'default', -} - -interface SceneTimeRangeWithAnnotationsState extends SceneTimeRangeState { - alternateTimeRange: TimeRange; - onTimeRangeChange?: (timeRange: TimeRange) => void; - mode: TimeRangeWithAnnotationsMode; -} - -export class SceneTimeRangeWithAnnotations - extends SceneObjectBase - implements SceneTimeRangeLike -{ - constructor( - state: Omit< - SceneTimeRangeWithAnnotationsState, - 'mode' | 'from' | 'to' | 'value' | 'timeZone' | 'alternateTimeRange' - > = {} - ) { - super({ - ...state, - // We set a default time range here. It will be overwritten on activation based on ancestor time range. - from: 'now-6h', - to: 'now', - value: getDefaultTimeRange(), - alternateTimeRange: getDefaultTimeRange(), - mode: TimeRangeWithAnnotationsMode.ANNOTATIONS, - }); - - this.addActivationHandler(() => { - const timeRange = this.realTimeRange; - - this.setState({ - ...timeRange.state, - alternateTimeRange: timeRange.state.value, - }); - - this._subs.add(timeRange.subscribeToState((newState) => this.setState(newState))); - }); - } - - changeMode(newMode: TimeRangeWithAnnotationsMode) { - this.setState({ mode: newMode }); - } - - private get realTimeRange() { - const parentsceneObject = this.parent; - if (!parentsceneObject?.parent) { - throw Error('A time range change override will not function if it is on a scene with no parent.'); - } - return sceneGraph.getTimeRange(parentsceneObject?.parent); - } - - onTimeRangeChange(timeRange: TimeRange): void { - if (this.state.mode === TimeRangeWithAnnotationsMode.DEFAULT) { - this.realTimeRange.onTimeRangeChange(timeRange); - return; - } - - this.setState({ alternateTimeRange: timeRange }); - this.state.onTimeRangeChange?.(timeRange); - } - - onTimeZoneChange(timeZone: string): void { - this.realTimeRange.onTimeZoneChange(timeZone); - } - - getTimeZone(): string { - return this.realTimeRange.getTimeZone(); - } - - onRefresh(): void { - this.realTimeRange.onRefresh(); - } -} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts new file mode 100644 index 00000000..7f468aed --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -0,0 +1,152 @@ +import { dateTime, TimeRange } from '@grafana/data'; +import { sceneGraph, SceneObjectBase, SceneTimeRangeLike, SceneTimeRangeState, VizPanel } from '@grafana/scenes'; + +import { EventAnnotationTimeRangeChanged } from '../domain/events/EventAnnotationTimeRangeChanged'; +import { getDefaultTimeRange } from '../domain/getDefaultTimeRange'; +import { RangeAnnotation } from '../domain/RangeAnnotation'; + +export enum TimeRangeWithAnnotationsMode { + ANNOTATIONS = 'annotations', + DEFAULT = 'default', +} + +interface SceneTimeRangeWithAnnotationsState extends SceneTimeRangeState { + annotationTimeRange: TimeRange; + annotationColor: string; + annotationTitle: string; + mode: TimeRangeWithAnnotationsMode; +} + +export class SceneTimeRangeWithAnnotations + extends SceneObjectBase + implements SceneTimeRangeLike +{ + constructor({ + annotationColor, + annotationTitle, + mode, + }: { + annotationColor: SceneTimeRangeWithAnnotationsState['annotationColor']; + annotationTitle: SceneTimeRangeWithAnnotationsState['annotationTitle']; + mode: SceneTimeRangeWithAnnotationsState['mode']; + }) { + const defaultTimeRange = getDefaultTimeRange(); + + super({ + // temporary values, they will be updated in onActivate + ...defaultTimeRange, + annotationTimeRange: { + from: dateTime(0), + to: dateTime(0), + raw: { from: dateTime(0), to: dateTime(0) }, + }, + annotationColor, + annotationTitle, + mode, + }); + + this.addActivationHandler(this.onActivate.bind(this)); + } + + onActivate() { + const ancestorTimeRangeObject = this.getAncestorTimeRange(); + + this.setState({ + ...ancestorTimeRangeObject.state, + // TODO + // annotationTimeRange: ancestorTimeRangeObject.state.value, + }); + + this._subs.add(ancestorTimeRangeObject.subscribeToState((newState) => this.setState(newState))); + + const { $data } = this.getTimeseriesPanel().state; + + this._subs.add( + $data?.subscribeToState((newState, prevState) => { + if (newState.data && !newState.data.annotations?.length && prevState.data?.annotations?.length) { + newState.data.annotations = prevState.data.annotations; + } + }) + ); + } + + protected getAncestorTimeRange(): SceneTimeRangeLike { + if (!this.parent || !this.parent.parent) { + throw new Error(typeof this + ' must be used within $timeRange scope'); + } + + return sceneGraph.getTimeRange(this.parent.parent); + } + + protected getTimeseriesPanel(): VizPanel { + try { + const vizPanel = sceneGraph.getAncestor(this, VizPanel); + + if (vizPanel.state.pluginId !== 'timeseries') { + throw new TypeError('Incorrect VizPanel type!'); + } + + return vizPanel; + } catch (error) { + throw new Error('Ancestor timeseries panel not found!'); + } + } + + protected updateTimeseriesAnnotation() { + const { annotationTimeRange, annotationColor, annotationTitle } = this.state; + + const { $data } = this.getTimeseriesPanel().state; + + const data = $data?.state.data; + if (!data || !annotationTimeRange) { + return; + } + + const annotation = new RangeAnnotation(); + + annotation.addRange({ + color: annotationColor, + text: annotationTitle, + time: annotationTimeRange.from.unix() * 1000, + timeEnd: annotationTimeRange.to.unix() * 1000, + }); + + $data?.setState({ + data: { + ...data, + annotations: [annotation], + }, + }); + } + + onTimeRangeChange(timeRange: TimeRange): void { + const { mode, annotationTimeRange } = this.state; + + if (mode === TimeRangeWithAnnotationsMode.DEFAULT) { + this.getAncestorTimeRange().onTimeRangeChange(timeRange); + return; + } + + // we don't do this.setState({ annotationTimeRange: timeRange }); + // because it would cause a ttimeseries query to be made to the API + annotationTimeRange.from = timeRange.from; + annotationTimeRange.to = timeRange.to; + annotationTimeRange.raw = timeRange.raw; + + this.publishEvent(new EventAnnotationTimeRangeChanged({ timeRange }), true); + + this.updateTimeseriesAnnotation(); + } + + onTimeZoneChange(timeZone: string): void { + this.getAncestorTimeRange().onTimeZoneChange(timeZone); + } + + getTimeZone(): string { + return this.getAncestorTimeRange().getTimeZone(); + } + + onRefresh(): void { + this.getAncestorTimeRange().onRefresh(); + } +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts new file mode 100644 index 00000000..7d26ef7d --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts @@ -0,0 +1,9 @@ +import { BusEventWithPayload, TimeRange } from '@grafana/data'; + +export interface EventAnnotationTimeRangeChangedPayload { + timeRange: TimeRange; +} + +export class EventAnnotationTimeRangeChanged extends BusEventWithPayload { + public static type = 'annotation-timerange-changed'; +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts index 8c391091..f1780be3 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts @@ -2,10 +2,10 @@ import { BusEventWithPayload } from '@grafana/data'; import { TimerangeSelectionMode } from '../actions/SwitchTimeRangeSelectionModeAction'; -export interface EventSwitchTimerangeSelectionTypePayload { +export interface EventSwitchTimerangeSelectionModePayload { mode: TimerangeSelectionMode; } -export class EventSwitchTimerangeSelectionMode extends BusEventWithPayload { +export class EventSwitchTimerangeSelectionMode extends BusEventWithPayload { public static type = 'switch-timerange-selection-mode'; } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/getDefaultTimeRange.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/getDefaultTimeRange.ts new file mode 100644 index 00000000..7367dc61 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/getDefaultTimeRange.ts @@ -0,0 +1,16 @@ +import { dateTime } from '@grafana/data'; +import { SceneTimeRangeState } from '@grafana/scenes'; + +export function getDefaultTimeRange(): SceneTimeRangeState { + const now = dateTime(); + + return { + from: 'now-30m', + to: 'now', + value: { + from: dateTime(now).subtract(30, 'minutes'), + to: now, + raw: { from: 'now-30m', to: 'now' }, + }, + }; +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/diffProfileApiClient.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/diffProfileApiClient.ts new file mode 100644 index 00000000..7a0c6611 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/diffProfileApiClient.ts @@ -0,0 +1,44 @@ +import { dateTimeParse, TimeRange } from '@grafana/data'; +import { ApiClient } from '@shared/infrastructure/http/ApiClient'; +import { FlamebearerProfile } from '@shared/types/FlamebearerProfile'; + +type DiffProfileResponse = FlamebearerProfile; + +type GetParams = { + leftQuery: string; + leftTimeRange: TimeRange; + rightQuery: string; + rightTimeRange: TimeRange; + maxNodes: number | null; +}; + +class DiffProfileApiClient extends ApiClient { + async get(params: GetParams): Promise { + // /pyroscope/render-diff requests: timerange can be YYYYDDMM, Unix time, Unix time in ms (unix * 1000) + const leftFrom = Number(dateTimeParse(params.leftTimeRange.raw.from).unix()) * 1000; + const leftUntil = Number(dateTimeParse(params.leftTimeRange.raw.to).unix()) * 1000; + const rightFrom = Number(dateTimeParse(params.rightTimeRange.raw.from).unix()) * 1000; + const rightUntil = Number(dateTimeParse(params.rightTimeRange.raw.to).unix()) * 1000; + + const searchParams = new URLSearchParams({ + leftQuery: params.leftQuery, + leftFrom: String(leftFrom), + leftUntil: String(leftUntil), + rightQuery: params.rightQuery, + rightFrom: String(rightFrom), + rightUntil: String(rightUntil), + }); + + if (params.maxNodes) { + searchParams.set('max-nodes', String(params.maxNodes)); + } + + const response = await this.fetch(`/pyroscope/render-diff?${searchParams.toString()}`); + + const json = await response.json(); + + return json; + } +} + +export const diffProfileApiClient = new DiffProfileApiClient(); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/useFetchDiffProfile.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/useFetchDiffProfile.ts new file mode 100644 index 00000000..716b5b2c --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/useFetchDiffProfile.ts @@ -0,0 +1,75 @@ +import { TimeRange } from '@grafana/data'; +import { useMaxNodesFromUrl } from '@shared/domain/url-params/useMaxNodesFromUrl'; +import { useQuery } from '@tanstack/react-query'; + +import { diffProfileApiClient } from './diffProfileApiClient'; + +type FetchParams = { + baselineTimeRange: TimeRange; + baselineQuery: string; + comparisonTimeRange: TimeRange; + comparisonQuery: string; +}; + +export function useFetchDiffProfile({ + baselineTimeRange, + baselineQuery, + comparisonTimeRange, + comparisonQuery, +}: FetchParams) { + const [maxNodes] = useMaxNodesFromUrl(); + + const { isFetching, error, data, refetch } = useQuery({ + // for UX: keep previous data while fetching -> profile does not re-render with empty panels when refreshing + placeholderData: (previousData) => previousData, + enabled: Boolean( + baselineQuery && + comparisonQuery && + // determining the correct left/right ranges takes time and can lead to some values being 0 + // in this case, we would send 0 values to the API, which would make the pods crash + // so we enable only when we have non-zero parameters values + baselineTimeRange?.raw.from.valueOf() && + baselineTimeRange?.raw.to.valueOf() && + comparisonTimeRange?.raw.from.valueOf() && + comparisonTimeRange?.raw.to.valueOf() + ), + // we use "raw" to cache relative time ranges between renders, so that only refetch() will trigger a new query + // eslint-disable-next-line @tanstack/query/exhaustive-deps + queryKey: [ + 'diff-profile', + baselineQuery, + baselineTimeRange?.raw.from.toString(), + baselineTimeRange?.raw.to.toString(), + comparisonQuery, + comparisonTimeRange?.raw.from.toString(), + comparisonTimeRange?.raw.to.toString(), + maxNodes, + ], + queryFn: () => { + diffProfileApiClient.abort(); + + const params = { + leftQuery: baselineQuery, + leftTimeRange: baselineTimeRange!, + rightQuery: comparisonQuery, + rightTimeRange: comparisonTimeRange!, + maxNodes, + }; + + return diffProfileApiClient.get(params).then((json) => ({ + profile: { + version: json.version, + flamebearer: json.flamebearer, + metadata: json.metadata, + }, + })); + }, + }); + + return { + isFetching, + error: diffProfileApiClient.isAbortError(error) ? null : error, + ...data, + refetch, + }; +} diff --git a/src/pages/ProfilesExplorerView/domain/useBuildPyroscopeQuery.ts b/src/pages/ProfilesExplorerView/domain/useBuildPyroscopeQuery.ts new file mode 100644 index 00000000..3683409f --- /dev/null +++ b/src/pages/ProfilesExplorerView/domain/useBuildPyroscopeQuery.ts @@ -0,0 +1,23 @@ +import { sceneGraph, SceneObject } from '@grafana/scenes'; +import { useMemo } from 'react'; + +import { FiltersVariable } from './variables/FiltersVariable/FiltersVariable'; +import { ProfileMetricVariable } from './variables/ProfileMetricVariable'; +import { ServiceNameVariable } from './variables/ServiceNameVariable'; + +export function useBuildPyroscopeQuery(sceneObject: SceneObject, filterKey: string) { + const { value: serviceName } = sceneGraph + .findByKeyAndType(sceneObject, 'serviceName', ServiceNameVariable) + .useState(); + + const { value: profileMetricId } = sceneGraph + .findByKeyAndType(sceneObject, 'profileMetricId', ProfileMetricVariable) + .useState(); + + const { filterExpression } = sceneGraph.findByKeyAndType(sceneObject, filterKey, FiltersVariable).useState(); + + return useMemo( + () => `${profileMetricId}{service_name="${serviceName}",${filterExpression}}`, + [filterExpression, profileMetricId, serviceName] + ); +} diff --git a/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx b/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx index 9bc64a6a..3d79e8de 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/FiltersVariable.tsx @@ -4,12 +4,11 @@ import { useStyles2 } from '@grafana/ui'; import { CompleteFilters } from '@shared/components/QueryBuilder/domain/types'; import { QueryBuilder } from '@shared/components/QueryBuilder/QueryBuilder'; import { useQueryFromUrl } from '@shared/domain/url-params/useQueryFromUrl'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect } from 'react'; -import { ProfileMetricVariable } from '../ProfileMetricVariable'; +import { useBuildPyroscopeQuery } from '../../useBuildPyroscopeQuery'; import { ProfilesDataSourceVariable } from '../ProfilesDataSourceVariable'; -import { ServiceNameVariable } from '../ServiceNameVariable'; -import { convertPyroscopeToVariableFilter, expressionBuilder } from './filters-ops'; +import { convertPyroscopeToVariableFilter } from './filters-ops'; export class FiltersVariable extends AdHocFiltersVariable { static DEFAULT_VALUE = []; @@ -20,67 +19,62 @@ export class FiltersVariable extends AdHocFiltersVariable { name: key, label: 'Filters', filters: FiltersVariable.DEFAULT_VALUE, + expressionBuilder: (filters) => + filters.map(({ key, operator, value }) => `${key}${operator}"${value}"`).join(','), }); - this.addActivationHandler(() => { - // VariableDependencyConfig does not work :man_shrug: (never called) - const dataSourceSub = sceneGraph - .findByKeyAndType(this, 'dataSource', ProfilesDataSourceVariable) - .subscribeToState(() => { - this.setState({ filters: [] }); - }); + this.addActivationHandler(this.onActivate.bind(this)); + } - return () => { - dataSourceSub.unsubscribe(); - }; - }); + onActivate() { + // VariableDependencyConfig does not work :man_shrug: (never called) + const dataSourceSub = sceneGraph + .findByKeyAndType(this, 'dataSource', ProfilesDataSourceVariable) + .subscribeToState(() => { + this.setState({ filters: [] }); + }); + + return () => { + dataSourceSub.unsubscribe(); + }; } - updateQuery = (query: string, filters: CompleteFilters) => { + onChangeQuery = (query: string, filters: CompleteFilters) => { this.setState({ filters: filters.map(convertPyroscopeToVariableFilter), }); }; - static Component = ({ model }: SceneComponentProps) => { + static Component = ({ model }: SceneComponentProps) => { const styles = useStyles2(getStyles); - const { filters } = model.useState(); + const { key } = model.useState(); const [, setQuery] = useQueryFromUrl(); - const { value: dataSourceUid } = sceneGraph - .findByKeyAndType(model, 'dataSource', ProfilesDataSourceVariable) - .useState(); + const query = useBuildPyroscopeQuery(model, key as string); - const { value: serviceName } = sceneGraph.findByKeyAndType(model, 'serviceName', ServiceNameVariable).useState(); + useEffect(() => { + if (typeof query === 'string') { + // Explain Flame Graph (AI button) depends on the query value so we have to sync it here + setQuery(query); + } + }, [query, setQuery]); - const { value: profileMetricId } = sceneGraph - .findByKeyAndType(model, 'profileMetricId', ProfileMetricVariable) + const { value: dataSourceUid } = sceneGraph + .findByKeyAndType(model, 'dataSource', ProfilesDataSourceVariable) .useState(); - const filterExpression = useMemo( - () => expressionBuilder(serviceName as string, profileMetricId as string, filters), - [filters, profileMetricId, serviceName] - ); - const { from, to } = sceneGraph.getTimeRange(model).state.value; - useEffect(() => { - if (typeof filterExpression === 'string') { - // Explain Flame Graph (AI button) depends on the query value so we have to sync it here - setQuery(filterExpression); - } - }, [filterExpression, setQuery]); - return ( ); }; diff --git a/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/filters-ops.tsx b/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/filters-ops.tsx index 2cba4c86..d07d5a87 100644 --- a/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/filters-ops.tsx +++ b/src/pages/ProfilesExplorerView/domain/variables/FiltersVariable/filters-ops.tsx @@ -22,17 +22,6 @@ export const addFilter = (model: FiltersVariable, filter: AdHocVariableFilter) = model.setState({ filters: [...model.state.filters, filter] }); }; -export const expressionBuilder = (serviceName: string, profileMetricId: string, filters: AdHocVariableFilter[]) => { - if (!serviceName || !profileMetricId) { - return ''; - } - - const completeFilters = [{ key: 'service_name', operator: '=', value: serviceName }, ...filters]; - const selector = completeFilters.map(({ key, operator, value }) => `${key}${operator}"${value}"`).join(','); - - return `${profileMetricId}{${selector}}`; -}; - export const parseVariableValue = (variableValue = '') => !variableValue ? [] From 24397a2740cc921b6bba886d9d86d2a8f54f3b4f Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 16 Aug 2024 18:23:25 +0200 Subject: [PATCH 43/76] fix(SceneProfilesExplorer): Hide time and refresh pickers --- .../SceneProfilesExplorer/SceneProfilesExplorer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 5bbc21ed..42a9cf0b 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -317,7 +317,9 @@ export class SceneProfilesExplorer extends SceneObjectBase { const { explorationType, controls, body, $variables } = this.useState(); - const [timePickerControl, refreshPickerControl] = controls as [SceneObject, SceneObject]; + const [timePickerControl, refreshPickerControl] = + explorationType === ExplorationType.DIFF_FLAME_GRAPH ? [] : (controls as [SceneObject, SceneObject]); + const dataSourceVariable = $variables.state.variables[0] as ProfilesDataSourceVariable; const bodySceneObject = body?.state.primary as any; From 269a77c4951881c8f1422dc449e2b0b09daac14d Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 16 Aug 2024 18:42:16 +0200 Subject: [PATCH 44/76] feat(SceneGroupLabels): Clicking on "Compare" opens the new view --- .../SceneGroupByLabels/SceneGroupByLabels.tsx | 54 +++++-------------- .../SceneProfilesExplorer.tsx | 22 +++++--- 2 files changed, 27 insertions(+), 49 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index 11bea591..ca7435b7 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -1,6 +1,5 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { MultiValueVariableState, SceneComponentProps, @@ -11,7 +10,6 @@ import { } from '@grafana/scenes'; import { Stack, useStyles2 } from '@grafana/ui'; import { reportInteraction } from '@shared/domain/reportInteraction'; -import { buildQuery } from '@shared/domain/url-params/parseQuery'; import React, { useMemo } from 'react'; import { Unsubscribable } from 'rxjs'; @@ -24,7 +22,6 @@ import { EventViewServiceFlameGraph } from '../../../../domain/events/EventViewS import { addFilter } from '../../../../domain/variables/FiltersVariable/filters-ops'; import { FiltersVariable } from '../../../../domain/variables/FiltersVariable/FiltersVariable'; import { GroupByVariable } from '../../../../domain/variables/GroupByVariable/GroupByVariable'; -import { computeRoundedTimeRange } from '../../../../helpers/computeRoundedTimeRange'; import { getSceneVariableValue } from '../../../../helpers/getSceneVariableValue'; import { interpolateQueryRunnerVariables } from '../../../../infrastructure/helpers/interpolateQueryRunnerVariables'; import { getProfileMetricLabel } from '../../../../infrastructure/series/helpers/getProfileMetricLabel'; @@ -41,7 +38,7 @@ import { GridItemData } from '../../../SceneByVariableRepeaterGrid/types/GridIte import { SceneDrawer } from '../../../SceneDrawer'; import { SceneLabelValuesBarGauge } from '../../../SceneLabelValuesBarGauge'; import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries'; -import { SceneProfilesExplorer } from '../../../SceneProfilesExplorer/SceneProfilesExplorer'; +import { ExplorationType, SceneProfilesExplorer } from '../../../SceneProfilesExplorer/SceneProfilesExplorer'; import { SceneStatsPanel } from './components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel'; import { CompareTarget } from './components/SceneLabelValuesGrid/domain/types'; import { SceneLabelValuesGrid } from './components/SceneLabelValuesGrid/SceneLabelValuesGrid'; @@ -331,58 +328,31 @@ export class SceneGroupByLabels extends SceneObjectBase this.setState({ compare: new Map() }); } - buildDiffUrl(): string { + updateCompareFilters() { const { compare } = this.state; const baselineItem = compare.get(CompareTarget.BASELINE); const comparisonItem = compare.get(CompareTarget.COMPARISON); - let { appUrl } = config; - if (appUrl.at(-1) !== '/') { - // ensures that the API pathname is appended correctly (appUrl seems to always have it but better to be extra careful) - appUrl += '/'; - } - - const diffUrl = new URL('a/grafana-pyroscope-app/comparison-diff', appUrl); - - // data source - diffUrl.searchParams.set('var-dataSource', getSceneVariableValue(this, 'dataSource')); - - // time range - const { from, to } = computeRoundedTimeRange(sceneGraph.getTimeRange(this).state.value); - diffUrl.searchParams.set('from', from.toString()); - diffUrl.searchParams.set('to', to.toString()); - const baselineQueryRunnerParams = interpolateQueryRunnerVariables(this, baselineItem as GridItemData); const comparisonQueryRunnerParams = interpolateQueryRunnerVariables(this, comparisonItem as GridItemData); - // // query - just in case - const query = buildQuery({ - serviceId: baselineQueryRunnerParams.serviceName, - profileMetricId: baselineQueryRunnerParams.profileMetricId, - labels: baselineQueryRunnerParams.filters.map(({ key, operator, value }) => `${key}${operator}"${value}"`), + sceneGraph.findByKeyAndType(this, 'filtersBaseline', FiltersVariable).setState({ + filters: baselineQueryRunnerParams.filters, }); - diffUrl.searchParams.set('query', query); - - // left & right queries - const [leftQuery, rightQuery] = [baselineQueryRunnerParams, comparisonQueryRunnerParams].map( - ({ serviceName: serviceId, profileMetricId, filters }) => - buildQuery({ - serviceId, - profileMetricId, - labels: filters.map(({ key, operator, value }) => `${key}${operator}"${value}"`), - }) - ); - - diffUrl.searchParams.set('leftQuery', leftQuery); - diffUrl.searchParams.set('rightQuery', rightQuery); - return diffUrl.toString(); + sceneGraph.findByKeyAndType(this, 'filtersComparison', FiltersVariable).setState({ + filters: comparisonQueryRunnerParams.filters, + }); } onClickCompareButton = () => { - window.open(this.buildDiffUrl(), '_blank'); + this.updateCompareFilters(); reportInteraction('g_pyroscope_app_compare_link_clicked'); + + ( + sceneGraph.findByKeyAndType(this, 'profiles-explorer', SceneProfilesExplorer) as SceneProfilesExplorer + ).setExplorationType({ type: ExplorationType.DIFF_FLAME_GRAPH }); }; onClickClearCompareButton = () => { diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 42a9cf0b..abd69ece 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -194,7 +194,7 @@ export class SceneProfilesExplorer extends SceneObjectBase { this.setExplorationType({ type: ExplorationType.PROFILE_TYPES, - comesFromUserAction: true, + resetVariables: true, item: event.payload.item, }); }); @@ -202,7 +202,7 @@ export class SceneProfilesExplorer extends SceneObjectBase { this.setExplorationType({ type: ExplorationType.LABELS, - comesFromUserAction: true, + resetVariables: true, item: event.payload.item, }); }); @@ -210,7 +210,7 @@ export class SceneProfilesExplorer extends SceneObjectBase { this.setExplorationType({ type: ExplorationType.FLAME_GRAPH, - comesFromUserAction: true, + resetVariables: true, item: event.payload.item, }); }); @@ -226,14 +226,14 @@ export class SceneProfilesExplorer extends SceneObjectBase { + sceneGraph.findByKeyAndType(this, filterKey, FiltersVariable)?.setState({ + filters: FiltersVariable.DEFAULT_VALUE, + }); + }); + } + sceneGraph.findByKeyAndType(this, 'groupBy', GroupByVariable)?.changeValueTo(GroupByVariable.DEFAULT_VALUE); sceneGraph.findByKeyAndType(this, 'panel-type-switcher', ScenePanelTypeSwitcher)?.reset(); From a76bc31baab704fe07bfdfa42edb8528baab3619 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 16 Aug 2024 18:52:15 +0200 Subject: [PATCH 45/76] feat(SceneComparePanel): Set default time range value = main time range value --- .../components/SceneComparePanel/SceneComparePanel.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index 1933185f..b310230a 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -32,7 +32,6 @@ import { TimerangeSelectionMode, } from './domain/actions/SwitchTimeRangeSelectionModeAction'; import { EventSwitchTimerangeSelectionMode } from './domain/events/EventSwitchTimerangeSelectionMode'; -import { getDefaultTimeRange } from './domain/getDefaultTimeRange'; import { buildCompareTimeSeriesQueryRunner } from './infrastructure/buildCompareTimeSeriesQueryRunner'; export interface SceneComparePanelState extends SceneObjectState { @@ -61,7 +60,7 @@ export class SceneComparePanel extends SceneObjectBase { title: target === CompareTarget.BASELINE ? 'Baseline' : 'Comparison', filterKey: target === CompareTarget.BASELINE ? 'filtersBaseline' : 'filtersComparison', color: target === CompareTarget.BASELINE ? BASELINE_COLORS.COLOR.toString() : COMPARISON_COLORS.COLOR.toString(), - $timeRange: new SceneTimeRange(getDefaultTimeRange()), + $timeRange: new SceneTimeRange(), timePicker: new SceneTimePicker({ isOnCanvas: true }), refreshPicker: new SceneRefreshPicker({ isOnCanvas: true }), timeseries: undefined, @@ -71,7 +70,9 @@ export class SceneComparePanel extends SceneObjectBase { } onActivate() { - const { title, target } = this.state; + const { $timeRange, title, target } = this.state; + + $timeRange.setState(sceneGraph.getTimeRange(this.parent!).state); const timeseries = this.buildTimeSeries(); From f7e30ef790b7ce664df883de22850ce92827f5d2 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 16 Aug 2024 20:17:45 +0200 Subject: [PATCH 46/76] feat(SceneExploreDiffFlameGraphs): Sync y-axis (raw version) --- .../SceneExploreDiffFlameGraphs.tsx | 70 ++++++++++++++++--- .../SceneComparePanel/SceneComparePanel.tsx | 22 +++--- .../SceneTimeRangeWithAnnotations.ts | 7 +- .../components/SceneLabelValuesTimeseries.tsx | 13 ++-- 4 files changed, 86 insertions(+), 26 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx index 54e2b83c..279f3a08 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { DashboardCursorSync, GrafanaTheme2, TimeRange } from '@grafana/data'; +import { DashboardCursorSync, DataFrame, GrafanaTheme2, TimeRange } from '@grafana/data'; import { behaviors, SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { Spinner, useStyles2 } from '@grafana/ui'; import { AiPanel } from '@shared/components/AiPanel/AiPanel'; @@ -10,8 +10,10 @@ import { useToggleSidePanel } from '@shared/domain/useToggleSidePanel'; import { useFetchPluginSettings } from '@shared/infrastructure/settings/useFetchPluginSettings'; import { FlamebearerProfile } from '@shared/types/FlamebearerProfile'; import { InlineBanner } from '@shared/ui/InlineBanner'; +import { cloneDeep, merge } from 'lodash'; import React, { useEffect } from 'react'; +import { EventDataReceived } from '../../domain/events/EventDataReceived'; import { useBuildPyroscopeQuery } from '../../domain/useBuildPyroscopeQuery'; import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; @@ -36,7 +38,6 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase { - eventSub.unsubscribe(); - profileMetricVariable.setState({ query: ProfileMetricVariable.QUERY_DEFAULT }); profileMetricVariable.update(true); }; @@ -79,9 +78,64 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase { - this.forceRender(); - }); + this._subs.add( + this.subscribeToEvent(EventAnnotationTimeRangeChanged, () => { + this.forceRender(); + }) + ); + + const { baselinePanel, comparisonPanel } = this.state; + + function findYMax(series: DataFrame[]) { + let yMax = -1; + + for (const value of series[0].fields[1].values) { + if (value > yMax) { + yMax = value; + } + } + + return yMax; + } + + let lastMax = -1; + + function updateYMax() { + const max = Math.max(yBaselineMax, yComparisonMax); + + if (max === lastMax) { + return; + } + + [baselinePanel, comparisonPanel].forEach((panel) => { + const timeseries = panel.state.timeseriesPanel!.state.body; + const { state: prevState } = timeseries; + + timeseries.clearFieldConfigCache(); + + timeseries.setState({ + fieldConfig: merge(cloneDeep(prevState.fieldConfig), { defaults: { max } }), + }); + }); + } + + let yBaselineMax = -1; + this._subs.add( + baselinePanel.subscribeToEvent(EventDataReceived, (event) => { + yBaselineMax = -1; + yBaselineMax = findYMax(event.payload.series); + updateYMax(); + }) + ); + + let yComparisonMax = -1; + this._subs.add( + comparisonPanel.subscribeToEvent(EventDataReceived, (event) => { + yComparisonMax = -1; + yComparisonMax = findYMax(event.payload.series); + updateYMax(); + }) + ); } useSceneExploreDiffFlameGraphs = () => { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index b310230a..24e42d08 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -41,7 +41,7 @@ export interface SceneComparePanelState extends SceneObjectState { color: string; timePicker: SceneTimePicker; refreshPicker: SceneRefreshPicker; - timeseries?: SceneLabelValuesTimeseries; + timeseriesPanel?: SceneLabelValuesTimeseries; $timeRange: SceneTimeRange; } @@ -49,7 +49,7 @@ export class SceneComparePanel extends SceneObjectBase { protected _variableDependency = new VariableDependencyConfig(this, { variableNames: ['profileMetricId'], onVariableUpdateCompleted: () => { - this.state.timeseries?.updateTitle(this.buildTimeseriesTitle()); + this.state.timeseriesPanel?.updateTitle(this.buildTimeseriesTitle()); }, }); @@ -63,7 +63,7 @@ export class SceneComparePanel extends SceneObjectBase { $timeRange: new SceneTimeRange(), timePicker: new SceneTimePicker({ isOnCanvas: true }), refreshPicker: new SceneRefreshPicker({ isOnCanvas: true }), - timeseries: undefined, + timeseriesPanel: undefined, }); this.addActivationHandler(this.onActivate.bind(this)); @@ -72,11 +72,12 @@ export class SceneComparePanel extends SceneObjectBase { onActivate() { const { $timeRange, title, target } = this.state; + // TODO: sync with URL search params $timeRange.setState(sceneGraph.getTimeRange(this.parent!).state); - const timeseries = this.buildTimeSeries(); + const timeseriesPanel = this.buildTimeSeriesPanel(); - timeseries.state.body.setState({ + timeseriesPanel.state.body.setState({ $timeRange: new SceneTimeRangeWithAnnotations({ mode: TimeRangeWithAnnotationsMode.ANNOTATIONS, annotationColor: @@ -85,7 +86,7 @@ export class SceneComparePanel extends SceneObjectBase { }), }); - this.setState({ timeseries }); + this.setState({ timeseriesPanel: timeseriesPanel }); const eventSub = this.subscribeToEvents(); @@ -96,7 +97,8 @@ export class SceneComparePanel extends SceneObjectBase { subscribeToEvents() { return this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { - (this.state.timeseries?.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).setState({ + // FIXME: this will cause a EventDataReceived to be published in SceneLabelValuesTimeseries + (this.state.timeseriesPanel?.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).setState({ mode: event.payload.mode === TimerangeSelectionMode.FLAMEGRAPH ? TimeRangeWithAnnotationsMode.ANNOTATIONS @@ -105,7 +107,7 @@ export class SceneComparePanel extends SceneObjectBase { }); } - buildTimeSeries() { + buildTimeSeriesPanel() { const { target, filterKey, title, color } = this.state; return new SceneLabelValuesTimeseries({ @@ -151,12 +153,12 @@ export class SceneComparePanel extends SceneObjectBase { } getDiffTimeRange() { - return this.state.timeseries?.state.body.state.$timeRange as SceneTimeRangeWithAnnotations; + return this.state.timeseriesPanel?.state.body.state.$timeRange as SceneTimeRangeWithAnnotations; } public static Component = ({ model }: SceneComponentProps) => { const styles = useStyles2(getStyles); - const { title, timeseries, timePicker, refreshPicker, filterKey } = model.useState(); + const { title, timeseriesPanel: timeseries, timePicker, refreshPicker, filterKey } = model.useState(); const filtersVariable = sceneGraph.findByKey(model, filterKey) as FiltersVariable; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index 7f468aed..051e81e1 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -59,7 +59,7 @@ export class SceneTimeRangeWithAnnotations this._subs.add(ancestorTimeRangeObject.subscribeToState((newState) => this.setState(newState))); - const { $data } = this.getTimeseriesPanel().state; + const { $data } = this.getTimeseries().state; this._subs.add( $data?.subscribeToState((newState, prevState) => { @@ -78,7 +78,7 @@ export class SceneTimeRangeWithAnnotations return sceneGraph.getTimeRange(this.parent.parent); } - protected getTimeseriesPanel(): VizPanel { + protected getTimeseries(): VizPanel { try { const vizPanel = sceneGraph.getAncestor(this, VizPanel); @@ -95,7 +95,7 @@ export class SceneTimeRangeWithAnnotations protected updateTimeseriesAnnotation() { const { annotationTimeRange, annotationColor, annotationTitle } = this.state; - const { $data } = this.getTimeseriesPanel().state; + const { $data } = this.getTimeseries().state; const data = $data?.state.data; if (!data || !annotationTimeRange) { @@ -111,6 +111,7 @@ export class SceneTimeRangeWithAnnotations timeEnd: annotationTimeRange.to.unix() * 1000, }); + // FIXME: this will cause a EventDataReceived to be published in SceneLabelValuesTimeseries $data?.setState({ data: { ...data, diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx index f9d6db23..d4cef81b 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx @@ -75,14 +75,12 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { - if (state.data?.state !== LoadingState.Done) { + const sub = (body.state.$data as SceneDataTransformer)!.subscribeToState((newState) => { + if (newState.data?.state !== LoadingState.Done) { return; } - const { series } = state.data; - - this.publishEvent(new EventDataReceived({ series }), true); + const { series } = newState.data; if (!series.length) { return; @@ -91,6 +89,11 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { From 171ddb802103cb16323c23ffcb418a9ac0bc3f10 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 20 Aug 2024 09:55:35 +0200 Subject: [PATCH 47/76] fix(SceneLabelValuesTimeseries): Update config only when new timeseries have been fetched --- .../SceneExploreDiffFlameGraphs.tsx | 5 ++-- .../SceneComparePanel/SceneComparePanel.tsx | 1 - .../SceneTimeRangeWithAnnotations.ts | 1 - .../components/SceneLabelValuesTimeseries.tsx | 27 +++++++++---------- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx index 279f3a08..6452397a 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx @@ -74,9 +74,10 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index 24e42d08..b0a6aa9a 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -97,7 +97,6 @@ export class SceneComparePanel extends SceneObjectBase { subscribeToEvents() { return this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { - // FIXME: this will cause a EventDataReceived to be published in SceneLabelValuesTimeseries (this.state.timeseriesPanel?.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).setState({ mode: event.payload.mode === TimerangeSelectionMode.FLAMEGRAPH diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index 051e81e1..87e9157e 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -111,7 +111,6 @@ export class SceneTimeRangeWithAnnotations timeEnd: annotationTimeRange.to.unix() * 1000, }); - // FIXME: this will cause a EventDataReceived to be published in SceneLabelValuesTimeseries $data?.setState({ data: { ...data, diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx index d4cef81b..60800ca1 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx @@ -75,25 +75,22 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { - if (newState.data?.state !== LoadingState.Done) { - return; - } - - const { series } = newState.data; + const sub = (body.state.$data as SceneDataProvider).subscribeToState((newState, prevState) => { + if (newState.data?.state === LoadingState.Done && prevState.data?.state !== LoadingState.Done) { + const { series } = newState.data; - if (!series.length) { - return; - } + if (!series.length) { + return; + } - const config = this.state.displayAllValues ? this.getAllValuesConfig(series) : this.getConfig(series); + const config = this.state.displayAllValues ? this.getAllValuesConfig(series) : this.getConfig(series); - body.setState(config); + body.setState(config); - // we publish the event only after setting the new config so that the subscribers can modify it - // (e.g. sync y-axis in SceneExploreDiffFlameGraphs.tsx) - this.publishEvent(new EventDataReceived({ series }), true); - // FIXME: this event is also published for unrelated reasons (see SceneComparePanel.ts and SceneTimeRangeWithAnnotations.ts) + // we publish the event only after setting the new config so that the subscribers can modify it + // (e.g. sync y-axis in SceneExploreDiffFlameGraphs.tsx) + this.publishEvent(new EventDataReceived({ series }), true); + } }); return () => { From 24a474871c1cc5a27f5684e36800cb64881985c6 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 20 Aug 2024 11:06:57 +0200 Subject: [PATCH 48/76] refactor(*): Extract sync y axis as a behaviour + small fixes --- .../SceneByVariableRepeaterGrid.tsx | 4 +- .../SceneExploreDiffFlameGraphs.tsx | 99 +++++-------------- .../domain/behaviours/syncYAxis.ts | 51 ++++++++++ .../SceneLabelValuesGrid.tsx | 4 +- .../components/SceneLabelValuePanel.tsx | 4 +- .../components/SceneLabelValuesBarGauge.tsx | 13 +-- .../components/SceneLabelValuesTimeseries.tsx | 24 +++-- .../domain/events/EventDataReceived.ts | 9 -- .../events/EventTimeseriesDataReceived.ts | 9 ++ 9 files changed, 107 insertions(+), 110 deletions(-) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/behaviours/syncYAxis.ts delete mode 100644 src/pages/ProfilesExplorerView/domain/events/EventDataReceived.ts create mode 100644 src/pages/ProfilesExplorerView/domain/events/EventTimeseriesDataReceived.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx index e23580ea..7506f7b3 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx @@ -16,7 +16,7 @@ import { noOp } from '@shared/domain/noOp'; import { debounce, isEqual } from 'lodash'; import React from 'react'; -import { EventDataReceived } from '../../domain/events/EventDataReceived'; +import { EventTimeseriesDataReceived } from '../../domain/events/EventTimeseriesDataReceived'; import { FiltersVariable } from '../../domain/variables/FiltersVariable/FiltersVariable'; import { getSceneVariableValue } from '../../helpers/getSceneVariableValue'; import { SceneLabelValuesBarGauge } from '../SceneLabelValuesBarGauge'; @@ -315,7 +315,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { + const sub = vizPanel.subscribeToEvent(EventTimeseriesDataReceived, (event) => { if (event.payload.series.length > 0) { return; } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx index 6452397a..3f1e61f3 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { DashboardCursorSync, DataFrame, GrafanaTheme2, TimeRange } from '@grafana/data'; +import { DashboardCursorSync, GrafanaTheme2, TimeRange } from '@grafana/data'; import { behaviors, SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { Spinner, useStyles2 } from '@grafana/ui'; import { AiPanel } from '@shared/components/AiPanel/AiPanel'; @@ -10,16 +10,15 @@ import { useToggleSidePanel } from '@shared/domain/useToggleSidePanel'; import { useFetchPluginSettings } from '@shared/infrastructure/settings/useFetchPluginSettings'; import { FlamebearerProfile } from '@shared/types/FlamebearerProfile'; import { InlineBanner } from '@shared/ui/InlineBanner'; -import { cloneDeep, merge } from 'lodash'; import React, { useEffect } from 'react'; -import { EventDataReceived } from '../../domain/events/EventDataReceived'; import { useBuildPyroscopeQuery } from '../../domain/useBuildPyroscopeQuery'; import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; import { CompareTarget } from '../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; import { EventAnnotationTimeRangeChanged } from './components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged'; import { SceneComparePanel } from './components/SceneComparePanel/SceneComparePanel'; +import { syncYAxis } from './domain/behaviours/syncYAxis'; import { useFetchDiffProfile } from './infrastructure/useFetchDiffProfile'; interface SceneExploreDiffFlameGraphsState extends SceneObjectState { @@ -29,19 +28,24 @@ interface SceneExploreDiffFlameGraphsState extends SceneObjectState { export class SceneExploreDiffFlameGraphs extends SceneObjectBase { constructor() { + const baselinePanel = new SceneComparePanel({ + target: CompareTarget.BASELINE, + }); + + const comparisonPanel = new SceneComparePanel({ + target: CompareTarget.COMPARISON, + }); + super({ key: 'explore-diff-flame-graphs', - baselinePanel: new SceneComparePanel({ - target: CompareTarget.BASELINE, - }), - comparisonPanel: new SceneComparePanel({ - target: CompareTarget.COMPARISON, - }), + baselinePanel, + comparisonPanel, $behaviors: [ new behaviors.CursorSync({ key: 'metricCrosshairSync', sync: DashboardCursorSync.Crosshair, }), + syncYAxis(), ], }); @@ -54,7 +58,16 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase { + this.forceRender(); + }) + ); return () => { profileMetricVariable.setState({ query: ProfileMetricVariable.QUERY_DEFAULT }); @@ -73,72 +86,6 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase { - this.forceRender(); - }) - ); - - const { baselinePanel, comparisonPanel } = this.state; - - function findYMax(series: DataFrame[]) { - let yMax = -1; - - for (const value of series[0].fields[1].values) { - if (value > yMax) { - yMax = value; - } - } - - return yMax; - } - - let lastMax = -1; - - function updateYMax() { - const max = Math.max(yBaselineMax, yComparisonMax); - - if (max === lastMax) { - return; - } - - [baselinePanel, comparisonPanel].forEach((panel) => { - const timeseries = panel.state.timeseriesPanel!.state.body; - const { state: prevState } = timeseries; - - timeseries.clearFieldConfigCache(); - - timeseries.setState({ - fieldConfig: merge(cloneDeep(prevState.fieldConfig), { defaults: { max } }), - }); - }); - } - - let yBaselineMax = -1; - this._subs.add( - baselinePanel.subscribeToEvent(EventDataReceived, (event) => { - yBaselineMax = -1; - yBaselineMax = findYMax(event.payload.series); - updateYMax(); - }) - ); - - let yComparisonMax = -1; - this._subs.add( - comparisonPanel.subscribeToEvent(EventDataReceived, (event) => { - yComparisonMax = -1; - yComparisonMax = findYMax(event.payload.series); - updateYMax(); - }) - ); - } - useSceneExploreDiffFlameGraphs = () => { const { baselinePanel, comparisonPanel } = this.useState(); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/behaviours/syncYAxis.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/behaviours/syncYAxis.ts new file mode 100644 index 00000000..15cada6b --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/behaviours/syncYAxis.ts @@ -0,0 +1,51 @@ +import { DataFrame } from '@grafana/data'; +import { sceneGraph, SceneObject, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { cloneDeep, merge } from 'lodash'; + +import { EventTimeseriesDataReceived } from '../../../../domain/events/EventTimeseriesDataReceived'; + +export function syncYAxis() { + return (vizPanel: SceneObject) => { + const maxima = new Map(); + + const eventSub = vizPanel.subscribeToEvent(EventTimeseriesDataReceived, (event) => { + const { series } = event.payload; + + maxima.set(series[0].refId as string, findMaxValue(series)); + + updateTimeseriesAxis(vizPanel, Math.max(...maxima.values())); + }); + + return () => { + eventSub.unsubscribe(); + }; + }; +} + +function findMaxValue(series: DataFrame[]) { + let max = -1; + + for (const value of series[0].fields[1].values) { + if (value > max) { + max = value; + } + } + + return max; +} + +function updateTimeseriesAxis(vizPanel: SceneObject, max: number) { + // findAllObjects searches down the full scene graph + const timeseries = sceneGraph.findAllObjects( + vizPanel, + (o) => o instanceof VizPanel && o.state.pluginId === 'timeseries' + ) as VizPanel[]; + + for (const t of timeseries) { + t.clearFieldConfigCache(); // required + + t.setState({ + fieldConfig: merge(cloneDeep(t.state.fieldConfig), { defaults: { max } }), + }); + } +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index 8d2f7104..b9435cf5 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -15,7 +15,7 @@ import { Spinner } from '@grafana/ui'; import { debounce, isEqual } from 'lodash'; import React from 'react'; -import { EventDataReceived } from '../../../../../../domain/events/EventDataReceived'; +import { EventTimeseriesDataReceived } from '../../../../../../domain/events/EventTimeseriesDataReceived'; import { FiltersVariable } from '../../../../../../domain/variables/FiltersVariable/FiltersVariable'; import { GroupByVariable } from '../../../../../../domain/variables/GroupByVariable/GroupByVariable'; import { getSceneVariableValue } from '../../../../../../helpers/getSceneVariableValue'; @@ -338,7 +338,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase { + const sub = vizPanel.subscribeToEvent(EventTimeseriesDataReceived, (event) => { if (!this.state.hideNoData || event.payload.series.length) { return; } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx index dc4b098b..11bff4e2 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx @@ -4,7 +4,7 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanelState } import { useStyles2 } from '@grafana/ui'; import React from 'react'; -import { EventDataReceived } from '../../../../../../../domain/events/EventDataReceived'; +import { EventTimeseriesDataReceived } from '../../../../../../../domain/events/EventTimeseriesDataReceived'; import { getSeriesStatsValue } from '../../../../../../../infrastructure/helpers/getSeriesStatsValue'; import { GridItemData } from '../../../../../../SceneByVariableRepeaterGrid/types/GridItemData'; import { SceneLabelValuesTimeseries } from '../../../../../../SceneLabelValuesTimeseries'; @@ -40,7 +40,7 @@ export class SceneLabelValuePanel extends SceneObjectBase { + const timeseriesSub = timeseriesPanel.subscribeToEvent(EventTimeseriesDataReceived, (event) => { const [s] = event.payload.series; if (!s) { diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx index 957d9e2c..f252236e 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx @@ -11,7 +11,7 @@ import { import { BarGaugeDisplayMode, BarGaugeNamePlacement, BarGaugeSizing, BarGaugeValueMode } from '@grafana/schema'; import React from 'react'; -import { EventDataReceived } from '../domain/events/EventDataReceived'; +import { EventTimeseriesDataReceived } from '../domain/events/EventTimeseriesDataReceived'; import { getColorByIndex } from '../helpers/getColorByIndex'; import { getSeriesLabelFieldName } from '../infrastructure/helpers/getSeriesLabelFieldName'; import { getSeriesStatsValue } from '../infrastructure/helpers/getSeriesStatsValue'; @@ -51,16 +51,17 @@ export class SceneLabelValuesBarGauge extends SceneObjectBase { - if (state.data?.state !== LoadingState.Done) { + const sub = (body.state.$data as SceneDataTransformer)!.subscribeToState((newState) => { + if (newState.data?.state !== LoadingState.Done) { return; } - const { series } = state.data; - - this.publishEvent(new EventDataReceived({ series }), true); + const { series } = newState.data; body.setState(this.getConfig(item, series)); + + // we publish the event only after setting the new config so that the subscribers can modify it + this.publishEvent(new EventTimeseriesDataReceived({ series }), true); }); return () => { diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx index 60800ca1..81aa54b5 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesTimeseries.tsx @@ -12,7 +12,7 @@ import { import { GraphGradientMode } from '@grafana/schema'; import React from 'react'; -import { EventDataReceived } from '../domain/events/EventDataReceived'; +import { EventTimeseriesDataReceived } from '../domain/events/EventTimeseriesDataReceived'; import { getColorByIndex } from '../helpers/getColorByIndex'; import { getSeriesLabelFieldName } from '../infrastructure/helpers/getSeriesLabelFieldName'; import { getSeriesStatsValue } from '../infrastructure/helpers/getSeriesStatsValue'; @@ -75,22 +75,20 @@ export class SceneLabelValuesTimeseries extends SceneObjectBase { - if (newState.data?.state === LoadingState.Done && prevState.data?.state !== LoadingState.Done) { - const { series } = newState.data; + const sub = (body.state.$data as SceneDataProvider).subscribeToState((newState) => { + if (newState.data?.state !== LoadingState.Done) { + return; + } - if (!series.length) { - return; - } + const { series } = newState.data; - const config = this.state.displayAllValues ? this.getAllValuesConfig(series) : this.getConfig(series); + const config = this.state.displayAllValues ? this.getAllValuesConfig(series) : this.getConfig(series); - body.setState(config); + body.setState(config); - // we publish the event only after setting the new config so that the subscribers can modify it - // (e.g. sync y-axis in SceneExploreDiffFlameGraphs.tsx) - this.publishEvent(new EventDataReceived({ series }), true); - } + // we publish the event only after setting the new config so that the subscribers can modify it + // (e.g. sync y-axis in SceneExploreDiffFlameGraphs.tsx) + this.publishEvent(new EventTimeseriesDataReceived({ series }), true); }); return () => { diff --git a/src/pages/ProfilesExplorerView/domain/events/EventDataReceived.ts b/src/pages/ProfilesExplorerView/domain/events/EventDataReceived.ts deleted file mode 100644 index 782ab841..00000000 --- a/src/pages/ProfilesExplorerView/domain/events/EventDataReceived.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BusEventWithPayload, DataFrame } from '@grafana/data'; - -export interface EventDataReceivedPayload { - series: DataFrame[]; -} - -export class EventDataReceived extends BusEventWithPayload { - public static type = 'data-received'; -} diff --git a/src/pages/ProfilesExplorerView/domain/events/EventTimeseriesDataReceived.ts b/src/pages/ProfilesExplorerView/domain/events/EventTimeseriesDataReceived.ts new file mode 100644 index 00000000..5f453870 --- /dev/null +++ b/src/pages/ProfilesExplorerView/domain/events/EventTimeseriesDataReceived.ts @@ -0,0 +1,9 @@ +import { BusEventWithPayload, DataFrame } from '@grafana/data'; + +export interface EventTimeseriesDataReceivedPayload { + series: DataFrame[]; +} + +export class EventTimeseriesDataReceived extends BusEventWithPayload { + public static type = 'timeseries-data-received'; +} From 01220c94a8e7027e39d0fe52cfe48fadfb880f0a Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 20 Aug 2024 11:39:35 +0200 Subject: [PATCH 49/76] feat(*): sync flame graph time ranges with URL search params --- .../SceneComparePanel/SceneComparePanel.tsx | 5 +- .../SceneTimeRangeWithAnnotations.ts | 86 +++++++++++++++---- .../domain/evaluateTimeRange.ts | 21 +++++ .../SceneComparePanel/domain/parseUrlParam.ts | 41 +++++++++ .../domain/behaviours/syncYAxis.ts | 6 ++ .../SceneProfilesExplorer.tsx | 4 + src/shared/ui/PageTitle.tsx | 4 +- 7 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/evaluateTimeRange.ts create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/parseUrlParam.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index b0a6aa9a..4d167419 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -70,10 +70,7 @@ export class SceneComparePanel extends SceneObjectBase { } onActivate() { - const { $timeRange, title, target } = this.state; - - // TODO: sync with URL search params - $timeRange.setState(sceneGraph.getTimeRange(this.parent!).state); + const { title, target } = this.state; const timeseriesPanel = this.buildTimeSeriesPanel(); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index 87e9157e..42d3a3d9 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -1,8 +1,18 @@ import { dateTime, TimeRange } from '@grafana/data'; -import { sceneGraph, SceneObjectBase, SceneTimeRangeLike, SceneTimeRangeState, VizPanel } from '@grafana/scenes'; - +import { + sceneGraph, + SceneObjectBase, + SceneObjectUrlSyncConfig, + SceneObjectUrlValues, + SceneTimeRangeLike, + SceneTimeRangeState, + VizPanel, +} from '@grafana/scenes'; + +import { evaluateTimeRange } from '../domain/evaluateTimeRange'; import { EventAnnotationTimeRangeChanged } from '../domain/events/EventAnnotationTimeRangeChanged'; import { getDefaultTimeRange } from '../domain/getDefaultTimeRange'; +import { parseUrlParam } from '../domain/parseUrlParam'; import { RangeAnnotation } from '../domain/RangeAnnotation'; export enum TimeRangeWithAnnotationsMode { @@ -21,6 +31,8 @@ export class SceneTimeRangeWithAnnotations extends SceneObjectBase implements SceneTimeRangeLike { + protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['aFrom', 'aTo'] }); + constructor({ annotationColor, annotationTitle, @@ -38,7 +50,7 @@ export class SceneTimeRangeWithAnnotations annotationTimeRange: { from: dateTime(0), to: dateTime(0), - raw: { from: dateTime(0), to: dateTime(0) }, + raw: { from: '', to: '' }, }, annotationColor, annotationTitle, @@ -48,13 +60,47 @@ export class SceneTimeRangeWithAnnotations this.addActivationHandler(this.onActivate.bind(this)); } + getUrlState() { + const { annotationTimeRange } = this.state; + + return { + aFrom: + typeof annotationTimeRange.raw.from === 'string' + ? annotationTimeRange.raw.from + : annotationTimeRange.raw.from.toISOString(), + aTo: + typeof annotationTimeRange.raw.to === 'string' + ? annotationTimeRange.raw.to + : annotationTimeRange.raw.to.toISOString(), + }; + } + + // eslint-disable-next-line sonarjs/cognitive-complexity + updateFromUrl(values: SceneObjectUrlValues) { + const { aFrom, aTo } = values; + + if (!aTo && !aFrom) { + return; + } + + const { annotationTimeRange } = this.state; + + this.setState({ + annotationTimeRange: evaluateTimeRange( + parseUrlParam(aFrom) ?? annotationTimeRange.from, + parseUrlParam(aTo) ?? annotationTimeRange.to, + this.getTimeZone(), + this.state.fiscalYearStartMonth, + this.state.UNSAFE_nowDelay + ), + }); + } + onActivate() { const ancestorTimeRangeObject = this.getAncestorTimeRange(); this.setState({ ...ancestorTimeRangeObject.state, - // TODO - // annotationTimeRange: ancestorTimeRangeObject.state.value, }); this._subs.add(ancestorTimeRangeObject.subscribeToState((newState) => this.setState(newState))); @@ -63,7 +109,17 @@ export class SceneTimeRangeWithAnnotations this._subs.add( $data?.subscribeToState((newState, prevState) => { - if (newState.data && !newState.data.annotations?.length && prevState.data?.annotations?.length) { + if (!newState.data) { + return; + } + + // add annotation for the first time + if (!newState.data.annotations?.length && !prevState.data?.annotations?.length) { + this.updateTimeseriesAnnotation(); + return; + } + + if (!newState.data.annotations?.length && prevState.data?.annotations?.length) { newState.data.annotations = prevState.data.annotations; } }) @@ -98,7 +154,7 @@ export class SceneTimeRangeWithAnnotations const { $data } = this.getTimeseries().state; const data = $data?.state.data; - if (!data || !annotationTimeRange) { + if (!data) { return; } @@ -111,6 +167,7 @@ export class SceneTimeRangeWithAnnotations timeEnd: annotationTimeRange.to.unix() * 1000, }); + // tradeoff: this will trigger any $data subscribers even though the data itself hasn't changed $data?.setState({ data: { ...data, @@ -120,22 +177,21 @@ export class SceneTimeRangeWithAnnotations } onTimeRangeChange(timeRange: TimeRange): void { - const { mode, annotationTimeRange } = this.state; + const { mode } = this.state; if (mode === TimeRangeWithAnnotationsMode.DEFAULT) { this.getAncestorTimeRange().onTimeRangeChange(timeRange); return; } - // we don't do this.setState({ annotationTimeRange: timeRange }); - // because it would cause a ttimeseries query to be made to the API - annotationTimeRange.from = timeRange.from; - annotationTimeRange.to = timeRange.to; - annotationTimeRange.raw = timeRange.raw; - - this.publishEvent(new EventAnnotationTimeRangeChanged({ timeRange }), true); + // note: this update causes a timeseries query to be made to the API + this.setState({ + annotationTimeRange: timeRange, + }); this.updateTimeseriesAnnotation(); + + this.publishEvent(new EventAnnotationTimeRangeChanged({ timeRange }), true); } onTimeZoneChange(timeZone: string): void { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/evaluateTimeRange.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/evaluateTimeRange.ts new file mode 100644 index 00000000..2984ebaa --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/evaluateTimeRange.ts @@ -0,0 +1,21 @@ +import { dateMath, DateTime, TimeRange } from '@grafana/data'; +import { TimeZone } from '@grafana/schema'; + +export function evaluateTimeRange( + from: string | DateTime, + to: string | DateTime, + timeZone: TimeZone, + fiscalYearStartMonth?: number, + delay?: string +): TimeRange { + const hasDelay = delay && to === 'now'; + + return { + from: dateMath.parse(from, false, timeZone, fiscalYearStartMonth)!, + to: dateMath.parse(hasDelay ? 'now-' + delay : to, true, timeZone, fiscalYearStartMonth)!, + raw: { + from: from, + to: to, + }, + }; +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/parseUrlParam.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/parseUrlParam.ts new file mode 100644 index 00000000..43d19108 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/parseUrlParam.ts @@ -0,0 +1,41 @@ +import { toUtc } from '@grafana/data'; +import { SceneObjectUrlValue } from '@grafana/scenes'; + +const INTERVAL_STRING_REGEX = /^\d+[yYmMsSwWhHdD]$/; + +// eslint-disable-next-line sonarjs/cognitive-complexity +export function parseUrlParam(value: SceneObjectUrlValue): string | null { + if (typeof value !== 'string') { + return null; + } + + if (value.indexOf('now') !== -1) { + return value; + } + + if (INTERVAL_STRING_REGEX.test(value)) { + return value; + } + + if (value.length === 8) { + const utcValue = toUtc(value, 'YYYYMMDD'); + if (utcValue.isValid()) { + return utcValue.toISOString(); + } + } else if (value.length === 15) { + const utcValue = toUtc(value, 'YYYYMMDDTHHmmss'); + if (utcValue.isValid()) { + return utcValue.toISOString(); + } + } else if (value.length === 24) { + const utcValue = toUtc(value); + return utcValue.toISOString(); + } + + const epoch = parseInt(value, 10); + if (!isNaN(epoch)) { + return toUtc(epoch).toISOString(); + } + + return null; +} diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/behaviours/syncYAxis.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/behaviours/syncYAxis.ts index 15cada6b..1a4d7db1 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/behaviours/syncYAxis.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/behaviours/syncYAxis.ts @@ -10,6 +10,12 @@ export function syncYAxis() { const eventSub = vizPanel.subscribeToEvent(EventTimeseriesDataReceived, (event) => { const { series } = event.payload; + const refId = series[0]?.refId; + + if (!refId) { + console.warn('Missing refId! Cannot sync y-axis on the timeseries.', series); + return; + } maxima.set(series[0].refId as string, findMaxValue(series)); diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index abd69ece..9f9e4c60 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -20,6 +20,7 @@ import { import { IconButton, InlineLabel, useStyles2 } from '@grafana/ui'; import { displayError, displaySuccess } from '@shared/domain/displayStatus'; import { reportInteraction } from '@shared/domain/reportInteraction'; +import { useTimeRangeFromUrl } from '@shared/domain/url-params/useTimeRangeFromUrl'; import { VersionInfoTooltip } from '@shared/ui/VersionInfoTooltip'; import React from 'react'; @@ -362,6 +363,9 @@ export class SceneProfilesExplorer extends SceneObjectBase) { const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks + // TODO: TEMP to ensure that the default timerange is set + useTimeRangeFromUrl(); // eslint-disable-line react-hooks/rules-of-hooks + const { data, actions } = model.useProfilesExplorer(); const { diff --git a/src/shared/ui/PageTitle.tsx b/src/shared/ui/PageTitle.tsx index e6f146a4..25af1628 100644 --- a/src/shared/ui/PageTitle.tsx +++ b/src/shared/ui/PageTitle.tsx @@ -3,7 +3,6 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Stack, useStyles2 } from '@grafana/ui'; import { QueryAnalysisResult } from '@shared/components/QueryAnalysisTooltip/domain/QueryAnalysis'; import { QueryAnalysisTooltip } from '@shared/components/QueryAnalysisTooltip/QueryAnalysisTooltip'; -import { useQueryFromUrl } from '@shared/domain/url-params/useQueryFromUrl'; import React, { memo, ReactNode } from 'react'; import { Helmet } from 'react-helmet'; @@ -16,8 +15,7 @@ type PageTitleProps = { function PageTitleComponent({ title, queryAnalysis }: PageTitleProps) { const styles = useStyles2(getStyles); - const [query] = useQueryFromUrl(); - const fullTitle = typeof title === 'string' ? `${title} | ${query} | Pyroscope` : '...'; + const fullTitle = typeof title === 'string' ? `${title} | Pyroscope` : 'Pyroscope'; return ( <> From 190221a798d007e23e803bf0c5b673fe52b9c486 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 20 Aug 2024 12:08:07 +0200 Subject: [PATCH 50/76] feat(*): Use proper time range defaults --- .../SceneComparePanel/SceneComparePanel.tsx | 17 +++++++++++++++-- .../components/SceneTimeRangeWithAnnotations.ts | 2 +- .../SceneProfilesExplorer.tsx | 7 ++----- .../domain/getDefaultTimeRange.ts | 0 4 files changed, 18 insertions(+), 8 deletions(-) rename src/pages/ProfilesExplorerView/{components/SceneExploreDiffFlameGraphs/components/SceneComparePanel => }/domain/getDefaultTimeRange.ts (100%) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx index 4d167419..2b10a909 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx @@ -9,6 +9,7 @@ import { SceneRefreshPicker, SceneTimePicker, SceneTimeRange, + SceneTimeRangeLike, VariableDependencyConfig, } from '@grafana/scenes'; import { InlineLabel, useStyles2 } from '@grafana/ui'; @@ -42,7 +43,7 @@ export interface SceneComparePanelState extends SceneObjectState { timePicker: SceneTimePicker; refreshPicker: SceneRefreshPicker; timeseriesPanel?: SceneLabelValuesTimeseries; - $timeRange: SceneTimeRange; + $timeRange?: SceneTimeRange; } export class SceneComparePanel extends SceneObjectBase { @@ -60,7 +61,7 @@ export class SceneComparePanel extends SceneObjectBase { title: target === CompareTarget.BASELINE ? 'Baseline' : 'Comparison', filterKey: target === CompareTarget.BASELINE ? 'filtersBaseline' : 'filtersComparison', color: target === CompareTarget.BASELINE ? BASELINE_COLORS.COLOR.toString() : COMPARISON_COLORS.COLOR.toString(), - $timeRange: new SceneTimeRange(), + $timeRange: undefined, timePicker: new SceneTimePicker({ isOnCanvas: true }), refreshPicker: new SceneRefreshPicker({ isOnCanvas: true }), timeseriesPanel: undefined, @@ -72,6 +73,10 @@ export class SceneComparePanel extends SceneObjectBase { onActivate() { const { title, target } = this.state; + const { from, to, value } = this.getAncestorTimeRange().state; + + this.setState({ $timeRange: new SceneTimeRange({ from, to, value }) }); + const timeseriesPanel = this.buildTimeSeriesPanel(); timeseriesPanel.state.body.setState({ @@ -92,6 +97,14 @@ export class SceneComparePanel extends SceneObjectBase { }; } + protected getAncestorTimeRange(): SceneTimeRangeLike { + if (!this.parent || !this.parent.parent) { + throw new Error(typeof this + ' must be used within $timeRange scope'); + } + + return sceneGraph.getTimeRange(this.parent.parent); + } + subscribeToEvents() { return this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { (this.state.timeseriesPanel?.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).setState({ diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index 42d3a3d9..a62bbd8c 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -9,9 +9,9 @@ import { VizPanel, } from '@grafana/scenes'; +import { getDefaultTimeRange } from '../../../../../domain/getDefaultTimeRange'; import { evaluateTimeRange } from '../domain/evaluateTimeRange'; import { EventAnnotationTimeRangeChanged } from '../domain/events/EventAnnotationTimeRangeChanged'; -import { getDefaultTimeRange } from '../domain/getDefaultTimeRange'; import { parseUrlParam } from '../domain/parseUrlParam'; import { RangeAnnotation } from '../domain/RangeAnnotation'; diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 9f9e4c60..0103eb89 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -20,7 +20,6 @@ import { import { IconButton, InlineLabel, useStyles2 } from '@grafana/ui'; import { displayError, displaySuccess } from '@shared/domain/displayStatus'; import { reportInteraction } from '@shared/domain/reportInteraction'; -import { useTimeRangeFromUrl } from '@shared/domain/url-params/useTimeRangeFromUrl'; import { VersionInfoTooltip } from '@shared/ui/VersionInfoTooltip'; import React from 'react'; @@ -31,6 +30,7 @@ import { SceneExploreServiceProfileTypes } from '../../components/SceneExploreSe import { EventViewServiceFlameGraph } from '../../domain/events/EventViewServiceFlameGraph'; import { EventViewServiceLabels } from '../../domain/events/EventViewServiceLabels'; import { EventViewServiceProfiles } from '../../domain/events/EventViewServiceProfiles'; +import { getDefaultTimeRange } from '../../domain/getDefaultTimeRange'; import { FiltersVariable } from '../../domain/variables/FiltersVariable/FiltersVariable'; import { GroupByVariable } from '../../domain/variables/GroupByVariable/GroupByVariable'; import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; @@ -108,7 +108,7 @@ export class SceneProfilesExplorer extends SceneObjectBase) { const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks - // TODO: TEMP to ensure that the default timerange is set - useTimeRangeFromUrl(); // eslint-disable-line react-hooks/rules-of-hooks - const { data, actions } = model.useProfilesExplorer(); const { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/getDefaultTimeRange.ts b/src/pages/ProfilesExplorerView/domain/getDefaultTimeRange.ts similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/getDefaultTimeRange.ts rename to src/pages/ProfilesExplorerView/domain/getDefaultTimeRange.ts From 7786bf918517a908e395bb1557c0376be7b20da2 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 20 Aug 2024 12:20:42 +0200 Subject: [PATCH 51/76] feat(Share): Update share link to use diff params as well --- .../SceneProfilesExplorer/SceneProfilesExplorer.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 0103eb89..234970e3 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { dateTimeParse, GrafanaTheme2 } from '@grafana/data'; +import { dateMath, GrafanaTheme2 } from '@grafana/data'; import { EmbeddedSceneState, getUrlSyncManager, @@ -313,9 +313,14 @@ export class SceneProfilesExplorer extends SceneObjectBase { try { const shareableUrl = new URL(window.location.toString()); + const { searchParams } = shareableUrl; - ['from', 'to'].forEach((name) => { - shareableUrl.searchParams.set(name, String(dateTimeParse(shareableUrl.searchParams.get(name)).valueOf())); + searchParams.delete('query'); // TODO: temp while removing the comparison pages + + ['from', 'to', 'from-2', 'to-2', 'from-3', 'to-3', 'aFrom', 'aTo', 'aFrom-2', 'aTo-2'].forEach((name) => { + if (searchParams.has(name)) { + searchParams.set(name, String(dateMath.parse(searchParams.get(name))!.valueOf())); + } }); await navigator.clipboard.writeText(shareableUrl.toString()); From 1898c1a05b74a9a320bef95ecad236b5b7ce1c56 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 20 Aug 2024 12:21:53 +0200 Subject: [PATCH 52/76] feat(SceneExploreDiffFlameGraphs): Slight improvement --- .../SceneExploreDiffFlameGraphs.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx index 3f1e61f3..6e82e5ff 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx @@ -95,6 +95,8 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase Date: Tue, 20 Aug 2024 16:26:24 +0200 Subject: [PATCH 53/76] refactor(*): Various fixes & code improvements --- .../SceneExploreDiffFlameGraph.tsx} | 14 +-- .../SceneComparePanel/SceneComparePanel.tsx | 1 + .../SceneTimeRangeWithAnnotations.ts | 98 ++++++++++--------- .../domain/RangeAnnotation.ts | 0 .../SwitchTimeRangeSelectionModeAction.tsx | 0 .../domain/evaluateTimeRange.ts | 0 .../events/EventAnnotationTimeRangeChanged.ts | 0 .../EventSwitchTimerangeSelectionMode.ts | 0 .../SceneComparePanel/domain/parseUrlParam.ts | 0 .../buildCompareTimeSeriesQueryRunner.ts | 0 .../domain/behaviours/syncYAxis.ts | 0 .../infrastructure/diffProfileApiClient.ts | 0 .../infrastructure/useFetchDiffProfile.ts | 0 .../SceneGroupByLabels/SceneGroupByLabels.tsx | 11 +-- .../SceneProfilesExplorer.tsx | 45 ++++++--- .../domain/events/EventViewDiffFlameGraph.tsx | 7 ++ src/plugin.json | 16 --- 17 files changed, 105 insertions(+), 87 deletions(-) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx => SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx} (95%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/components/SceneComparePanel/SceneComparePanel.tsx (99%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts (88%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/components/SceneComparePanel/domain/RangeAnnotation.ts (100%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx (100%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/components/SceneComparePanel/domain/evaluateTimeRange.ts (100%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts (100%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts (100%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/components/SceneComparePanel/domain/parseUrlParam.ts (100%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/components/SceneComparePanel/infrastructure/buildCompareTimeSeriesQueryRunner.ts (100%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/domain/behaviours/syncYAxis.ts (100%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/infrastructure/diffProfileApiClient.ts (100%) rename src/pages/ProfilesExplorerView/components/{SceneExploreDiffFlameGraphs => SceneExploreDiffFlameGraph}/infrastructure/useFetchDiffProfile.ts (100%) create mode 100644 src/pages/ProfilesExplorerView/domain/events/EventViewDiffFlameGraph.tsx diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx similarity index 95% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx index 6e82e5ff..e8dd7d85 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx @@ -21,12 +21,12 @@ import { SceneComparePanel } from './components/SceneComparePanel/SceneComparePa import { syncYAxis } from './domain/behaviours/syncYAxis'; import { useFetchDiffProfile } from './infrastructure/useFetchDiffProfile'; -interface SceneExploreDiffFlameGraphsState extends SceneObjectState { +interface SceneExploreDiffFlameGraphState extends SceneObjectState { baselinePanel: SceneComparePanel; comparisonPanel: SceneComparePanel; } -export class SceneExploreDiffFlameGraphs extends SceneObjectBase { +export class SceneExploreDiffFlameGraph extends SceneObjectBase { constructor() { const baselinePanel = new SceneComparePanel({ target: CompareTarget.BASELINE, @@ -37,7 +37,7 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase { + useSceneExploreDiffFlameGraph = () => { const { baselinePanel, comparisonPanel } = this.useState(); const baselineTimeRange = baselinePanel.getDiffTimeRange()?.state.annotationTimeRange as TimeRange; @@ -129,10 +129,10 @@ export class SceneExploreDiffFlameGraphs extends SceneObjectBase) { + static Component({ model }: SceneComponentProps) { const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks - const { data } = model.useSceneExploreDiffFlameGraphs(); + const { data } = model.useSceneExploreDiffFlameGraph(); const { baselinePanel, comparisonPanel, diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx similarity index 99% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx index 2b10a909..f15c9882 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx @@ -81,6 +81,7 @@ export class SceneComparePanel extends SceneObjectBase { timeseriesPanel.state.body.setState({ $timeRange: new SceneTimeRangeWithAnnotations({ + key: `${target}-annotation-timerange`, mode: TimeRangeWithAnnotationsMode.ANNOTATIONS, annotationColor: target === CompareTarget.BASELINE ? BASELINE_COLORS.OVERLAY.toString() : COMPARISON_COLORS.OVERLAY.toString(), diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts similarity index 88% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index a62bbd8c..3bdd4070 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -27,17 +27,25 @@ interface SceneTimeRangeWithAnnotationsState extends SceneTimeRangeState { mode: TimeRangeWithAnnotationsMode; } +const TIMERANGE_NIL = { + from: dateTime(0), + to: dateTime(0), + raw: { from: '', to: '' }, +}; + export class SceneTimeRangeWithAnnotations extends SceneObjectBase implements SceneTimeRangeLike { - protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['aFrom', 'aTo'] }); + protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['diffFrom', 'diffTo'] }); constructor({ + key, annotationColor, annotationTitle, mode, }: { + key: string; annotationColor: SceneTimeRangeWithAnnotationsState['annotationColor']; annotationTitle: SceneTimeRangeWithAnnotationsState['annotationTitle']; mode: SceneTimeRangeWithAnnotationsState['mode']; @@ -45,13 +53,10 @@ export class SceneTimeRangeWithAnnotations const defaultTimeRange = getDefaultTimeRange(); super({ + key, // temporary values, they will be updated in onActivate ...defaultTimeRange, - annotationTimeRange: { - from: dateTime(0), - to: dateTime(0), - raw: { from: '', to: '' }, - }, + annotationTimeRange: TIMERANGE_NIL, annotationColor, annotationTitle, mode, @@ -60,55 +65,18 @@ export class SceneTimeRangeWithAnnotations this.addActivationHandler(this.onActivate.bind(this)); } - getUrlState() { - const { annotationTimeRange } = this.state; - - return { - aFrom: - typeof annotationTimeRange.raw.from === 'string' - ? annotationTimeRange.raw.from - : annotationTimeRange.raw.from.toISOString(), - aTo: - typeof annotationTimeRange.raw.to === 'string' - ? annotationTimeRange.raw.to - : annotationTimeRange.raw.to.toISOString(), - }; - } - - // eslint-disable-next-line sonarjs/cognitive-complexity - updateFromUrl(values: SceneObjectUrlValues) { - const { aFrom, aTo } = values; - - if (!aTo && !aFrom) { - return; - } - - const { annotationTimeRange } = this.state; - - this.setState({ - annotationTimeRange: evaluateTimeRange( - parseUrlParam(aFrom) ?? annotationTimeRange.from, - parseUrlParam(aTo) ?? annotationTimeRange.to, - this.getTimeZone(), - this.state.fiscalYearStartMonth, - this.state.UNSAFE_nowDelay - ), - }); - } - onActivate() { const ancestorTimeRangeObject = this.getAncestorTimeRange(); this.setState({ ...ancestorTimeRangeObject.state, + key: this.state.key, }); this._subs.add(ancestorTimeRangeObject.subscribeToState((newState) => this.setState(newState))); - const { $data } = this.getTimeseries().state; - this._subs.add( - $data?.subscribeToState((newState, prevState) => { + this.getTimeseries().state.$data?.subscribeToState((newState, prevState) => { if (!newState.data) { return; } @@ -119,6 +87,7 @@ export class SceneTimeRangeWithAnnotations return; } + // ensure we retain the previous annotations, if they exist if (!newState.data.annotations?.length && prevState.data?.annotations?.length) { newState.data.annotations = prevState.data.annotations; } @@ -176,6 +145,45 @@ export class SceneTimeRangeWithAnnotations }); } + nullifyAnnotationTimeRange() { + this.setState({ annotationTimeRange: TIMERANGE_NIL }); + } + + getUrlState() { + const { annotationTimeRange } = this.state; + + return { + diffFrom: + typeof annotationTimeRange.raw.from === 'string' + ? annotationTimeRange.raw.from + : annotationTimeRange.raw.from.toISOString(), + diffTo: + typeof annotationTimeRange.raw.to === 'string' + ? annotationTimeRange.raw.to + : annotationTimeRange.raw.to.toISOString(), + }; + } + + updateFromUrl(values: SceneObjectUrlValues) { + const { diffFrom, diffTo } = values; + + if (!diffTo && !diffFrom) { + return; + } + + const { annotationTimeRange } = this.state; + + this.setState({ + annotationTimeRange: evaluateTimeRange( + parseUrlParam(diffFrom) ?? annotationTimeRange.from, + parseUrlParam(diffTo) ?? annotationTimeRange.to, + this.getTimeZone(), + this.state.fiscalYearStartMonth, + this.state.UNSAFE_nowDelay + ), + }); + } + onTimeRangeChange(timeRange: TimeRange): void { const { mode } = this.state; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/RangeAnnotation.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/RangeAnnotation.ts similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/RangeAnnotation.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/RangeAnnotation.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/actions/SwitchTimeRangeSelectionModeAction.tsx diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/evaluateTimeRange.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/evaluateTimeRange.ts similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/evaluateTimeRange.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/evaluateTimeRange.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/events/EventSwitchTimerangeSelectionMode.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/parseUrlParam.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/parseUrlParam.ts similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/domain/parseUrlParam.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/parseUrlParam.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/infrastructure/buildCompareTimeSeriesQueryRunner.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/infrastructure/buildCompareTimeSeriesQueryRunner.ts similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/components/SceneComparePanel/infrastructure/buildCompareTimeSeriesQueryRunner.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/infrastructure/buildCompareTimeSeriesQueryRunner.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/behaviours/syncYAxis.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/domain/behaviours/syncYAxis.ts similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/domain/behaviours/syncYAxis.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/domain/behaviours/syncYAxis.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/diffProfileApiClient.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/diffProfileApiClient.ts similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/diffProfileApiClient.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/diffProfileApiClient.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/useFetchDiffProfile.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts similarity index 100% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraphs/infrastructure/useFetchDiffProfile.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index ca7435b7..cb7bc03a 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -12,6 +12,7 @@ import { Stack, useStyles2 } from '@grafana/ui'; import { reportInteraction } from '@shared/domain/reportInteraction'; import React, { useMemo } from 'react'; import { Unsubscribable } from 'rxjs'; +import { EventViewDiffFlameGraph } from 'src/pages/ProfilesExplorerView/domain/events/EventViewDiffFlameGraph'; import { FavAction } from '../../../../domain/actions/FavAction'; import { SelectAction } from '../../../../domain/actions/SelectAction'; @@ -38,7 +39,7 @@ import { GridItemData } from '../../../SceneByVariableRepeaterGrid/types/GridIte import { SceneDrawer } from '../../../SceneDrawer'; import { SceneLabelValuesBarGauge } from '../../../SceneLabelValuesBarGauge'; import { SceneLabelValuesTimeseries } from '../../../SceneLabelValuesTimeseries'; -import { ExplorationType, SceneProfilesExplorer } from '../../../SceneProfilesExplorer/SceneProfilesExplorer'; +import { SceneProfilesExplorer } from '../../../SceneProfilesExplorer/SceneProfilesExplorer'; import { SceneStatsPanel } from './components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel'; import { CompareTarget } from './components/SceneLabelValuesGrid/domain/types'; import { SceneLabelValuesGrid } from './components/SceneLabelValuesGrid/SceneLabelValuesGrid'; @@ -346,13 +347,11 @@ export class SceneGroupByLabels extends SceneObjectBase } onClickCompareButton = () => { - this.updateCompareFilters(); - reportInteraction('g_pyroscope_app_compare_link_clicked'); - ( - sceneGraph.findByKeyAndType(this, 'profiles-explorer', SceneProfilesExplorer) as SceneProfilesExplorer - ).setExplorationType({ type: ExplorationType.DIFF_FLAME_GRAPH }); + this.updateCompareFilters(); + + this.publishEvent(new EventViewDiffFlameGraph({}), true); }; onClickClearCompareButton = () => { diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 234970e3..61367349 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -27,6 +27,7 @@ import { SceneExploreAllServices } from '../../components/SceneExploreAllService import { SceneExploreFavorites } from '../../components/SceneExploreFavorites/SceneExploreFavorites'; import { SceneExploreServiceLabels } from '../../components/SceneExploreServiceLabels/SceneExploreServiceLabels'; import { SceneExploreServiceProfileTypes } from '../../components/SceneExploreServiceProfileTypes/SceneExploreServiceProfileTypes'; +import { EventViewDiffFlameGraph } from '../../domain/events/EventViewDiffFlameGraph'; import { EventViewServiceFlameGraph } from '../../domain/events/EventViewServiceFlameGraph'; import { EventViewServiceLabels } from '../../domain/events/EventViewServiceLabels'; import { EventViewServiceProfiles } from '../../domain/events/EventViewServiceProfiles'; @@ -44,7 +45,8 @@ import { SceneNoDataSwitcher } from '../SceneByVariableRepeaterGrid/components/S import { ScenePanelTypeSwitcher } from '../SceneByVariableRepeaterGrid/components/ScenePanelTypeSwitcher'; import { SceneQuickFilter } from '../SceneByVariableRepeaterGrid/components/SceneQuickFilter'; import { GridItemData } from '../SceneByVariableRepeaterGrid/types/GridItemData'; -import { SceneExploreDiffFlameGraphs } from '../SceneExploreDiffFlameGraphs/SceneExploreDiffFlameGraphs'; +import { SceneTimeRangeWithAnnotations } from '../SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations'; +import { SceneExploreDiffFlameGraph } from '../SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph'; import { SceneExploreServiceFlameGraph } from '../SceneExploreServiceFlameGraph/SceneExploreServiceFlameGraph'; import { ExplorationTypeSelector } from './ui/ExplorationTypeSelector'; @@ -216,8 +218,16 @@ export class SceneProfilesExplorer extends SceneObjectBase { + this.setExplorationType({ + type: ExplorationType.DIFF_FLAME_GRAPH, + resetVariables: true, + }); + }); + return { unsubscribe() { + diffFlameGraphSub.unsubscribe(); flameGraphSub.unsubscribe(); labelsSub.unsubscribe(); profilesSub.unsubscribe(); @@ -261,7 +271,7 @@ export class SceneProfilesExplorer extends SceneObjectBase { - sceneGraph.findByKeyAndType(this, filterKey, FiltersVariable)?.setState({ + sceneGraph.findByKeyAndType(this, filterKey, FiltersVariable).setState({ filters: FiltersVariable.DEFAULT_VALUE, }); }); + + ['baseline-annotation-timerange', 'comparison-annotation-timerange'].forEach((timeRangeKey) => { + sceneGraph.findByKeyAndType(this, timeRangeKey, SceneTimeRangeWithAnnotations).nullifyAnnotationTimeRange(); + }); } - sceneGraph.findByKeyAndType(this, 'groupBy', GroupByVariable)?.changeValueTo(GroupByVariable.DEFAULT_VALUE); + sceneGraph.findByKeyAndType(this, 'groupBy', GroupByVariable).changeValueTo(GroupByVariable.DEFAULT_VALUE); - sceneGraph.findByKeyAndType(this, 'panel-type-switcher', ScenePanelTypeSwitcher)?.reset(); + sceneGraph.findByKeyAndType(this, 'panel-type-switcher', ScenePanelTypeSwitcher).reset(); } onClickShareLink = async () => { @@ -317,11 +334,13 @@ export class SceneProfilesExplorer extends SceneObjectBase { - if (searchParams.has(name)) { - searchParams.set(name, String(dateMath.parse(searchParams.get(name))!.valueOf())); + ['from', 'to', 'from-2', 'to-2', 'from-3', 'to-3', 'diffFrom', 'diffTo', 'diffFrom-2', 'diffTo-2'].forEach( + (name) => { + if (searchParams.has(name)) { + searchParams.set(name, String(dateMath.parse(searchParams.get(name))!.valueOf())); + } } - }); + ); await navigator.clipboard.writeText(shareableUrl.toString()); displaySuccess(['Link copied to clipboard!']); diff --git a/src/pages/ProfilesExplorerView/domain/events/EventViewDiffFlameGraph.tsx b/src/pages/ProfilesExplorerView/domain/events/EventViewDiffFlameGraph.tsx new file mode 100644 index 00000000..93b8d370 --- /dev/null +++ b/src/pages/ProfilesExplorerView/domain/events/EventViewDiffFlameGraph.tsx @@ -0,0 +1,7 @@ +import { BusEventWithPayload } from '@grafana/data'; + +export interface EventViewDiffFlameGraphPayload {} + +export class EventViewDiffFlameGraph extends BusEventWithPayload { + public static type = 'view-diff-flame-graph'; +} diff --git a/src/plugin.json b/src/plugin.json index a5ea43e7..8da77aee 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -47,22 +47,6 @@ "addToNav": true, "defaultNav": false }, - { - "type": "page", - "name": "Comparison view", - "path": "/a/%PLUGIN_ID%/comparison", - "role": "Viewer", - "addToNav": true, - "defaultNav": false - }, - { - "type": "page", - "name": "Comparison diff view", - "path": "/a/%PLUGIN_ID%/comparison-diff", - "role": "Viewer", - "addToNav": true, - "defaultNav": false - }, { "type": "page", "name": "Ad hoc view", From c248ec98aac93d3714210bb7fcce07db21da0e22 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Tue, 20 Aug 2024 18:19:48 +0200 Subject: [PATCH 54/76] chore(SceneExploreDiffFlameGraph): Add info banner --- .../SceneExploreDiffFlameGraph.tsx | 37 ++++++++++++++----- .../infrastructure/useFetchDiffProfile.ts | 3 +- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx index e8dd7d85..05868434 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx @@ -28,13 +28,9 @@ interface SceneExploreDiffFlameGraphState extends SceneObjectState { export class SceneExploreDiffFlameGraph extends SceneObjectBase { constructor() { - const baselinePanel = new SceneComparePanel({ - target: CompareTarget.BASELINE, - }); + const baselinePanel = new SceneComparePanel({ target: CompareTarget.BASELINE }); - const comparisonPanel = new SceneComparePanel({ - target: CompareTarget.COMPARISON, - }); + const comparisonPanel = new SceneComparePanel({ target: CompareTarget.COMPARISON }); super({ key: 'explore-diff-flame-graph', @@ -108,10 +104,16 @@ export class SceneExploreDiffFlameGraph extends SceneObjectBase
+ {shouldDisplayInfo && ( + + )} + {fetchProfileError && ( )} @@ -241,6 +253,11 @@ const getStyles = (theme: GrafanaTheme2) => ({ padding: ${theme.spacing(1)}; border: 1px solid ${theme.colors.border.weak}; border-radius: 2px; + + & [role='status'], + & [role='alert'] { + margin-bottom: 0; + } `, flameGraphHeaderActions: css` display: flex; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts index 716b5b2c..cd220f53 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts @@ -25,8 +25,7 @@ export function useFetchDiffProfile({ enabled: Boolean( baselineQuery && comparisonQuery && - // determining the correct left/right ranges takes time and can lead to some values being 0 - // in this case, we would send 0 values to the API, which would make the pods crash + // warning: sending zero parameters values to the API would make the pods crash // so we enable only when we have non-zero parameters values baselineTimeRange?.raw.from.valueOf() && baselineTimeRange?.raw.to.valueOf() && From 72101a652ed92d84776a671c8fead45f06bd73f1 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Wed, 21 Aug 2024 11:13:46 +0200 Subject: [PATCH 55/76] fix(*): Time ranges sync and reset --- .../SceneExploreDiffFlameGraph.tsx | 21 ++++- .../SceneComparePanel/SceneComparePanel.tsx | 27 ++++-- .../SceneTimeRangeWithAnnotations.ts | 42 ++++----- .../SceneProfilesExplorer.tsx | 92 ++++++++++--------- 4 files changed, 102 insertions(+), 80 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx index 05868434..391bddc5 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx @@ -1,6 +1,13 @@ import { css } from '@emotion/css'; import { DashboardCursorSync, GrafanaTheme2, TimeRange } from '@grafana/data'; -import { behaviors, SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { + behaviors, + SceneComponentProps, + sceneGraph, + SceneObjectBase, + SceneObjectState, + SceneTimeRangeState, +} from '@grafana/scenes'; import { Spinner, useStyles2 } from '@grafana/ui'; import { AiPanel } from '@shared/components/AiPanel/AiPanel'; import { AIButton } from '@shared/components/AiPanel/components/AIButton'; @@ -27,10 +34,16 @@ interface SceneExploreDiffFlameGraphState extends SceneObjectState { } export class SceneExploreDiffFlameGraph extends SceneObjectBase { - constructor() { - const baselinePanel = new SceneComparePanel({ target: CompareTarget.BASELINE }); + constructor({ initTimeRangeState }: { initTimeRangeState?: SceneTimeRangeState }) { + const baselinePanel = new SceneComparePanel({ + target: CompareTarget.BASELINE, + initTimeRangeState, + }); - const comparisonPanel = new SceneComparePanel({ target: CompareTarget.COMPARISON }); + const comparisonPanel = new SceneComparePanel({ + target: CompareTarget.COMPARISON, + initTimeRangeState, + }); super({ key: 'explore-diff-flame-graph', diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx index f15c9882..293d4081 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx @@ -10,6 +10,7 @@ import { SceneTimePicker, SceneTimeRange, SceneTimeRangeLike, + SceneTimeRangeState, VariableDependencyConfig, } from '@grafana/scenes'; import { InlineLabel, useStyles2 } from '@grafana/ui'; @@ -42,8 +43,8 @@ export interface SceneComparePanelState extends SceneObjectState { color: string; timePicker: SceneTimePicker; refreshPicker: SceneRefreshPicker; + $timeRange: SceneTimeRange; timeseriesPanel?: SceneLabelValuesTimeseries; - $timeRange?: SceneTimeRange; } export class SceneComparePanel extends SceneObjectBase { @@ -54,28 +55,34 @@ export class SceneComparePanel extends SceneObjectBase { }, }); - constructor({ target }: { target: SceneComparePanelState['target'] }) { + constructor({ + target, + initTimeRangeState, + }: { + target: SceneComparePanelState['target']; + initTimeRangeState?: SceneTimeRangeState; + }) { super({ - key: `diff-panel-${target}`, + key: `${target}-panel`, target, title: target === CompareTarget.BASELINE ? 'Baseline' : 'Comparison', filterKey: target === CompareTarget.BASELINE ? 'filtersBaseline' : 'filtersComparison', color: target === CompareTarget.BASELINE ? BASELINE_COLORS.COLOR.toString() : COMPARISON_COLORS.COLOR.toString(), - $timeRange: undefined, + $timeRange: new SceneTimeRange({ key: `${target}-panel-timerange` }), timePicker: new SceneTimePicker({ isOnCanvas: true }), refreshPicker: new SceneRefreshPicker({ isOnCanvas: true }), timeseriesPanel: undefined, }); - this.addActivationHandler(this.onActivate.bind(this)); + this.addActivationHandler(this.onActivate.bind(this, initTimeRangeState)); } - onActivate() { - const { title, target } = this.state; + onActivate(initTimeRangeState?: SceneTimeRangeState) { + const { title, target, $timeRange } = this.state; - const { from, to, value } = this.getAncestorTimeRange().state; - - this.setState({ $timeRange: new SceneTimeRange({ from, to, value }) }); + if (initTimeRangeState) { + $timeRange.setState(initTimeRangeState); + } const timeseriesPanel = this.buildTimeSeriesPanel(); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index 3bdd4070..bb27645f 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -8,8 +8,8 @@ import { SceneTimeRangeState, VizPanel, } from '@grafana/scenes'; +import { omit } from 'lodash'; -import { getDefaultTimeRange } from '../../../../../domain/getDefaultTimeRange'; import { evaluateTimeRange } from '../domain/evaluateTimeRange'; import { EventAnnotationTimeRangeChanged } from '../domain/events/EventAnnotationTimeRangeChanged'; import { parseUrlParam } from '../domain/parseUrlParam'; @@ -22,9 +22,9 @@ export enum TimeRangeWithAnnotationsMode { interface SceneTimeRangeWithAnnotationsState extends SceneTimeRangeState { annotationTimeRange: TimeRange; + mode: TimeRangeWithAnnotationsMode; annotationColor: string; annotationTitle: string; - mode: TimeRangeWithAnnotationsMode; } const TIMERANGE_NIL = { @@ -39,41 +39,33 @@ export class SceneTimeRangeWithAnnotations { protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['diffFrom', 'diffTo'] }); - constructor({ - key, - annotationColor, - annotationTitle, - mode, - }: { + constructor(options: { key: string; - annotationColor: SceneTimeRangeWithAnnotationsState['annotationColor']; - annotationTitle: SceneTimeRangeWithAnnotationsState['annotationTitle']; - mode: SceneTimeRangeWithAnnotationsState['mode']; + mode: TimeRangeWithAnnotationsMode; + annotationColor: string; + annotationTitle: string; }) { - const defaultTimeRange = getDefaultTimeRange(); - super({ - key, - // temporary values, they will be updated in onActivate - ...defaultTimeRange, + from: TIMERANGE_NIL.raw.from, + to: TIMERANGE_NIL.raw.to, + value: TIMERANGE_NIL, annotationTimeRange: TIMERANGE_NIL, - annotationColor, - annotationTitle, - mode, + ...options, }); this.addActivationHandler(this.onActivate.bind(this)); } onActivate() { - const ancestorTimeRangeObject = this.getAncestorTimeRange(); + const ancestorTimeRange = this.getAncestorTimeRange(); - this.setState({ - ...ancestorTimeRangeObject.state, - key: this.state.key, - }); + this.setState(omit(ancestorTimeRange.state, 'key')); - this._subs.add(ancestorTimeRangeObject.subscribeToState((newState) => this.setState(newState))); + this._subs.add( + ancestorTimeRange.subscribeToState((newState) => { + this.setState(omit(newState, 'key')); + }) + ); this._subs.add( this.getTimeseries().state.$data?.subscribeToState((newState, prevState) => { diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 61367349..b910d1b1 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -21,6 +21,7 @@ import { IconButton, InlineLabel, useStyles2 } from '@grafana/ui'; import { displayError, displaySuccess } from '@shared/domain/displayStatus'; import { reportInteraction } from '@shared/domain/reportInteraction'; import { VersionInfoTooltip } from '@shared/ui/VersionInfoTooltip'; +import { omit } from 'lodash'; import React from 'react'; import { SceneExploreAllServices } from '../../components/SceneExploreAllServices/SceneExploreAllServices'; @@ -51,6 +52,7 @@ import { SceneExploreServiceFlameGraph } from '../SceneExploreServiceFlameGraph/ import { ExplorationTypeSelector } from './ui/ExplorationTypeSelector'; export interface SceneProfilesExplorerState extends Partial { + $timeRange: SceneTimeRange; $variables: SceneVariableSet; gridControls: Array; explorationType?: ExplorationType; @@ -128,7 +130,7 @@ export class SceneProfilesExplorer extends SceneObjectBase { this.setExplorationType({ type: ExplorationType.PROFILE_TYPES, - resetVariables: true, + comesFromUserAction: true, item: event.payload.item, }); }); @@ -205,7 +207,7 @@ export class SceneProfilesExplorer extends SceneObjectBase { this.setExplorationType({ type: ExplorationType.LABELS, - resetVariables: true, + comesFromUserAction: true, item: event.payload.item, }); }); @@ -213,7 +215,7 @@ export class SceneProfilesExplorer extends SceneObjectBase { this.setExplorationType({ type: ExplorationType.FLAME_GRAPH, - resetVariables: true, + comesFromUserAction: true, item: event.payload.item, }); }); @@ -221,7 +223,7 @@ export class SceneProfilesExplorer extends SceneObjectBase { this.setExplorationType({ type: ExplorationType.DIFF_FLAME_GRAPH, - resetVariables: true, + comesFromUserAction: true, }); }); @@ -237,24 +239,59 @@ export class SceneProfilesExplorer extends SceneObjectBase { + sceneGraph.findByKeyAndType(this, filterKey, FiltersVariable).setState({ + filters: FiltersVariable.DEFAULT_VALUE, + }); + }); + + const { from, to, value } = this.state.$timeRange.state; + + ['baseline-panel-timerange', 'comparison-panel-timerange'].forEach((timeRangeKey) => { + sceneGraph.findByKeyAndType(this, timeRangeKey, SceneTimeRange).setState({ from, to, value }); + }); + + ['baseline-annotation-timerange', 'comparison-annotation-timerange'].forEach((timeRangeKey) => { + sceneGraph.findByKeyAndType(this, timeRangeKey, SceneTimeRangeWithAnnotations).nullifyAnnotationTimeRange(); + }); + } + } + + buildBodyScene(explorationType: ExplorationType, item?: GridItemData, comesFromUserAction?: boolean) { let primary; switch (explorationType) { @@ -271,7 +308,9 @@ export class SceneProfilesExplorer extends SceneObjectBase { - sceneGraph.findByKeyAndType(this, filterKey, FiltersVariable).setState({ - filters: FiltersVariable.DEFAULT_VALUE, - }); - }); - - ['baseline-annotation-timerange', 'comparison-annotation-timerange'].forEach((timeRangeKey) => { - sceneGraph.findByKeyAndType(this, timeRangeKey, SceneTimeRangeWithAnnotations).nullifyAnnotationTimeRange(); - }); - } - - sceneGraph.findByKeyAndType(this, 'groupBy', GroupByVariable).changeValueTo(GroupByVariable.DEFAULT_VALUE); - - sceneGraph.findByKeyAndType(this, 'panel-type-switcher', ScenePanelTypeSwitcher).reset(); - } - onClickShareLink = async () => { try { const shareableUrl = new URL(window.location.toString()); From 1b1fe2cf9d6b93bb97f762a68aa83d9334af1d82 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Wed, 21 Aug 2024 15:14:41 +0200 Subject: [PATCH 56/76] refactor(*): Simplify code --- .../SceneExploreDiffFlameGraph.tsx | 47 +++----- .../SceneComparePanel/SceneComparePanel.tsx | 111 +++++++++--------- .../SceneTimeRangeWithAnnotations.ts | 3 - .../events/EventAnnotationTimeRangeChanged.ts | 9 -- .../SceneProfilesExplorer.tsx | 5 +- 5 files changed, 72 insertions(+), 103 deletions(-) delete mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx index 391bddc5..10426d7d 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx @@ -1,13 +1,6 @@ import { css } from '@emotion/css'; -import { DashboardCursorSync, GrafanaTheme2, TimeRange } from '@grafana/data'; -import { - behaviors, - SceneComponentProps, - sceneGraph, - SceneObjectBase, - SceneObjectState, - SceneTimeRangeState, -} from '@grafana/scenes'; +import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data'; +import { behaviors, SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { Spinner, useStyles2 } from '@grafana/ui'; import { AiPanel } from '@shared/components/AiPanel/AiPanel'; import { AIButton } from '@shared/components/AiPanel/components/AIButton'; @@ -23,7 +16,6 @@ import { useBuildPyroscopeQuery } from '../../domain/useBuildPyroscopeQuery'; import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; import { CompareTarget } from '../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; -import { EventAnnotationTimeRangeChanged } from './components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged'; import { SceneComparePanel } from './components/SceneComparePanel/SceneComparePanel'; import { syncYAxis } from './domain/behaviours/syncYAxis'; import { useFetchDiffProfile } from './infrastructure/useFetchDiffProfile'; @@ -34,15 +26,15 @@ interface SceneExploreDiffFlameGraphState extends SceneObjectState { } export class SceneExploreDiffFlameGraph extends SceneObjectBase { - constructor({ initTimeRangeState }: { initTimeRangeState?: SceneTimeRangeState }) { + constructor({ useAncestorTimeRange }: { useAncestorTimeRange: boolean }) { const baselinePanel = new SceneComparePanel({ target: CompareTarget.BASELINE, - initTimeRangeState, + useAncestorTimeRange, }); const comparisonPanel = new SceneComparePanel({ target: CompareTarget.COMPARISON, - initTimeRangeState, + useAncestorTimeRange, }); super({ @@ -67,17 +59,6 @@ export class SceneExploreDiffFlameGraph extends SceneObjectBase { - this.forceRender(); - }) - ); - return () => { profileMetricVariable.setState({ query: ProfileMetricVariable.QUERY_DEFAULT }); profileMetricVariable.update(true); @@ -98,10 +79,10 @@ export class SceneExploreDiffFlameGraph extends SceneObjectBase { const { baselinePanel, comparisonPanel } = this.useState(); - const baselineTimeRange = baselinePanel.getDiffTimeRange()?.state.annotationTimeRange as TimeRange; + const { annotationTimeRange: baselineTimeRange } = baselinePanel.useDiffTimeRange(); const baselineQuery = useBuildPyroscopeQuery(this, 'filtersBaseline'); - const comparisonTimeRange = comparisonPanel.getDiffTimeRange()?.state.annotationTimeRange as TimeRange; + const { annotationTimeRange: comparisonTimeRange } = comparisonPanel.useDiffTimeRange(); const comparisonQuery = useBuildPyroscopeQuery(this, 'filtersComparison'); const { settings, error: fetchSettingsError } = useFetchPluginSettings(); @@ -122,10 +103,10 @@ export class SceneExploreDiffFlameGraph extends SceneObjectBase) { - const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks + const styles = useStyles2(getStyles); const { data } = model.useSceneExploreDiffFlameGraph(); const { @@ -162,9 +144,8 @@ export class SceneExploreDiffFlameGraph extends SceneObjectBase { if (isLoading) { sidePanel.close(); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx index 293d4081..23469fe7 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx @@ -10,11 +10,11 @@ import { SceneTimePicker, SceneTimeRange, SceneTimeRangeLike, - SceneTimeRangeState, VariableDependencyConfig, } from '@grafana/scenes'; import { InlineLabel, useStyles2 } from '@grafana/ui'; import { getProfileMetric, ProfileMetricId } from '@shared/infrastructure/profile-metrics/getProfileMetric'; +import { omit } from 'lodash'; import React from 'react'; import { BASELINE_COLORS, COMPARISON_COLORS } from '../../../../../../pages/ComparisonView/ui/colors'; @@ -38,65 +38,58 @@ import { buildCompareTimeSeriesQueryRunner } from './infrastructure/buildCompare export interface SceneComparePanelState extends SceneObjectState { target: CompareTarget; - title: string; filterKey: 'filtersBaseline' | 'filtersComparison'; + title: string; color: string; timePicker: SceneTimePicker; refreshPicker: SceneRefreshPicker; $timeRange: SceneTimeRange; - timeseriesPanel?: SceneLabelValuesTimeseries; + timeseriesPanel: SceneLabelValuesTimeseries; } export class SceneComparePanel extends SceneObjectBase { protected _variableDependency = new VariableDependencyConfig(this, { variableNames: ['profileMetricId'], onVariableUpdateCompleted: () => { - this.state.timeseriesPanel?.updateTitle(this.buildTimeseriesTitle()); + this.state.timeseriesPanel.updateTitle(this.buildTimeseriesTitle()); }, }); constructor({ target, - initTimeRangeState, + useAncestorTimeRange, }: { target: SceneComparePanelState['target']; - initTimeRangeState?: SceneTimeRangeState; + useAncestorTimeRange: boolean; }) { + const filterKey = target === CompareTarget.BASELINE ? 'filtersBaseline' : 'filtersComparison'; + const title = target === CompareTarget.BASELINE ? 'Baseline' : 'Comparison'; + const color = + target === CompareTarget.BASELINE ? BASELINE_COLORS.COLOR.toString() : COMPARISON_COLORS.COLOR.toString(); + super({ key: `${target}-panel`, target, - title: target === CompareTarget.BASELINE ? 'Baseline' : 'Comparison', - filterKey: target === CompareTarget.BASELINE ? 'filtersBaseline' : 'filtersComparison', - color: target === CompareTarget.BASELINE ? BASELINE_COLORS.COLOR.toString() : COMPARISON_COLORS.COLOR.toString(), + filterKey, + title, + color, $timeRange: new SceneTimeRange({ key: `${target}-panel-timerange` }), timePicker: new SceneTimePicker({ isOnCanvas: true }), refreshPicker: new SceneRefreshPicker({ isOnCanvas: true }), - timeseriesPanel: undefined, + timeseriesPanel: SceneComparePanel.buildTimeSeriesPanel({ target, filterKey, title, color }), }); - this.addActivationHandler(this.onActivate.bind(this, initTimeRangeState)); + this.addActivationHandler(this.onActivate.bind(this, useAncestorTimeRange)); } - onActivate(initTimeRangeState?: SceneTimeRangeState) { - const { title, target, $timeRange } = this.state; + onActivate(useAncestorTimeRange: boolean) { + const { $timeRange, timeseriesPanel } = this.state; - if (initTimeRangeState) { - $timeRange.setState(initTimeRangeState); + if (useAncestorTimeRange) { + $timeRange.setState(omit(this.getAncestorTimeRange().state, 'key')); } - const timeseriesPanel = this.buildTimeSeriesPanel(); - - timeseriesPanel.state.body.setState({ - $timeRange: new SceneTimeRangeWithAnnotations({ - key: `${target}-annotation-timerange`, - mode: TimeRangeWithAnnotationsMode.ANNOTATIONS, - annotationColor: - target === CompareTarget.BASELINE ? BASELINE_COLORS.OVERLAY.toString() : COMPARISON_COLORS.OVERLAY.toString(), - annotationTitle: `${title} time range`, - }), - }); - - this.setState({ timeseriesPanel: timeseriesPanel }); + timeseriesPanel.updateTitle(this.buildTimeseriesTitle()); const eventSub = this.subscribeToEvents(); @@ -105,33 +98,12 @@ export class SceneComparePanel extends SceneObjectBase { }; } - protected getAncestorTimeRange(): SceneTimeRangeLike { - if (!this.parent || !this.parent.parent) { - throw new Error(typeof this + ' must be used within $timeRange scope'); - } - - return sceneGraph.getTimeRange(this.parent.parent); - } - - subscribeToEvents() { - return this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { - (this.state.timeseriesPanel?.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).setState({ - mode: - event.payload.mode === TimerangeSelectionMode.FLAMEGRAPH - ? TimeRangeWithAnnotationsMode.ANNOTATIONS - : TimeRangeWithAnnotationsMode.DEFAULT, - }); - }); - } - - buildTimeSeriesPanel() { - const { target, filterKey, title, color } = this.state; - - return new SceneLabelValuesTimeseries({ + static buildTimeSeriesPanel({ target, filterKey, title, color }: any) { + const timeseriesPanel = new SceneLabelValuesTimeseries({ item: { index: 0, value: target, - label: this.buildTimeseriesTitle(), + label: '', queryRunnerParams: {}, }, data: new SceneDataTransformer({ @@ -161,6 +133,37 @@ export class SceneComparePanel extends SceneObjectBase { }), headerActions: () => [new SwitchTimeRangeSelectionModeAction()], }); + + timeseriesPanel.state.body.setState({ + $timeRange: new SceneTimeRangeWithAnnotations({ + key: `${target}-annotation-timerange`, + mode: TimeRangeWithAnnotationsMode.ANNOTATIONS, + annotationColor: + target === CompareTarget.BASELINE ? BASELINE_COLORS.OVERLAY.toString() : COMPARISON_COLORS.OVERLAY.toString(), + annotationTitle: `${title} time range`, + }), + }); + + return timeseriesPanel; + } + + protected getAncestorTimeRange(): SceneTimeRangeLike { + if (!this.parent || !this.parent.parent) { + throw new Error(typeof this + ' must be used within $timeRange scope'); + } + + return sceneGraph.getTimeRange(this.parent.parent); + } + + subscribeToEvents() { + return this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { + (this.state.timeseriesPanel.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).setState({ + mode: + event.payload.mode === TimerangeSelectionMode.FLAMEGRAPH + ? TimeRangeWithAnnotationsMode.ANNOTATIONS + : TimeRangeWithAnnotationsMode.DEFAULT, + }); + }); } buildTimeseriesTitle() { @@ -169,8 +172,8 @@ export class SceneComparePanel extends SceneObjectBase { return description || getProfileMetricLabel(profileMetricId); } - getDiffTimeRange() { - return this.state.timeseriesPanel?.state.body.state.$timeRange as SceneTimeRangeWithAnnotations; + useDiffTimeRange() { + return (this.state.timeseriesPanel.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).useState(); } public static Component = ({ model }: SceneComponentProps) => { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index bb27645f..22990cfe 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -11,7 +11,6 @@ import { import { omit } from 'lodash'; import { evaluateTimeRange } from '../domain/evaluateTimeRange'; -import { EventAnnotationTimeRangeChanged } from '../domain/events/EventAnnotationTimeRangeChanged'; import { parseUrlParam } from '../domain/parseUrlParam'; import { RangeAnnotation } from '../domain/RangeAnnotation'; @@ -190,8 +189,6 @@ export class SceneTimeRangeWithAnnotations }); this.updateTimeseriesAnnotation(); - - this.publishEvent(new EventAnnotationTimeRangeChanged({ timeRange }), true); } onTimeZoneChange(timeZone: string): void { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts deleted file mode 100644 index 7d26ef7d..00000000 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/events/EventAnnotationTimeRangeChanged.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BusEventWithPayload, TimeRange } from '@grafana/data'; - -export interface EventAnnotationTimeRangeChangedPayload { - timeRange: TimeRange; -} - -export class EventAnnotationTimeRangeChanged extends BusEventWithPayload { - public static type = 'annotation-timerange-changed'; -} diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index b910d1b1..083579b9 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -21,7 +21,6 @@ import { IconButton, InlineLabel, useStyles2 } from '@grafana/ui'; import { displayError, displaySuccess } from '@shared/domain/displayStatus'; import { reportInteraction } from '@shared/domain/reportInteraction'; import { VersionInfoTooltip } from '@shared/ui/VersionInfoTooltip'; -import { omit } from 'lodash'; import React from 'react'; import { SceneExploreAllServices } from '../../components/SceneExploreAllServices/SceneExploreAllServices'; @@ -308,9 +307,7 @@ export class SceneProfilesExplorer extends SceneObjectBase Date: Wed, 21 Aug 2024 15:19:07 +0200 Subject: [PATCH 57/76] chore: Remove comment --- .../infrastructure/useFetchDiffProfile.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/pages/ComparisonView/components/FlameGraphContainer/infrastructure/useFetchDiffProfile.ts b/src/pages/ComparisonView/components/FlameGraphContainer/infrastructure/useFetchDiffProfile.ts index 69ebfe39..7a110836 100644 --- a/src/pages/ComparisonView/components/FlameGraphContainer/infrastructure/useFetchDiffProfile.ts +++ b/src/pages/ComparisonView/components/FlameGraphContainer/infrastructure/useFetchDiffProfile.ts @@ -12,16 +12,6 @@ export function useFetchDiffProfile({ disabled }: FetchParams) { const [maxNodes] = useMaxNodesFromUrl(); const { left, right } = useLeftRightParamsFromUrl(); - // console.log( - // '*** useFetchDiffProfile', - // left.query, - // right.query, - // left.timeRange.raw.from.valueOf(), - // left.timeRange.raw.to.valueOf(), - // right.timeRange.raw.from.valueOf(), - // right.timeRange.raw.to.valueOf() - // ); - const { isFetching, error, data, refetch } = useQuery({ // for UX: keep previous data while fetching -> profile does not re-render with empty panels when refreshing placeholderData: (previousData) => previousData, From 321ca9ca5fecc01cf8108884dbe8246e5e1b7941 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Wed, 21 Aug 2024 17:00:27 +0200 Subject: [PATCH 58/76] fix(*): Early return when no series have been received --- .../components/SceneComparePanel/SceneComparePanel.tsx | 2 ++ .../components/SceneTimeRangeWithAnnotations.ts | 7 +++---- .../components/SceneLabelValuesBarGauge.tsx | 4 ++++ .../components/SceneLabelValuesTimeseries.tsx | 4 ++++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx index 23469fe7..5f6b7653 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx @@ -157,6 +157,8 @@ export class SceneComparePanel extends SceneObjectBase { subscribeToEvents() { return this.subscribeToEvent(EventSwitchTimerangeSelectionMode, (event) => { + // this triggers a timeseries request to the API + // TODO: caching? (this.state.timeseriesPanel.state.body.state.$timeRange as SceneTimeRangeWithAnnotations).setState({ mode: event.payload.mode === TimerangeSelectionMode.FLAMEGRAPH diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index 22990cfe..d2f759c6 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -183,10 +183,9 @@ export class SceneTimeRangeWithAnnotations return; } - // note: this update causes a timeseries query to be made to the API - this.setState({ - annotationTimeRange: timeRange, - }); + // this triggers a timeseries request to the API + // TODO: caching? + this.setState({ annotationTimeRange: timeRange }); this.updateTimeseriesAnnotation(); } diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx index f252236e..de6421d5 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx @@ -58,6 +58,10 @@ export class SceneLabelValuesBarGauge extends SceneObjectBase Date: Thu, 22 Aug 2024 10:16:27 +0200 Subject: [PATCH 59/76] feat(Labels): Allow same item to be selected as baseline and comparison --- .../SceneGroupByLabels/SceneGroupByLabels.tsx | 8 +-- .../components/SceneLabelValuePanel.tsx | 7 +- .../SceneStatsPanel/SceneStatsPanel.tsx | 23 +++---- .../SceneStatsPanel/ui/StatsPanel.tsx | 65 +++++++++++-------- 4 files changed, 52 insertions(+), 51 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index cb7bc03a..d237310a 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -294,12 +294,6 @@ export class SceneGroupByLabels extends SceneObjectBase selectForCompare(compareTarget: CompareTarget, item: GridItemData) { const compare = new Map(this.state.compare); - const otherTarget = compareTarget === CompareTarget.BASELINE ? CompareTarget.COMPARISON : CompareTarget.BASELINE; - - if (compare.get(otherTarget)?.value === item.value) { - compare.delete(otherTarget); - } - compare.set(compareTarget, item); this.setState({ compare }); @@ -317,7 +311,7 @@ export class SceneGroupByLabels extends SceneObjectBase // TODO: optimize if needed // we can remove the loop if we clear the current selection in the UI before updating the compare map (see selectForCompare() and onClickClearCompareButton()) for (const panel of statsPanels) { - panel.setCompareTargetValue(baselineItem, comparisonItem); + panel.updateCompareActions(baselineItem, comparisonItem); } } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx index 11bff4e2..3c8473fb 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx @@ -66,14 +66,15 @@ export class SceneLabelValuePanel extends SceneObjectBase) { const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks const { statsPanel, timeseriesPanel } = model.useState(); - const { compareTargetValue } = statsPanel.useState(); + const { actionChecks } = statsPanel.useState(); + const isSelected = actionChecks[0] || actionChecks[1]; return (
-
+
-
+
diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx index 3929a02a..99b22808 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx @@ -15,7 +15,7 @@ export type ItemStats = { interface SceneStatsPanelState extends SceneObjectState { item: GridItemData; itemStats?: ItemStats; - compareTargetValue?: CompareTarget; + actionChecks: boolean[]; } export class SceneStatsPanel extends SceneObjectBase { @@ -25,7 +25,7 @@ export class SceneStatsPanel extends SceneObjectBase { super({ item, itemStats: undefined, - compareTargetValue: undefined, + actionChecks: [false, false], }); this.addActivationHandler(this.onActivate.bind(this)); @@ -34,20 +34,15 @@ export class SceneStatsPanel extends SceneObjectBase { onActivate() { const compare = sceneGraph.findByKeyAndType(this, 'group-by-labels', SceneGroupByLabels).getCompare(); - this.setCompareTargetValue(compare.get(CompareTarget.BASELINE), compare.get(CompareTarget.COMPARISON)); + this.updateCompareActions(compare.get(CompareTarget.BASELINE), compare.get(CompareTarget.COMPARISON)); } - setCompareTargetValue(baselineItem?: GridItemData, comparisonItem?: GridItemData) { + updateCompareActions(baselineItem?: GridItemData, comparisonItem?: GridItemData) { const { item } = this.state; - let compareTargetValue; - if (baselineItem?.value === item.value) { - compareTargetValue = CompareTarget.BASELINE; - } else if (comparisonItem?.value === item.value) { - compareTargetValue = CompareTarget.COMPARISON; - } - - this.setState({ compareTargetValue }); + this.setState({ + actionChecks: [baselineItem?.value === item.value, comparisonItem?.value === item.value], + }); } onChangeCompareTarget = (compareTarget: CompareTarget) => { @@ -69,13 +64,13 @@ export class SceneStatsPanel extends SceneObjectBase { } static Component({ model }: SceneComponentProps) { - const { item, itemStats, compareTargetValue } = model.useState(); + const { item, itemStats, actionChecks } = model.useState(); return ( ); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx index 3ce74ff2..c57fb306 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx @@ -1,6 +1,6 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { getValueFormat, GrafanaTheme2 } from '@grafana/data'; -import { RadioButtonGroup, Spinner, useStyles2 } from '@grafana/ui'; +import { Checkbox, Spinner, useStyles2 } from '@grafana/ui'; import React, { useMemo } from 'react'; import { GridItemData } from '../../../../../../../../../components/SceneByVariableRepeaterGrid/types/GridItemData'; @@ -11,11 +11,11 @@ import { ItemStats } from '../SceneStatsPanel'; export type StatsPanelProps = { item: GridItemData; itemStats?: ItemStats; - compareTargetValue?: CompareTarget; + actionChecks: boolean[]; onChangeCompareTarget: (compareTarget: CompareTarget) => void; }; -export function StatsPanel({ item, itemStats, compareTargetValue, onChangeCompareTarget }: StatsPanelProps) { +export function StatsPanel({ item, itemStats, actionChecks, onChangeCompareTarget }: StatsPanelProps) { const styles = useStyles2(getStyles); const { index, value } = item; @@ -38,17 +38,15 @@ export function StatsPanel({ item, itemStats, compareTargetValue, onChangeCompar { label: 'Baseline', value: CompareTarget.BASELINE, - description: - compareTargetValue !== CompareTarget.BASELINE ? `Click to select "${value}" as baseline for comparison` : '', + description: !actionChecks[0] ? `Click to select "${value}" as baseline for comparison` : '', }, { label: 'Comparison', value: CompareTarget.COMPARISON, - description: - compareTargetValue !== CompareTarget.COMPARISON ? `Click to select "${value}" as target for comparison` : '', + description: !actionChecks[1] ? `Click to select "${value}" as target for comparison` : '', }, ], - [compareTargetValue, value] + [actionChecks, value] ); return ( @@ -57,14 +55,17 @@ export function StatsPanel({ item, itemStats, compareTargetValue, onChangeCompar {total} -
- +
+ {options.map((option, i) => ( + onChangeCompareTarget(option.value)} + /> + ))}
); @@ -88,21 +89,31 @@ const getStyles = (theme: GrafanaTheme2) => ({ text-align: center; margin-top: ${theme.spacing(5)}; `, - radioButtonsGroup: css` - width: 100%; + controls: css` + display: flex; + justify-content: space-between; + gap: 4px; + font-size: 11px; + `, + + checkbox: css` + column-gap: 4px; - & > * { - flex-grow: 1 !important; + &:nth-child(2) { + & :nth-child(1) { + grid-column-start: 2; + } + & :nth-child(2) { + grid-column-start: 1; + } } - & :nth-child(1):checked + label { - color: #fff; - background-color: ${theme.colors.primary.main}; // TODO + span { + color: ${theme.colors.text.secondary}; } - & :nth-child(2):checked + label { - color: #fff; - background-color: ${theme.colors.primary.main}; // TODO + &.checked span { + color: ${theme.colors.text.primary}; } `, }); From 2cf21946d4b3c42b9e73b161b17cdf8386b3d4a4 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 22 Aug 2024 11:20:56 +0200 Subject: [PATCH 60/76] fix(*): Support multiple data sources + reset annotations --- .../SceneExploreDiffFlameGraph.tsx | 8 ++++ .../SceneComparePanel/SceneComparePanel.tsx | 2 +- .../SceneTimeRangeWithAnnotations.ts | 11 ++++- ...leApiClient.ts => DiffProfileApiClient.ts} | 11 +++-- .../infrastructure/useFetchDiffProfile.ts | 43 +++++++++++++------ .../components/SceneMainServiceTimeseries.tsx | 2 +- 6 files changed, 57 insertions(+), 20 deletions(-) rename src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/{diffProfileApiClient.ts => DiffProfileApiClient.ts} (85%) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx index 10426d7d..114dc5b2 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx @@ -14,6 +14,7 @@ import React, { useEffect } from 'react'; import { useBuildPyroscopeQuery } from '../../domain/useBuildPyroscopeQuery'; import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; +import { ProfilesDataSourceVariable } from '../../domain/variables/ProfilesDataSourceVariable'; import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; import { CompareTarget } from '../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; import { SceneComparePanel } from './components/SceneComparePanel/SceneComparePanel'; @@ -87,11 +88,18 @@ export class SceneExploreDiffFlameGraph extends SceneObjectBase { protected _variableDependency = new VariableDependencyConfig(this, { variableNames: ['profileMetricId'], - onVariableUpdateCompleted: () => { + onReferencedVariableValueChanged: () => { this.state.timeseriesPanel.updateTitle(this.buildTimeseriesTitle()); }, }); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index d2f759c6..3f1db8e5 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -6,6 +6,7 @@ import { SceneObjectUrlValues, SceneTimeRangeLike, SceneTimeRangeState, + VariableDependencyConfig, VizPanel, } from '@grafana/scenes'; import { omit } from 'lodash'; @@ -36,6 +37,14 @@ export class SceneTimeRangeWithAnnotations extends SceneObjectBase implements SceneTimeRangeLike { + protected _variableDependency = new VariableDependencyConfig(this, { + variableNames: ['dataSource', 'serviceName'], + onReferencedVariableValueChanged: () => { + this.nullifyAnnotationTimeRange(); + this.updateTimeseriesAnnotation(); + }, + }); + protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['diffFrom', 'diffTo'] }); constructor(options: { @@ -108,7 +117,7 @@ export class SceneTimeRangeWithAnnotations } } - protected updateTimeseriesAnnotation() { + updateTimeseriesAnnotation() { const { annotationTimeRange, annotationColor, annotationTitle } = this.state; const { $data } = this.getTimeseries().state; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/diffProfileApiClient.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/DiffProfileApiClient.ts similarity index 85% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/diffProfileApiClient.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/DiffProfileApiClient.ts index 7a0c6611..cff84177 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/diffProfileApiClient.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/DiffProfileApiClient.ts @@ -1,7 +1,8 @@ import { dateTimeParse, TimeRange } from '@grafana/data'; -import { ApiClient } from '@shared/infrastructure/http/ApiClient'; import { FlamebearerProfile } from '@shared/types/FlamebearerProfile'; +import { DataSourceProxyClient } from '../../../infrastructure/series/http/DataSourceProxyClient'; + type DiffProfileResponse = FlamebearerProfile; type GetParams = { @@ -12,7 +13,11 @@ type GetParams = { maxNodes: number | null; }; -class DiffProfileApiClient extends ApiClient { +export class DiffProfileApiClient extends DataSourceProxyClient { + constructor(options: { dataSourceUid: string }) { + super(options); + } + async get(params: GetParams): Promise { // /pyroscope/render-diff requests: timerange can be YYYYDDMM, Unix time, Unix time in ms (unix * 1000) const leftFrom = Number(dateTimeParse(params.leftTimeRange.raw.from).unix()) * 1000; @@ -40,5 +45,3 @@ class DiffProfileApiClient extends ApiClient { return json; } } - -export const diffProfileApiClient = new DiffProfileApiClient(); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts index cd220f53..cfe15392 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts @@ -1,10 +1,15 @@ import { TimeRange } from '@grafana/data'; import { useMaxNodesFromUrl } from '@shared/domain/url-params/useMaxNodesFromUrl'; +import { queryClient } from '@shared/infrastructure/react-query/queryClient'; import { useQuery } from '@tanstack/react-query'; +import { useEffect } from 'react'; -import { diffProfileApiClient } from './diffProfileApiClient'; +import { DataSourceProxyClientBuilder } from '../../../infrastructure/series/http/DataSourceProxyClientBuilder'; +import { DiffProfileApiClient } from './DiffProfileApiClient'; type FetchParams = { + dataSourceUid: string; + serviceName: string; baselineTimeRange: TimeRange; baselineQuery: string; comparisonTimeRange: TimeRange; @@ -12,6 +17,8 @@ type FetchParams = { }; export function useFetchDiffProfile({ + dataSourceUid, + serviceName, baselineTimeRange, baselineQuery, comparisonTimeRange, @@ -19,6 +26,27 @@ export function useFetchDiffProfile({ }: FetchParams) { const [maxNodes] = useMaxNodesFromUrl(); + // we use "raw" to cache relative time ranges between renders, so that only refetch() will trigger a new query + const queryKey = [ + 'diff-profile', + baselineQuery, + baselineTimeRange?.raw.from.toString(), + baselineTimeRange?.raw.to.toString(), + comparisonQuery, + comparisonTimeRange?.raw.from.toString(), + comparisonTimeRange?.raw.to.toString(), + maxNodes, + ]; + + useEffect(() => { + queryClient.setQueryData(queryKey, { profile: null }); + }, [dataSourceUid, serviceName]); // eslint-disable-line react-hooks/exhaustive-deps + + const diffProfileApiClient = DataSourceProxyClientBuilder.build( + dataSourceUid, + DiffProfileApiClient + ) as DiffProfileApiClient; + const { isFetching, error, data, refetch } = useQuery({ // for UX: keep previous data while fetching -> profile does not re-render with empty panels when refreshing placeholderData: (previousData) => previousData, @@ -32,18 +60,7 @@ export function useFetchDiffProfile({ comparisonTimeRange?.raw.from.valueOf() && comparisonTimeRange?.raw.to.valueOf() ), - // we use "raw" to cache relative time ranges between renders, so that only refetch() will trigger a new query - // eslint-disable-next-line @tanstack/query/exhaustive-deps - queryKey: [ - 'diff-profile', - baselineQuery, - baselineTimeRange?.raw.from.toString(), - baselineTimeRange?.raw.to.toString(), - comparisonQuery, - comparisonTimeRange?.raw.from.toString(), - comparisonTimeRange?.raw.to.toString(), - maxNodes, - ], + queryKey, // eslint-disable-line @tanstack/query/exhaustive-deps queryFn: () => { diffProfileApiClient.abort(); diff --git a/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx b/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx index 648f16ce..9defa96d 100644 --- a/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneMainServiceTimeseries.tsx @@ -25,7 +25,7 @@ export class SceneMainServiceTimeseries extends SceneObjectBase { + onReferencedVariableValueChanged: () => { this.state.body?.updateTitle(this.buildTitle()); }, }); From 54eb49b922c667bc9cf47d01b98b7a109e7726ca Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 22 Aug 2024 11:34:06 +0200 Subject: [PATCH 61/76] refactor(ExplorationTypeSelector): DRY --- .../ui/ExplorationTypeSelector.tsx | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/ui/ExplorationTypeSelector.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/ui/ExplorationTypeSelector.tsx index 782e39e5..7a226353 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/ui/ExplorationTypeSelector.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/ui/ExplorationTypeSelector.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { SelectableValue } from '@grafana/data'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Button, Icon, InlineLabel, useStyles2 } from '@grafana/ui'; import { noOp } from '@shared/domain/noOp'; import React from 'react'; @@ -18,7 +18,7 @@ export function ExplorationTypeSelector({ options, value, onChange }: Exploratio Exploration
- {options.slice(0, options.length - 2).map((option, i) => { + {options.map((option, i) => { const isActive = value === option.value; return ( <> @@ -35,29 +35,7 @@ export function ExplorationTypeSelector({ options, value, onChange }: Exploratio {option.label} - {i < options.length - 3 && } - - ); - })} -
    
- {/* eslint-disable-next-line sonarjs/cognitive-complexity */} - {options.slice(-2).map((option) => { - const isActive = value === option.value; - return ( - <> - -
  
+ {i < options.length - 3 ? : <>  } ); })} @@ -66,7 +44,7 @@ export function ExplorationTypeSelector({ options, value, onChange }: Exploratio ); } -const getStyles = () => ({ +const getStyles = (theme: GrafanaTheme2) => ({ explorationTypeContainer: css` display: flex; align-items: center; @@ -80,6 +58,10 @@ const getStyles = () => ({ button: css` height: 30px; line-height: 30px; + + &:last-child { + margin-left: ${theme.spacing(1)}; + } `, active: css` &:hover { From 2867c1b2253592ba29a3206b163d80c56563b9b2 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 22 Aug 2024 12:02:09 +0200 Subject: [PATCH 62/76] refactor(syncYAxis): Simplify code --- .../domain/behaviours/syncYAxis.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/domain/behaviours/syncYAxis.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/domain/behaviours/syncYAxis.ts index 1a4d7db1..2fe419e7 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/domain/behaviours/syncYAxis.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/domain/behaviours/syncYAxis.ts @@ -1,4 +1,3 @@ -import { DataFrame } from '@grafana/data'; import { sceneGraph, SceneObject, SceneObjectState, VizPanel } from '@grafana/scenes'; import { cloneDeep, merge } from 'lodash'; @@ -17,7 +16,7 @@ export function syncYAxis() { return; } - maxima.set(series[0].refId as string, findMaxValue(series)); + maxima.set(series[0].refId as string, Math.max(...series[0].fields[1].values)); updateTimeseriesAxis(vizPanel, Math.max(...maxima.values())); }); @@ -28,18 +27,6 @@ export function syncYAxis() { }; } -function findMaxValue(series: DataFrame[]) { - let max = -1; - - for (const value of series[0].fields[1].values) { - if (value > max) { - max = value; - } - } - - return max; -} - function updateTimeseriesAxis(vizPanel: SceneObject, max: number) { // findAllObjects searches down the full scene graph const timeseries = sceneGraph.findAllObjects( From 07d9ad555fa5d784c2d3097102cb1f887d8aa1f0 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 22 Aug 2024 12:06:10 +0200 Subject: [PATCH 63/76] chore: Add missing comments --- .../components/SceneComparePanel/domain/evaluateTimeRange.ts | 2 ++ .../components/SceneComparePanel/domain/parseUrlParam.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/evaluateTimeRange.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/evaluateTimeRange.ts index 2984ebaa..c4d747fc 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/evaluateTimeRange.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/evaluateTimeRange.ts @@ -1,6 +1,8 @@ import { dateMath, DateTime, TimeRange } from '@grafana/data'; import { TimeZone } from '@grafana/schema'; +/* Copied from https://github.com/grafana/scenes/blob/main/packages/scenes/src/utils/evaluateTimeRange.ts */ + export function evaluateTimeRange( from: string | DateTime, to: string | DateTime, diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/parseUrlParam.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/parseUrlParam.ts index 43d19108..fbf83664 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/parseUrlParam.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/domain/parseUrlParam.ts @@ -3,6 +3,8 @@ import { SceneObjectUrlValue } from '@grafana/scenes'; const INTERVAL_STRING_REGEX = /^\d+[yYmMsSwWhHdD]$/; +/* Copied from https://github.com/grafana/scenes/blob/main/packages/scenes/src/utils/parseUrlParam.ts */ + // eslint-disable-next-line sonarjs/cognitive-complexity export function parseUrlParam(value: SceneObjectUrlValue): string | null { if (typeof value !== 'string') { From d932ed6651de754325408ac8f71078e59301d3a5 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 22 Aug 2024 12:24:40 +0200 Subject: [PATCH 64/76] chore(StatsPanel): minor UI tweaks --- .../components/SceneStatsPanel/ui/StatsPanel.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx index c57fb306..61ed6d99 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx @@ -92,12 +92,11 @@ const getStyles = (theme: GrafanaTheme2) => ({ controls: css` display: flex; justify-content: space-between; - gap: 4px; font-size: 11px; `, checkbox: css` - column-gap: 4px; + column-gap: 3px; &:nth-child(2) { & :nth-child(1) { From 35de4c34a5bc51a9dd8630539f0db3245d2d0592 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 22 Aug 2024 12:27:40 +0200 Subject: [PATCH 65/76] fix(SceneLabelValuePanel): Fix after main rebase --- .../components/SceneLabelValuePanel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx index f731fd03..3cc5f6d9 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx @@ -70,11 +70,11 @@ export class SceneLabelValuePanel extends SceneObjectBase -
+
+
-
+
From ea5b2ff260b57a5774e19ceedb466ac9ff68310b Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 22 Aug 2024 12:30:04 +0200 Subject: [PATCH 66/76] fix(CompareActions): Enable toggle on checkbox --- .../components/SceneGroupByLabels/SceneGroupByLabels.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx index d237310a..28fb1389 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/SceneGroupByLabels.tsx @@ -294,7 +294,11 @@ export class SceneGroupByLabels extends SceneObjectBase selectForCompare(compareTarget: CompareTarget, item: GridItemData) { const compare = new Map(this.state.compare); - compare.set(compareTarget, item); + if (compare.get(compareTarget)?.value === item.value) { + compare.delete(compareTarget); + } else { + compare.set(compareTarget, item); + } this.setState({ compare }); From f9791ffabfd37281901b699dd92a718d72febeb6 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 22 Aug 2024 19:25:16 +0200 Subject: [PATCH 67/76] fix(*): Small UI fixes --- .../SceneLabelValuesGrid.tsx | 8 +++++- .../SceneStatsPanel/SceneStatsPanel.tsx | 2 +- .../SceneStatsPanel/ui/StatsPanel.tsx | 4 ++- .../SceneGroupByLabels/ui/CompareActions.tsx | 25 +++++++++++++------ 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index b9435cf5..5e03880c 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -418,6 +418,12 @@ export class SceneLabelValuesGrid extends SceneObjectBase) { const { body, isLoading } = model.useState(); - return isLoading ? : ; + return isLoading ? ( + + ) : ( +
+ +
+ ); } } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx index 99b22808..b0f744e2 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx @@ -19,7 +19,7 @@ interface SceneStatsPanelState extends SceneObjectState { } export class SceneStatsPanel extends SceneObjectBase { - static WIDTH_IN_PIXELS = 180; + static WIDTH_IN_PIXELS = 186; constructor({ item }: { item: GridItemData }) { super({ diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx index 61ed6d99..a395cb31 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx @@ -94,7 +94,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ justify-content: space-between; font-size: 11px; `, - checkbox: css` column-gap: 3px; @@ -110,6 +109,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ span { color: ${theme.colors.text.secondary}; } + span:hover { + color: ${theme.colors.text.primary}; + } &.checked span { color: ${theme.colors.text.primary}; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx index 3c6fda75..38608399 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/ui/CompareActions.tsx @@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Button, useStyles2 } from '@grafana/ui'; import { noOp } from '@shared/domain/noOp'; -import React from 'react'; +import React, { useMemo } from 'react'; import { SceneStatsPanel } from '../components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel'; import { CompareTarget } from '../components/SceneLabelValuesGrid/domain/types'; @@ -19,6 +19,21 @@ export function CompareActions({ compare, onClickCompare, onClickClear }: Compar const compareIsDisabled = compare.size < 2; const hasSelection = compare.size > 0; + const tooltip = useMemo(() => { + if (compare.size === 2) { + return `Compare "${compare.get(CompareTarget.BASELINE)?.label}" vs "${ + compare.get(CompareTarget.COMPARISON)?.label + }"`; + } + if (compare.size === 0) { + return 'Select both a baseline and a comparison panel to compare their flame graphs'; + } + + return compare.has(CompareTarget.BASELINE) + ? `Select another panel to compare against "${compare.get(CompareTarget.BASELINE)?.label}"` + : `Select another panel to compare against "${compare.get(CompareTarget.COMPARISON)?.label}"`; + }, [compare]); + return (
From 309c6f01f02f35610b5879f1b3011de60b6f7b4e Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Thu, 22 Aug 2024 19:47:04 +0200 Subject: [PATCH 68/76] refactor(*): Create SceneDiffFlameGraph --- .../SceneExploreDiffFlameGraph.tsx | 201 ++---------------- .../SceneComparePanel/SceneComparePanel.tsx | 2 +- .../SceneDiffFlameGraph.tsx | 199 +++++++++++++++++ .../infrastructure/DiffProfileApiClient.ts | 2 +- .../infrastructure/useFetchDiffProfile.ts | 2 +- .../SceneFlameGraph.tsx | 3 +- 6 files changed, 223 insertions(+), 186 deletions(-) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/SceneDiffFlameGraph.tsx rename src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/{ => components/SceneDiffFlameGraph}/infrastructure/DiffProfileApiClient.ts (93%) rename src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/{ => components/SceneDiffFlameGraph}/infrastructure/useFetchDiffProfile.ts (95%) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx index 114dc5b2..e302d374 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/SceneExploreDiffFlameGraph.tsx @@ -1,47 +1,34 @@ import { css } from '@emotion/css'; import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data'; import { behaviors, SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; -import { Spinner, useStyles2 } from '@grafana/ui'; -import { AiPanel } from '@shared/components/AiPanel/AiPanel'; -import { AIButton } from '@shared/components/AiPanel/components/AIButton'; -import { FlameGraph } from '@shared/components/FlameGraph/FlameGraph'; -import { displayWarning } from '@shared/domain/displayStatus'; -import { useToggleSidePanel } from '@shared/domain/useToggleSidePanel'; -import { useFetchPluginSettings } from '@shared/infrastructure/settings/useFetchPluginSettings'; -import { FlamebearerProfile } from '@shared/types/FlamebearerProfile'; -import { InlineBanner } from '@shared/ui/InlineBanner'; -import React, { useEffect } from 'react'; +import { useStyles2 } from '@grafana/ui'; +import React from 'react'; -import { useBuildPyroscopeQuery } from '../../domain/useBuildPyroscopeQuery'; import { ProfileMetricVariable } from '../../domain/variables/ProfileMetricVariable'; -import { ProfilesDataSourceVariable } from '../../domain/variables/ProfilesDataSourceVariable'; import { ServiceNameVariable } from '../../domain/variables/ServiceNameVariable'; import { CompareTarget } from '../SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/domain/types'; import { SceneComparePanel } from './components/SceneComparePanel/SceneComparePanel'; +import { SceneDiffFlameGraph } from './components/SceneDiffFlameGraph/SceneDiffFlameGraph'; import { syncYAxis } from './domain/behaviours/syncYAxis'; -import { useFetchDiffProfile } from './infrastructure/useFetchDiffProfile'; interface SceneExploreDiffFlameGraphState extends SceneObjectState { baselinePanel: SceneComparePanel; comparisonPanel: SceneComparePanel; + body: SceneDiffFlameGraph; } export class SceneExploreDiffFlameGraph extends SceneObjectBase { constructor({ useAncestorTimeRange }: { useAncestorTimeRange: boolean }) { - const baselinePanel = new SceneComparePanel({ - target: CompareTarget.BASELINE, - useAncestorTimeRange, - }); - - const comparisonPanel = new SceneComparePanel({ - target: CompareTarget.COMPARISON, - useAncestorTimeRange, - }); - super({ key: 'explore-diff-flame-graph', - baselinePanel, - comparisonPanel, + baselinePanel: new SceneComparePanel({ + target: CompareTarget.BASELINE, + useAncestorTimeRange, + }), + comparisonPanel: new SceneComparePanel({ + target: CompareTarget.COMPARISON, + useAncestorTimeRange, + }), $behaviors: [ new behaviors.CursorSync({ key: 'metricCrosshairSync', @@ -49,6 +36,7 @@ export class SceneExploreDiffFlameGraph extends SceneObjectBase { - const { baselinePanel, comparisonPanel } = this.useState(); + useDiffTimeRanges = () => { + const { baselinePanel, comparisonPanel } = this.state; const { annotationTimeRange: baselineTimeRange } = baselinePanel.useDiffTimeRange(); - const baselineQuery = useBuildPyroscopeQuery(this, 'filtersBaseline'); - const { annotationTimeRange: comparisonTimeRange } = comparisonPanel.useDiffTimeRange(); - const comparisonQuery = useBuildPyroscopeQuery(this, 'filtersComparison'); - const { settings, error: fetchSettingsError } = useFetchPluginSettings(); - - const dataSourceUid = sceneGraph.findByKeyAndType(this, 'dataSource', ProfilesDataSourceVariable).useState() - .value as string; - const serviceName = sceneGraph.findByKeyAndType(this, 'serviceName', ServiceNameVariable).useState() - .value as string; - - const { - isFetching, - error: fetchProfileError, - profile, - } = useFetchDiffProfile({ - dataSourceUid, - serviceName, + return { baselineTimeRange, - baselineQuery, comparisonTimeRange, - comparisonQuery, - }); - - const noProfileDataAvailable = !isFetching && !fetchProfileError && profile?.flamebearer.numTicks === 0; - const shouldDisplayFlamegraph = Boolean(!fetchProfileError && !noProfileDataAvailable && profile); - const shouldDisplayInfo = !Boolean( - baselineQuery && - comparisonQuery && - baselineTimeRange.raw.from.valueOf() && - baselineTimeRange.raw.to.valueOf() && - comparisonTimeRange.raw.from.valueOf() && - comparisonTimeRange.raw.to.valueOf() - ); - - return { - data: { - baselinePanel, - comparisonPanel, - isLoading: isFetching, - fetchProfileError, - noProfileDataAvailable, - shouldDisplayFlamegraph, - shouldDisplayInfo, - profile, - settings, - fetchSettingsError, - }, - actions: {}, }; }; - /* eslint-disable react-hooks/rules-of-hooks */ static Component({ model }: SceneComponentProps) { - const styles = useStyles2(getStyles); - - const { data } = model.useSceneExploreDiffFlameGraph(); - const { - baselinePanel, - comparisonPanel, - isLoading, - fetchProfileError, - shouldDisplayInfo, - shouldDisplayFlamegraph, - noProfileDataAvailable, - profile, - fetchSettingsError, - settings, - } = data; + const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks - const sidePanel = useToggleSidePanel(); - - useEffect(() => { - if (isLoading) { - sidePanel.close(); - } - }, [isLoading, sidePanel]); - - if (fetchSettingsError) { - displayWarning([ - 'Error while retrieving the plugin settings!', - 'Some features might not work as expected (e.g. flamegraph export options). Please try to reload the page, sorry for the inconvenience.', - ]); - } + const { baselinePanel, comparisonPanel, body } = model.useState(); return (
@@ -174,65 +89,13 @@ export class SceneExploreDiffFlameGraph extends SceneObjectBase
-
-
- {shouldDisplayInfo && ( - - )} - - {fetchProfileError && ( - - )} - - {noProfileDataAvailable && ( - - )} - -
- {isLoading && } - - {shouldDisplayFlamegraph && ( - { - sidePanel.open('ai'); - }} - disabled={isLoading || noProfileDataAvailable || sidePanel.isOpen('ai')} - interactionName="g_pyroscope_app_explain_flamegraph_clicked" - > - Explain Flame Graph - - )} -
- - {shouldDisplayFlamegraph && ( - - )} -
- - {sidePanel.isOpen('ai') && } -
+
); } } const getStyles = (theme: GrafanaTheme2) => ({ - flex: css` - display: flex; - `, container: css` width: 100%; display: flex; @@ -248,30 +111,4 @@ const getStyles = (theme: GrafanaTheme2) => ({ flex: 1 1 0; } `, - flameGraphPanel: css` - min-width: 0; - flex-grow: 1; - width: 100%; - padding: ${theme.spacing(1)}; - border: 1px solid ${theme.colors.border.weak}; - border-radius: 2px; - - & [role='status'], - & [role='alert'] { - margin-bottom: 0; - } - `, - flameGraphHeaderActions: css` - display: flex; - align-items: flex-start; - - & > button { - margin-left: auto; - } - `, - sidePanel: css` - flex: 1 0 50%; - margin-left: 8px; - max-width: calc(50% - 4px); - `, }); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx index 69d91f2a..6fc94011 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx @@ -17,7 +17,7 @@ import { getProfileMetric, ProfileMetricId } from '@shared/infrastructure/profil import { omit } from 'lodash'; import React from 'react'; -import { BASELINE_COLORS, COMPARISON_COLORS } from '../../../../../../pages/ComparisonView/ui/colors'; +import { BASELINE_COLORS, COMPARISON_COLORS } from '../../../../../ComparisonView/ui/colors'; import { FiltersVariable } from '../../../../domain/variables/FiltersVariable/FiltersVariable'; import { getSceneVariableValue } from '../../../../helpers/getSceneVariableValue'; import { getSeriesStatsValue } from '../../../../infrastructure/helpers/getSeriesStatsValue'; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/SceneDiffFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/SceneDiffFlameGraph.tsx new file mode 100644 index 00000000..be987003 --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/SceneDiffFlameGraph.tsx @@ -0,0 +1,199 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { Spinner, useStyles2 } from '@grafana/ui'; +import { AiPanel } from '@shared/components/AiPanel/AiPanel'; +import { AIButton } from '@shared/components/AiPanel/components/AIButton'; +import { FlameGraph } from '@shared/components/FlameGraph/FlameGraph'; +import { displayWarning } from '@shared/domain/displayStatus'; +import { useToggleSidePanel } from '@shared/domain/useToggleSidePanel'; +import { getProfileMetric, ProfileMetricId } from '@shared/infrastructure/profile-metrics/getProfileMetric'; +import { useFetchPluginSettings } from '@shared/infrastructure/settings/useFetchPluginSettings'; +import { DomainHookReturnValue } from '@shared/types/DomainHookReturnValue'; +import { FlamebearerProfile } from '@shared/types/FlamebearerProfile'; +import { InlineBanner } from '@shared/ui/InlineBanner'; +import { Panel } from '@shared/ui/Panel/Panel'; +import React, { useEffect, useMemo } from 'react'; + +import { useBuildPyroscopeQuery } from '../../../../domain/useBuildPyroscopeQuery'; +import { ProfileMetricVariable } from '../../../../domain/variables/ProfileMetricVariable'; +import { ProfilesDataSourceVariable } from '../../../../domain/variables/ProfilesDataSourceVariable'; +import { ServiceNameVariable } from '../../../../domain/variables/ServiceNameVariable'; +import { SceneExploreDiffFlameGraph } from '../../SceneExploreDiffFlameGraph'; +import { useFetchDiffProfile } from './infrastructure/useFetchDiffProfile'; + +interface SceneDiffFlameGraphState extends SceneObjectState {} + +export class SceneDiffFlameGraph extends SceneObjectBase { + constructor() { + super({ key: 'diff-flame-graph' }); + } + + useSceneDiffFlameGraph = (): DomainHookReturnValue => { + const { baselineTimeRange, comparisonTimeRange } = (this.parent as SceneExploreDiffFlameGraph).useDiffTimeRanges(); + + const baselineQuery = useBuildPyroscopeQuery(this, 'filtersBaseline'); + const comparisonQuery = useBuildPyroscopeQuery(this, 'filtersComparison'); + + const { settings, error: fetchSettingsError } = useFetchPluginSettings(); + + const dataSourceUid = sceneGraph.findByKeyAndType(this, 'dataSource', ProfilesDataSourceVariable).useState() + .value as string; + const serviceName = sceneGraph.findByKeyAndType(this, 'serviceName', ServiceNameVariable).useState() + .value as string; + const profileMetricId = sceneGraph.findByKeyAndType(this, 'profileMetricId', ProfileMetricVariable).useState() + .value as string; + const profileMetricType = getProfileMetric(profileMetricId as ProfileMetricId).type; + + const { + isFetching, + error: fetchProfileError, + profile, + } = useFetchDiffProfile({ + dataSourceUid, + serviceName, + baselineTimeRange, + baselineQuery, + comparisonTimeRange, + comparisonQuery, + }); + + const noProfileDataAvailable = !isFetching && !fetchProfileError && profile?.flamebearer.numTicks === 0; + const shouldDisplayFlamegraph = Boolean(!fetchProfileError && !noProfileDataAvailable && profile); + const shouldDisplayInfo = !Boolean( + baselineQuery && + comparisonQuery && + baselineTimeRange.raw.from.valueOf() && + baselineTimeRange.raw.to.valueOf() && + comparisonTimeRange.raw.from.valueOf() && + comparisonTimeRange.raw.to.valueOf() + ); + + return { + data: { + title: `${serviceName} diff flame graph (${profileMetricType})`, + isLoading: isFetching, + fetchProfileError, + noProfileDataAvailable, + shouldDisplayFlamegraph, + shouldDisplayInfo, + profile: profile as FlamebearerProfile, + settings, + fetchSettingsError, + }, + actions: {}, + }; + }; + + static Component = ({ model }: SceneComponentProps) => { + const styles = useStyles2(getStyles); + + const { data } = model.useSceneDiffFlameGraph(); + const { + isLoading, + fetchProfileError, + shouldDisplayInfo, + shouldDisplayFlamegraph, + noProfileDataAvailable, + profile, + fetchSettingsError, + settings, + } = data; + + const sidePanel = useToggleSidePanel(); + + useEffect(() => { + if (data.isLoading) { + sidePanel.close(); + } + }, [data.isLoading, sidePanel]); + + if (fetchSettingsError) { + displayWarning([ + 'Error while retrieving the plugin settings!', + 'Some features might not work as expected (e.g. flamegraph export options). Please try to reload the page, sorry for the inconvenience.', + ]); + } + + const panelTitle = useMemo( + () => ( + <> + {data.title} + {data.isLoading && } + + ), + [data.isLoading, data.title, styles.spinner] + ); + + return ( +
+ sidePanel.open('ai')} + disabled={isLoading || noProfileDataAvailable || sidePanel.isOpen('ai')} + interactionName="g_pyroscope_app_explain_flamegraph_clicked" + > + Explain Flame Graph + + } + > + {shouldDisplayInfo && ( + + )} + + {fetchProfileError && ( + + )} + + {noProfileDataAvailable && ( + + )} + + {shouldDisplayFlamegraph && ( + + )} + + + {sidePanel.isOpen('ai') && } +
+ ); + }; +} + +const getStyles = (theme: GrafanaTheme2) => ({ + flex: css` + display: flex; + `, + flamegraphPanel: css` + min-width: 0; + flex-grow: 1; + `, + sidePanel: css` + flex: 1 0 50%; + margin-left: 8px; + max-width: calc(50% - 4px); + `, + spinner: css` + margin-left: ${theme.spacing(1)}; + `, + aiButton: css` + margin-top: ${theme.spacing(1)}; + `, +}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/DiffProfileApiClient.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/infrastructure/DiffProfileApiClient.ts similarity index 93% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/DiffProfileApiClient.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/infrastructure/DiffProfileApiClient.ts index cff84177..163374ca 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/DiffProfileApiClient.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/infrastructure/DiffProfileApiClient.ts @@ -1,7 +1,7 @@ import { dateTimeParse, TimeRange } from '@grafana/data'; import { FlamebearerProfile } from '@shared/types/FlamebearerProfile'; -import { DataSourceProxyClient } from '../../../infrastructure/series/http/DataSourceProxyClient'; +import { DataSourceProxyClient } from '../../../../../infrastructure/series/http/DataSourceProxyClient'; type DiffProfileResponse = FlamebearerProfile; diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/infrastructure/useFetchDiffProfile.ts similarity index 95% rename from src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts rename to src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/infrastructure/useFetchDiffProfile.ts index cfe15392..5ebaf78d 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/infrastructure/useFetchDiffProfile.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/infrastructure/useFetchDiffProfile.ts @@ -4,7 +4,7 @@ import { queryClient } from '@shared/infrastructure/react-query/queryClient'; import { useQuery } from '@tanstack/react-query'; import { useEffect } from 'react'; -import { DataSourceProxyClientBuilder } from '../../../infrastructure/series/http/DataSourceProxyClientBuilder'; +import { DataSourceProxyClientBuilder } from '../../../../../infrastructure/series/http/DataSourceProxyClientBuilder'; import { DiffProfileApiClient } from './DiffProfileApiClient'; type FetchParams = { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneFlameGraph.tsx index 8f2b7719..83c8c1d4 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceFlameGraph/SceneFlameGraph.tsx @@ -116,8 +116,9 @@ export class SceneFlameGraph extends SceneObjectBase { static Component = ({ model }: SceneComponentProps) => { const styles = useStyles2(getStyles); - const sidePanel = useToggleSidePanel(); const { data, actions } = model.useSceneFlameGraph(); + + const sidePanel = useToggleSidePanel(); const gitHubIntegration = useGitHubIntegration(sidePanel); useEffect(() => { From f76037fa0db3b3150f378edc4cd6b95757185809 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 23 Aug 2024 09:23:42 +0200 Subject: [PATCH 69/76] chore: Minor UI tweak --- .../components/SceneStatsPanel/ui/StatsPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx index a395cb31..3fd8743b 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx @@ -95,7 +95,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ font-size: 11px; `, checkbox: css` - column-gap: 3px; + column-gap: 4px; &:nth-child(2) { & :nth-child(1) { From 0ac31ee447a412332e1405ad77638222a2f04be8 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 23 Aug 2024 11:07:33 +0200 Subject: [PATCH 70/76] chore(*): Address PR comments #1 --- .../components/SceneTimeRangeWithAnnotations.ts | 6 ++++++ .../SceneProfilesExplorer/SceneProfilesExplorer.tsx | 10 +++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index 3f1db8e5..d2d1ade0 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -33,6 +33,12 @@ const TIMERANGE_NIL = { raw: { from: '', to: '' }, }; +/** + * This custom SceneTimeRange class provides the ability to draw annotations on timeseries vizualisations. + * Indeed, timeseries visualizations don't support drawing annotations by dragging (it's only supported when holding ctrl/command key) so we need to hijack the zooming event to emulate drawing. + * At the same time, the only way to hijack it is by passing custom $timeRange because TimeSeries vizualization handles zooming internally by looking for the nearest time range object. + * @see https://github.com/grafana/scenes/pull/744 + */ export class SceneTimeRangeWithAnnotations extends SceneObjectBase implements SceneTimeRangeLike diff --git a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx index 083579b9..6cbbf57c 100644 --- a/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneProfilesExplorer/SceneProfilesExplorer.tsx @@ -343,15 +343,19 @@ export class SceneProfilesExplorer extends SceneObjectBase { - if (searchParams.has(name)) { - searchParams.set(name, String(dateMath.parse(searchParams.get(name))!.valueOf())); + const value = searchParams.get(name); + if (value) { + searchParams.set(name, String(dateMath.parse(value)!.valueOf())); } } ); await navigator.clipboard.writeText(shareableUrl.toString()); displaySuccess(['Link copied to clipboard!']); - } catch {} + } catch (error) { + console.error('Error while creating the shareable link!'); + console.error(error); + } }; useProfilesExplorer = () => { From 98113a7b9f60305a27c3b5b5b7c5402745b53ce7 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 23 Aug 2024 11:18:09 +0200 Subject: [PATCH 71/76] test(EndToEnd): Add smoke test for the new diff view --- e2e/tests/explore-profiles/explore-profiles.spec.ts | 1 + .../components/SceneComparePanel/SceneComparePanel.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/e2e/tests/explore-profiles/explore-profiles.spec.ts b/e2e/tests/explore-profiles/explore-profiles.spec.ts index af719221..01f25c8e 100644 --- a/e2e/tests/explore-profiles/explore-profiles.spec.ts +++ b/e2e/tests/explore-profiles/explore-profiles.spec.ts @@ -8,6 +8,7 @@ test.describe('Explore Profiles', () => { { type: 'profiles', label: 'Profile types' }, { type: 'labels', label: 'Labels' }, { type: 'flame-graph', label: 'Flame graph' }, + { type: 'diff-flame-graph', label: 'Diff flame graph' }, { type: 'favorites', label: 'Favorites' }, ]) { test(label, async ({ exploreProfilesPage }) => { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx index 6fc94011..d25c97e4 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx @@ -18,6 +18,7 @@ import { omit } from 'lodash'; import React from 'react'; import { BASELINE_COLORS, COMPARISON_COLORS } from '../../../../../ComparisonView/ui/colors'; +import { getDefaultTimeRange } from '../../../../domain/getDefaultTimeRange'; import { FiltersVariable } from '../../../../domain/variables/FiltersVariable/FiltersVariable'; import { getSceneVariableValue } from '../../../../helpers/getSceneVariableValue'; import { getSeriesStatsValue } from '../../../../infrastructure/helpers/getSeriesStatsValue'; @@ -73,7 +74,7 @@ export class SceneComparePanel extends SceneObjectBase { filterKey, title, color, - $timeRange: new SceneTimeRange({ key: `${target}-panel-timerange` }), + $timeRange: new SceneTimeRange({ key: `${target}-panel-timerange`, ...getDefaultTimeRange() }), timePicker: new SceneTimePicker({ isOnCanvas: true }), refreshPicker: new SceneRefreshPicker({ isOnCanvas: true }), timeseriesPanel: SceneComparePanel.buildTimeSeriesPanel({ target, filterKey, title, color }), From 63b0732a1d5913645a758a9dd64eae760b8ed309 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 23 Aug 2024 12:01:36 +0200 Subject: [PATCH 72/76] chore(*): Address PR comments #2 --- .../SceneByVariableRepeaterGrid.tsx | 2 +- .../domain/behaviours/syncYAxis.ts | 8 ++++---- .../SceneLabelValuesGrid/SceneLabelValuesGrid.tsx | 2 +- .../components/SceneLabelValuePanel.tsx | 2 +- .../components/SceneLabelValuesBarGauge.tsx | 6 ++---- .../components/SceneLabelValuesTimeseries.tsx | 9 +++------ .../SceneProfilesExplorer/ui/ExplorationTypeSelector.tsx | 8 ++++++-- .../domain/events/EventTimeseriesDataReceived.ts | 2 +- 8 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx index 7506f7b3..2091703a 100644 --- a/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/SceneByVariableRepeaterGrid.tsx @@ -316,7 +316,7 @@ export class SceneByVariableRepeaterGrid extends SceneObjectBase { - if (event.payload.series.length > 0) { + if (event.payload.series?.length) { return; } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/domain/behaviours/syncYAxis.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/domain/behaviours/syncYAxis.ts index 2fe419e7..5e19c19a 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/domain/behaviours/syncYAxis.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/domain/behaviours/syncYAxis.ts @@ -8,15 +8,15 @@ export function syncYAxis() { const maxima = new Map(); const eventSub = vizPanel.subscribeToEvent(EventTimeseriesDataReceived, (event) => { - const { series } = event.payload; - const refId = series[0]?.refId; + const s = event.payload.series?.[0]; + const refId = s?.refId; if (!refId) { - console.warn('Missing refId! Cannot sync y-axis on the timeseries.', series); + console.warn('Missing refId! Cannot sync y-axis on the timeseries.', event.payload.series); return; } - maxima.set(series[0].refId as string, Math.max(...series[0].fields[1].values)); + maxima.set(s.refId as string, Math.max(...s.fields[1].values)); updateTimeseriesAxis(vizPanel, Math.max(...maxima.values())); }); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx index 5e03880c..29401065 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/SceneLabelValuesGrid.tsx @@ -339,7 +339,7 @@ export class SceneLabelValuesGrid extends SceneObjectBase { - if (!this.state.hideNoData || event.payload.series.length) { + if (!this.state.hideNoData || event.payload.series?.length) { return; } diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx index 3cc5f6d9..d2a4bf1d 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx @@ -41,7 +41,7 @@ export class SceneLabelValuePanel extends SceneObjectBase { - const [s] = event.payload.series; + const s = event.payload.series?.[0]; if (!s) { statsPanel.updateStats({ allValuesSum: 0, unit: 'short' }); diff --git a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx index de6421d5..2069bde5 100644 --- a/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneLabelValuesBarGauge.tsx @@ -58,12 +58,10 @@ export class SceneLabelValuesBarGauge extends SceneObjectBase - {i < options.length - 3 ? : <>  } + {i < options.length - 3 && } ); })} @@ -59,9 +59,13 @@ const getStyles = (theme: GrafanaTheme2) => ({ height: 30px; line-height: 30px; - &:last-child { + &:nth-last-child(2) { margin-left: ${theme.spacing(1)}; } + + &:nth-last-child(1) { + margin-left: ${theme.spacing(2)}; + } `, active: css` &:hover { diff --git a/src/pages/ProfilesExplorerView/domain/events/EventTimeseriesDataReceived.ts b/src/pages/ProfilesExplorerView/domain/events/EventTimeseriesDataReceived.ts index 5f453870..fa3e0528 100644 --- a/src/pages/ProfilesExplorerView/domain/events/EventTimeseriesDataReceived.ts +++ b/src/pages/ProfilesExplorerView/domain/events/EventTimeseriesDataReceived.ts @@ -1,7 +1,7 @@ import { BusEventWithPayload, DataFrame } from '@grafana/data'; export interface EventTimeseriesDataReceivedPayload { - series: DataFrame[]; + series?: DataFrame[]; } export class EventTimeseriesDataReceived extends BusEventWithPayload { From 5470a936cebd64aec5053a578dde71116d650eb5 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 23 Aug 2024 12:31:34 +0200 Subject: [PATCH 73/76] fix(Diff): Fix incorrect ranges + address PR comments --- .../SceneTimeRangeWithAnnotations.ts | 6 +-- .../SceneDiffFlameGraph.tsx | 28 +++++++---- .../infrastructure/DiffProfileApiClient.ts | 16 ++---- .../infrastructure/useFetchDiffProfile.ts | 50 +++++++------------ 4 files changed, 42 insertions(+), 58 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts index d2d1ade0..a8164512 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/components/SceneTimeRangeWithAnnotations.ts @@ -71,12 +71,10 @@ export class SceneTimeRangeWithAnnotations } onActivate() { - const ancestorTimeRange = this.getAncestorTimeRange(); - - this.setState(omit(ancestorTimeRange.state, 'key')); + this.setState(omit(this.getAncestorTimeRange().state, 'key')); this._subs.add( - ancestorTimeRange.subscribeToState((newState) => { + this.getAncestorTimeRange().subscribeToState((newState) => { this.setState(omit(newState, 'key')); }) ); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/SceneDiffFlameGraph.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/SceneDiffFlameGraph.tsx index be987003..7c178ae4 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/SceneDiffFlameGraph.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/SceneDiffFlameGraph.tsx @@ -45,29 +45,37 @@ export class SceneDiffFlameGraph extends SceneObjectBase { - // /pyroscope/render-diff requests: timerange can be YYYYDDMM, Unix time, Unix time in ms (unix * 1000) - const leftFrom = Number(dateTimeParse(params.leftTimeRange.raw.from).unix()) * 1000; - const leftUntil = Number(dateTimeParse(params.leftTimeRange.raw.to).unix()) * 1000; - const rightFrom = Number(dateTimeParse(params.rightTimeRange.raw.from).unix()) * 1000; - const rightUntil = Number(dateTimeParse(params.rightTimeRange.raw.to).unix()) * 1000; - const searchParams = new URLSearchParams({ leftQuery: params.leftQuery, - leftFrom: String(leftFrom), - leftUntil: String(leftUntil), + leftFrom: String(params.leftTimeRange.from.unix() * 1000), + leftUntil: String(params.leftTimeRange.to.unix() * 1000), rightQuery: params.rightQuery, - rightFrom: String(rightFrom), - rightUntil: String(rightUntil), + rightFrom: String(params.rightTimeRange.from.unix() * 1000), + rightUntil: String(params.rightTimeRange.to.unix() * 1000), }); if (params.maxNodes) { diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/infrastructure/useFetchDiffProfile.ts b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/infrastructure/useFetchDiffProfile.ts index 5ebaf78d..8bbf7e97 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/infrastructure/useFetchDiffProfile.ts +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneDiffFlameGraph/infrastructure/useFetchDiffProfile.ts @@ -1,15 +1,13 @@ import { TimeRange } from '@grafana/data'; import { useMaxNodesFromUrl } from '@shared/domain/url-params/useMaxNodesFromUrl'; -import { queryClient } from '@shared/infrastructure/react-query/queryClient'; import { useQuery } from '@tanstack/react-query'; -import { useEffect } from 'react'; import { DataSourceProxyClientBuilder } from '../../../../../infrastructure/series/http/DataSourceProxyClientBuilder'; import { DiffProfileApiClient } from './DiffProfileApiClient'; type FetchParams = { + enabled: boolean; dataSourceUid: string; - serviceName: string; baselineTimeRange: TimeRange; baselineQuery: string; comparisonTimeRange: TimeRange; @@ -17,8 +15,8 @@ type FetchParams = { }; export function useFetchDiffProfile({ + enabled, dataSourceUid, - serviceName, baselineTimeRange, baselineQuery, comparisonTimeRange, @@ -26,22 +24,6 @@ export function useFetchDiffProfile({ }: FetchParams) { const [maxNodes] = useMaxNodesFromUrl(); - // we use "raw" to cache relative time ranges between renders, so that only refetch() will trigger a new query - const queryKey = [ - 'diff-profile', - baselineQuery, - baselineTimeRange?.raw.from.toString(), - baselineTimeRange?.raw.to.toString(), - comparisonQuery, - comparisonTimeRange?.raw.from.toString(), - comparisonTimeRange?.raw.to.toString(), - maxNodes, - ]; - - useEffect(() => { - queryClient.setQueryData(queryKey, { profile: null }); - }, [dataSourceUid, serviceName]); // eslint-disable-line react-hooks/exhaustive-deps - const diffProfileApiClient = DataSourceProxyClientBuilder.build( dataSourceUid, DiffProfileApiClient @@ -50,25 +32,27 @@ export function useFetchDiffProfile({ const { isFetching, error, data, refetch } = useQuery({ // for UX: keep previous data while fetching -> profile does not re-render with empty panels when refreshing placeholderData: (previousData) => previousData, - enabled: Boolean( - baselineQuery && - comparisonQuery && - // warning: sending zero parameters values to the API would make the pods crash - // so we enable only when we have non-zero parameters values - baselineTimeRange?.raw.from.valueOf() && - baselineTimeRange?.raw.to.valueOf() && - comparisonTimeRange?.raw.from.valueOf() && - comparisonTimeRange?.raw.to.valueOf() - ), - queryKey, // eslint-disable-line @tanstack/query/exhaustive-deps + enabled, + // we use "raw" to cache relative time ranges between renders, so that only refetch() will trigger a new query + // eslint-disable-next-line @tanstack/query/exhaustive-deps + queryKey: [ + 'diff-profile', + baselineQuery, + baselineTimeRange.from.unix(), + baselineTimeRange.to.unix(), + comparisonQuery, + comparisonTimeRange.from.unix(), + comparisonTimeRange.to.unix(), + maxNodes, + ], queryFn: () => { diffProfileApiClient.abort(); const params = { leftQuery: baselineQuery, - leftTimeRange: baselineTimeRange!, + leftTimeRange: baselineTimeRange, rightQuery: comparisonQuery, - rightTimeRange: comparisonTimeRange!, + rightTimeRange: comparisonTimeRange, maxNodes, }; From 2b21e1432e315fd7fcf97a23f731e5a1e82444a1 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 23 Aug 2024 12:42:36 +0200 Subject: [PATCH 74/76] feat(Labels): Add tooltips on compare actions --- .../components/SceneLabelValuePanel.tsx | 4 +- .../SceneStatsPanel/SceneStatsPanel.tsx | 10 +- .../SceneStatsPanel/ui/CompareAction.tsx | 92 +++++++++++++++++++ .../SceneStatsPanel/ui/StatsPanel.tsx | 52 +++-------- 4 files changed, 113 insertions(+), 45 deletions(-) create mode 100644 src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/CompareAction.tsx diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx index d2a4bf1d..a2344f01 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneLabelValuePanel.tsx @@ -66,8 +66,8 @@ export class SceneLabelValuePanel extends SceneObjectBase) { const styles = useStyles2(getStyles); // eslint-disable-line react-hooks/rules-of-hooks const { statsPanel, timeseriesPanel } = model.useState(); - const { actionChecks } = statsPanel.useState(); - const isSelected = actionChecks[0] || actionChecks[1]; + const { compareActionChecks } = statsPanel.useState(); + const isSelected = compareActionChecks[0] || compareActionChecks[1]; return (
diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx index b0f744e2..26215ed3 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/SceneStatsPanel.tsx @@ -15,7 +15,7 @@ export type ItemStats = { interface SceneStatsPanelState extends SceneObjectState { item: GridItemData; itemStats?: ItemStats; - actionChecks: boolean[]; + compareActionChecks: boolean[]; } export class SceneStatsPanel extends SceneObjectBase { @@ -25,7 +25,7 @@ export class SceneStatsPanel extends SceneObjectBase { super({ item, itemStats: undefined, - actionChecks: [false, false], + compareActionChecks: [false, false], }); this.addActivationHandler(this.onActivate.bind(this)); @@ -41,7 +41,7 @@ export class SceneStatsPanel extends SceneObjectBase { const { item } = this.state; this.setState({ - actionChecks: [baselineItem?.value === item.value, comparisonItem?.value === item.value], + compareActionChecks: [baselineItem?.value === item.value, comparisonItem?.value === item.value], }); } @@ -64,13 +64,13 @@ export class SceneStatsPanel extends SceneObjectBase { } static Component({ model }: SceneComponentProps) { - const { item, itemStats, actionChecks } = model.useState(); + const { item, itemStats, compareActionChecks } = model.useState(); return ( ); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/CompareAction.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/CompareAction.tsx new file mode 100644 index 00000000..00f00fac --- /dev/null +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/CompareAction.tsx @@ -0,0 +1,92 @@ +import { css, cx } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Checkbox, Tooltip, useStyles2 } from '@grafana/ui'; +import React, { useEffect, useRef, useState } from 'react'; + +import { CompareTarget } from '../../../domain/types'; + +type CompareActionProps = { + option: { + label: string; + value: CompareTarget; + description: string; + }; + checked: boolean; + onChange: (compareTarget: CompareTarget) => void; +}; + +export function CompareAction({ option, checked, onChange }: CompareActionProps) { + const styles = useStyles2(getStyles); + + const [showTooltip, setShowTooltip] = useState(false); + const checkboxRef = useRef(null); + const label = (checkboxRef.current as HTMLInputElement)?.closest('label'); + + useEffect(() => { + if (!label || checked) { + setShowTooltip(false); + return; + } + + const onMouseEnter = () => { + setShowTooltip(true); + }; + + const onMouseLeave = () => { + setShowTooltip(false); + }; + + label.addEventListener('mouseenter', onMouseEnter); + label.addEventListener('mouseleave', onMouseLeave); + + return () => { + label.removeEventListener('mouseleave', onMouseLeave); + label.removeEventListener('mouseenter', onMouseEnter); + }; + }, [checked, label]); + + return ( + <> + + + + onChange(option.value)} + /> + + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + tooltipContent: css` + position: relative; + left: 42px; + `, + checkbox: css` + column-gap: 4px; + + &:last-child { + & :nth-child(1) { + grid-column-start: 2; + } + & :nth-child(2) { + grid-column-start: 1; + } + } + + span { + color: ${theme.colors.text.secondary}; + } + span:hover { + color: ${theme.colors.text.primary}; + } + + &.checked span { + color: ${theme.colors.text.primary}; + } + `, +}); diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx index 3fd8743b..f0393ad6 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/StatsPanel.tsx @@ -1,21 +1,22 @@ -import { css, cx } from '@emotion/css'; +import { css } from '@emotion/css'; import { getValueFormat, GrafanaTheme2 } from '@grafana/data'; -import { Checkbox, Spinner, useStyles2 } from '@grafana/ui'; +import { Spinner, useStyles2 } from '@grafana/ui'; import React, { useMemo } from 'react'; import { GridItemData } from '../../../../../../../../../components/SceneByVariableRepeaterGrid/types/GridItemData'; import { getColorByIndex } from '../../../../../../../../../helpers/getColorByIndex'; import { CompareTarget } from '../../../domain/types'; import { ItemStats } from '../SceneStatsPanel'; +import { CompareAction } from './CompareAction'; export type StatsPanelProps = { item: GridItemData; itemStats?: ItemStats; - actionChecks: boolean[]; + compareActionChecks: boolean[]; onChangeCompareTarget: (compareTarget: CompareTarget) => void; }; -export function StatsPanel({ item, itemStats, actionChecks, onChangeCompareTarget }: StatsPanelProps) { +export function StatsPanel({ item, itemStats, compareActionChecks, onChangeCompareTarget }: StatsPanelProps) { const styles = useStyles2(getStyles); const { index, value } = item; @@ -38,15 +39,15 @@ export function StatsPanel({ item, itemStats, actionChecks, onChangeCompareTarge { label: 'Baseline', value: CompareTarget.BASELINE, - description: !actionChecks[0] ? `Click to select "${value}" as baseline for comparison` : '', + description: !compareActionChecks[0] ? `Click to select "${value}" as baseline for comparison` : '', }, { label: 'Comparison', value: CompareTarget.COMPARISON, - description: !actionChecks[1] ? `Click to select "${value}" as target for comparison` : '', + description: !compareActionChecks[1] ? `Click to select "${value}" as target for comparison` : '', }, ], - [actionChecks, value] + [compareActionChecks, value] ); return ( @@ -55,15 +56,13 @@ export function StatsPanel({ item, itemStats, actionChecks, onChangeCompareTarge {total} -
+
{options.map((option, i) => ( - onChangeCompareTarget(option.value)} + option={option} + checked={compareActionChecks[i]} + onChange={onChangeCompareTarget} /> ))}
@@ -89,32 +88,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ text-align: center; margin-top: ${theme.spacing(5)}; `, - controls: css` + compareActions: css` display: flex; justify-content: space-between; font-size: 11px; `, - checkbox: css` - column-gap: 4px; - - &:nth-child(2) { - & :nth-child(1) { - grid-column-start: 2; - } - & :nth-child(2) { - grid-column-start: 1; - } - } - - span { - color: ${theme.colors.text.secondary}; - } - span:hover { - color: ${theme.colors.text.primary}; - } - - &.checked span { - color: ${theme.colors.text.primary}; - } - `, }); From c0e67409eb2f674b6964c64c4d42825d96a86ff4 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 23 Aug 2024 12:58:36 +0200 Subject: [PATCH 75/76] chore: Better naming/clarify with comment --- .../components/SceneStatsPanel/ui/CompareAction.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/CompareAction.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/CompareAction.tsx index 00f00fac..98b5ccc9 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/CompareAction.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreServiceLabels/components/SceneGroupByLabels/components/SceneLabelValuesGrid/components/SceneStatsPanel/ui/CompareAction.tsx @@ -22,6 +22,7 @@ export function CompareAction({ option, checked, onChange }: CompareActionProps) const checkboxRef = useRef(null); const label = (checkboxRef.current as HTMLInputElement)?.closest('label'); + // we write custom code to provide the tooltips because wrapping our checkbox into the component does not work useEffect(() => { if (!label || checked) { setShowTooltip(false); @@ -48,7 +49,7 @@ export function CompareAction({ option, checked, onChange }: CompareActionProps) return ( <> - + ({ - tooltipContent: css` + tooltipAnchor: css` position: relative; left: 42px; `, From 0e8ffa8bb13ae77ff43e2e433befadb68c5ab27d Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 23 Aug 2024 13:33:02 +0200 Subject: [PATCH 76/76] chore(SceneComparePanel): Minor UI fix --- .../components/SceneComparePanel/SceneComparePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx index d25c97e4..c7f21da7 100644 --- a/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx +++ b/src/pages/ProfilesExplorerView/components/SceneExploreDiffFlameGraph/components/SceneComparePanel/SceneComparePanel.tsx @@ -236,7 +236,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ timeseries: css` height: 200px; - & [data-viz-panel-key] > div { + & [data-viz-panel-key] > * { border: 0 none; } `,