From d76456d1ea09aacf69377e3acb00f737c65e7bb4 Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 3 Dec 2024 08:59:25 -0600 Subject: [PATCH 01/34] feat: rearrange menus and add new single panel above breakdowns --- src/Components/Panels/PanelMenu.tsx | 2 +- .../Breakdowns/FieldValuesBreakdownScene.tsx | 126 ++++++++++++------ .../Breakdowns/FieldsBreakdownScene.tsx | 65 +++++---- 3 files changed, 122 insertions(+), 71 deletions(-) diff --git a/src/Components/Panels/PanelMenu.tsx b/src/Components/Panels/PanelMenu.tsx index 2c88f0ab..72c91892 100644 --- a/src/Components/Panels/PanelMenu.tsx +++ b/src/Components/Panels/PanelMenu.tsx @@ -169,7 +169,7 @@ export class PanelMenu extends SceneObjectBase implements VizPan const getExploreLink = (sceneRef: SceneObject) => { const indexScene = sceneGraph.getAncestor(sceneRef, IndexScene); const $data = sceneGraph.getData(sceneRef); - let queryRunner = getQueryRunnerFromChildren($data)[0]; + let queryRunner = $data instanceof SceneQueryRunner ? $data : getQueryRunnerFromChildren($data)[0]; // If we don't have a query runner, then our panel is within a SceneCSSGridItem, we need to get the query runner from there if (!queryRunner) { diff --git a/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx index 8557a3eb..c7ae2462 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx @@ -177,62 +177,100 @@ export class FieldValuesBreakdownScene extends SceneObjectBase, + }), new SceneFlexItem({ minHeight: 300, body: PanelBuilders.timeseries().setTitle(optionValue).setMenu(new PanelMenu({})).build(), }), ], }), - new ByFrameRepeater({ - body: new SceneCSSGridLayout({ - templateColumns: FIELDS_BREAKDOWN_GRID_TEMPLATE_COLUMNS, - autoRows: '200px', - children: [ - new SceneFlexItem({ - body: new SceneReactObject({ - reactNode: , - }), + + // Grid + new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneReactObject({ + reactNode: , + }), + new SceneFlexItem({ + minHeight: 300, + body: PanelBuilders.timeseries().setTitle(optionValue).setMenu(new PanelMenu({})).build(), + }), + new SceneReactObject({ + reactNode: , + }), + new ByFrameRepeater({ + body: new SceneCSSGridLayout({ + templateColumns: FIELDS_BREAKDOWN_GRID_TEMPLATE_COLUMNS, + autoRows: '200px', + children: [ + new SceneFlexItem({ + body: new SceneReactObject({ + reactNode: , + }), + }), + ], + isLazy: true, }), - ], - isLazy: true, - }), - getLayoutChild: getFilterBreakdownValueScene( - getLabelValue, - query?.expr.includes('count_over_time') ? DrawStyle.Bars : DrawStyle.Line, - parserForThisField === 'structuredMetadata' ? VAR_METADATA : VAR_FIELDS, - sceneGraph.getAncestor(this, FieldsBreakdownScene).state.sort, - optionValue - ), - sortBy, - direction, - getFilter, + getLayoutChild: getFilterBreakdownValueScene( + getLabelValue, + query?.expr.includes('count_over_time') ? DrawStyle.Bars : DrawStyle.Line, + parserForThisField === 'structuredMetadata' ? VAR_METADATA : VAR_FIELDS, + sceneGraph.getAncestor(this, FieldsBreakdownScene).state.sort, + optionValue + ), + sortBy, + direction, + getFilter, + }), + ], }), - new ByFrameRepeater({ - body: new SceneCSSGridLayout({ - templateColumns: '1fr', - autoRows: '200px', - children: [ - new SceneFlexItem({ - body: new SceneReactObject({ - reactNode: , - }), + + // Rows + new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneReactObject({ + reactNode: , + }), + new SceneFlexItem({ + minHeight: 300, + body: PanelBuilders.timeseries().setTitle(optionValue).setMenu(new PanelMenu({})).build(), + }), + new SceneReactObject({ + reactNode: , + }), + new ByFrameRepeater({ + body: new SceneCSSGridLayout({ + templateColumns: '1fr', + autoRows: '200px', + children: [ + new SceneFlexItem({ + body: new SceneReactObject({ + reactNode: , + }), + }), + ], + isLazy: true, }), - ], - isLazy: true, - }), - getLayoutChild: getFilterBreakdownValueScene( - getLabelValue, - query?.expr.includes('count_over_time') ? DrawStyle.Bars : DrawStyle.Line, - parserForThisField === 'structuredMetadata' ? VAR_METADATA : VAR_FIELDS, - sceneGraph.getAncestor(this, FieldsBreakdownScene).state.sort, - optionValue - ), - sortBy, - direction, - getFilter, + getLayoutChild: getFilterBreakdownValueScene( + getLabelValue, + query?.expr.includes('count_over_time') ? DrawStyle.Bars : DrawStyle.Line, + parserForThisField === 'structuredMetadata' ? VAR_METADATA : VAR_FIELDS, + sceneGraph.getAncestor(this, FieldsBreakdownScene).state.sort, + optionValue + ), + sortBy, + direction, + getFilter, + }), + ], }), ], }); diff --git a/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx index 70b32bfd..dceb9f96 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx @@ -191,11 +191,12 @@ export class FieldsBreakdownScene extends SceneObjectBase { - if (layout instanceof ByFrameRepeater) { - layout.sort(event.sortBy, event.direction); - } + + const body = this.state.body; + if (body instanceof FieldValuesBreakdownScene && body.state.body instanceof LayoutSwitcher) { + body.state.body?.state.layouts.forEach((layout) => { + const byFrameRepeater = sceneGraph.findDescendents(body, ByFrameRepeater); + byFrameRepeater.forEach((r) => r.sort(event.sortBy, event.direction)); }); } reportAppInteraction( @@ -336,34 +337,46 @@ export class FieldsBreakdownScene extends SceneObjectBase) => { - const { body, loading, blockingMessage, search, sort } = model.useState(); + public static ParentMenu = ({ model }: SceneComponentProps) => { + const { body, loading } = model.useState(); + const styles = useStyles2(getStyles); const variable = getFieldGroupByVariable(model); const { options, value } = variable.useState(); + return ( +
+ {body instanceof FieldsAggregatedBreakdownScene && } + {body instanceof FieldValuesBreakdownScene && } + {!loading && options.length > 1 && ( + + )} +
+ ); + }; + public static FieldValueMenu = ({ model }: SceneComponentProps) => { + const { loading, search, sort } = model.useState(); + const styles = useStyles2(getStyles); + const variable = getFieldGroupByVariable(model); + const { value } = variable.useState(); + return ( +
+ {!loading && value !== ALL_VARIABLE_VALUE && ( + <> + + + + )} +
+ ); + }; + + public static Component = ({ model }: SceneComponentProps) => { + const { body, loading, blockingMessage } = model.useState(); const styles = useStyles2(getStyles); return (
-
- {body instanceof FieldsAggregatedBreakdownScene && } - {body instanceof FieldValuesBreakdownScene && } - {!loading && value !== ALL_VARIABLE_VALUE && ( - <> - - - - )} - {!loading && options.length > 1 && ( - - )} -
- + {body instanceof FieldsAggregatedBreakdownScene && model && }
{body && }
From 9b48adf0de708b747843ea49cfc54359031624f6 Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 3 Dec 2024 09:03:24 -0600 Subject: [PATCH 02/34] chore: fix search --- .../ServiceScene/Breakdowns/BreakdownSearchScene.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Components/ServiceScene/Breakdowns/BreakdownSearchScene.tsx b/src/Components/ServiceScene/Breakdowns/BreakdownSearchScene.tsx index bda2bd33..27284b79 100644 --- a/src/Components/ServiceScene/Breakdowns/BreakdownSearchScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/BreakdownSearchScene.tsx @@ -1,4 +1,4 @@ -import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import React, { ChangeEvent } from 'react'; import { ByFrameRepeater } from './ByFrameRepeater'; import { SearchInput } from './SearchInput'; @@ -60,8 +60,10 @@ export class BreakdownSearchScene extends SceneObjectBase { - if (child instanceof ByFrameRepeater && child.state.body.isActive) { + const byFrameRepeater = sceneGraph.findDescendents(body, ByFrameRepeater); + + byFrameRepeater?.forEach((child) => { + if (child.state.body.isActive) { child.filterByString(filter); } }); From 6518e13df169b6ab0e2e595a9f066401b4f322c2 Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 3 Dec 2024 10:48:04 -0600 Subject: [PATCH 03/34] feat: add summary timeseries to label values breakdown --- .../Breakdowns/ByFrameRepeater.tsx | 3 +- .../Breakdowns/ClearFiltersLayoutScene.tsx | 24 ++ .../Breakdowns/FieldValuesBreakdownScene.tsx | 4 +- .../Breakdowns/FieldsBreakdownScene.tsx | 93 ++------ .../Breakdowns/LabelBreakdownScene.tsx | 64 +++-- .../Breakdowns/LabelValuesBreakdownScene.tsx | 225 +++++++++++------- src/services/variableGetters.ts | 51 ++++ 7 files changed, 275 insertions(+), 189 deletions(-) create mode 100644 src/Components/ServiceScene/Breakdowns/ClearFiltersLayoutScene.tsx diff --git a/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx b/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx index 26ca7a67..4411fa70 100644 --- a/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx +++ b/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx @@ -135,7 +135,8 @@ export class ByFrameRepeater extends SceneObjectBase { }); if (newChildren.length === 0) { - this.state.body.setState({ children: [buildNoResultsScene(this.getFilter(), this.clearFilter)] }); + const filter = this.getFilter(); + this.state.body.setState({ children: [buildNoResultsScene(filter, this.clearFilter)] }); } else { this.state.body.setState({ children: newChildren }); } diff --git a/src/Components/ServiceScene/Breakdowns/ClearFiltersLayoutScene.tsx b/src/Components/ServiceScene/Breakdowns/ClearFiltersLayoutScene.tsx new file mode 100644 index 00000000..05a31ec9 --- /dev/null +++ b/src/Components/ServiceScene/Breakdowns/ClearFiltersLayoutScene.tsx @@ -0,0 +1,24 @@ +import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { GrotError } from '../../GrotError'; +import { Alert, Button } from '@grafana/ui'; +import React from 'react'; +import { emptyStateStyles } from './FieldsBreakdownScene'; + +export interface ClearFiltersLayoutSceneState extends SceneObjectState { + clearCallback: () => void; +} +export class ClearFiltersLayoutScene extends SceneObjectBase { + public static Component = ({ model }: SceneComponentProps) => { + const { clearCallback } = model.useState(); + return ( + + + No labels match these filters.{' '} + {' '} + + + ); + }; +} diff --git a/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx index c7ae2462..94433c86 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx @@ -203,7 +203,7 @@ export class FieldValuesBreakdownScene extends SceneObjectBase, + reactNode: , }), new ByFrameRepeater({ body: new SceneCSSGridLayout({ @@ -244,7 +244,7 @@ export class FieldValuesBreakdownScene extends SceneObjectBase, + reactNode: , }), new ByFrameRepeater({ body: new SceneCSSGridLayout({ diff --git a/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx index dceb9f96..36fba022 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx @@ -3,29 +3,25 @@ import React from 'react'; import { DataFrame, GrafanaTheme2, LoadingState } from '@grafana/data'; import { - AdHocFiltersVariable, QueryRunnerState, SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState, - SceneReactObject, - SceneVariable, SceneVariableSet, VariableDependencyConfig, VariableValueOption, } from '@grafana/scenes'; -import { Alert, Button, useStyles2 } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from 'services/analytics'; import { getSortByPreference } from 'services/store'; -import { ALL_VARIABLE_VALUE, SERVICE_NAME, SERVICE_UI_LABEL, VAR_FIELD_GROUP_BY, VAR_LABELS } from 'services/variables'; +import { ALL_VARIABLE_VALUE, VAR_FIELD_GROUP_BY, VAR_LABELS } from 'services/variables'; import { areArraysEqual } from '../../../services/comparison'; import { CustomConstantVariable, CustomConstantVariableState } from '../../../services/CustomConstantVariable'; import { navigateToValueBreakdown } from '../../../services/navigate'; import { checkPrimaryLabel, getPrimaryLabelFromUrl, ValueSlugs } from '../../../services/routing'; import { DEFAULT_SORT_BY } from '../../../services/sorting'; -import { GrotError } from '../../GrotError'; import { IndexScene } from '../../IndexScene/IndexScene'; import { getDetectedFieldsFrame, ServiceScene } from '../ServiceScene'; import { BreakdownSearchReset, BreakdownSearchScene } from './BreakdownSearchScene'; @@ -38,14 +34,20 @@ import { SortByScene, SortCriteriaChanged } from './SortByScene'; import { StatusWrapper } from './StatusWrapper'; import { getFieldOptions } from 'services/filters'; import { EmptyLayoutScene } from './EmptyLayoutScene'; -import { getFieldGroupByVariable, getLabelsVariable } from '../../../services/variableGetters'; +import { + clearVariables, + getFieldGroupByVariable, + getLabelsVariable, + getVariablesThatCanBeCleared, +} from '../../../services/variableGetters'; +import { ClearFiltersLayoutScene } from './ClearFiltersLayoutScene'; export const averageFields = ['duration', 'count', 'total', 'bytes']; export const FIELDS_BREAKDOWN_GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(400px, 1fr))'; export interface FieldsBreakdownSceneState extends SceneObjectState { body?: - | (SceneReactObject & SceneObject) + | (ClearFiltersLayoutScene & SceneObject) | (FieldsAggregatedBreakdownScene & SceneObject) | (FieldValuesBreakdownScene & SceneObject) | (EmptyLayoutScene & SceneObject); @@ -150,7 +152,7 @@ export class FieldsBreakdownScene extends SceneObjectBase 1) { this.state.changeFieldCount?.(0); - body = this.buildClearFiltersLayout(() => this.clearVariables(variablesToClear)); + body = new ClearFiltersLayoutScene({ clearCallback: () => clearVariables(this) }); } else { body = new EmptyLayoutScene({ type: 'fields' }); } @@ -223,11 +225,11 @@ export class FieldsBreakdownScene extends SceneObjectBase 1) { this.state.changeFieldCount?.(0); - stateUpdate.body = this.buildClearFiltersLayout(() => this.clearVariables(variablesToClear)); + stateUpdate.body = new ClearFiltersLayoutScene({ clearCallback: () => clearVariables(this) }); } else { stateUpdate.body = new EmptyLayoutScene({ type: 'fields' }); } @@ -241,7 +243,7 @@ export class FieldsBreakdownScene extends SceneObjectBase { - // clear patterns: needs to happen first, or it won't work as patterns is split into a variable and a state, and updating the variable triggers a state update - const indexScene = sceneGraph.getAncestor(this, IndexScene); - indexScene.setState({ - patterns: [], - }); - - variablesToClear.forEach((variable) => { - if (variable instanceof AdHocFiltersVariable && variable.state.key === 'adhoc_service_filter') { - let { labelName } = getPrimaryLabelFromUrl(); - // getPrimaryLabelFromUrl returns the label name that exists in the URL, which is "service" not "service_name" - if (labelName === SERVICE_UI_LABEL) { - labelName = SERVICE_NAME; - } - variable.setState({ - filters: variable.state.filters.filter((filter) => filter.key === labelName), - }); - } else if (variable instanceof AdHocFiltersVariable) { - variable.setState({ - filters: [], - }); - } else if (variable instanceof CustomConstantVariable) { - variable.setState({ - value: '', - text: '', - }); - } - }); - }; - - private buildClearFiltersLayout(clearCallback: () => void) { - return new SceneReactObject({ - reactNode: ( - - - No labels match these filters.{' '} - {' '} - - - ), - }); - } - public onFieldSelectorChange = (value?: string) => { if (!value) { return; @@ -352,7 +293,7 @@ export class FieldsBreakdownScene extends SceneObjectBase ); }; - public static FieldValueMenu = ({ model }: SceneComponentProps) => { + public static ValueMenu = ({ model }: SceneComponentProps) => { const { loading, search, sort } = model.useState(); const styles = useStyles2(getStyles); const variable = getFieldGroupByVariable(model); diff --git a/src/Components/ServiceScene/Breakdowns/LabelBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelBreakdownScene.tsx index 2c4e1ada..ca70f6b3 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelBreakdownScene.tsx @@ -63,7 +63,6 @@ export class LabelBreakdownScene extends SceneObjectBase { - if (layout instanceof ByFrameRepeater) { - layout.sort(event.sortBy, event.direction); - } + const body = this.state.body; + if (body instanceof LabelValuesBreakdownScene) { + const byFrameRepeaters = sceneGraph.findDescendents(body, ByFrameRepeater); + byFrameRepeaters.forEach((layout) => { + layout.sort(event.sortBy, event.direction); }); } reportAppInteraction( @@ -273,34 +272,57 @@ export class LabelBreakdownScene extends SceneObjectBase) => { - const { body, loading, blockingMessage, error, search, sort } = model.useState(); + public static ParentMenu = ({ model }: SceneComponentProps) => { + const { body, loading } = model.useState(); const variable = getLabelGroupByVariable(model); const { options, value } = variable.useState(); const styles = useStyles2(getStyles); + return ( +
+ {body instanceof LabelValuesBreakdownScene && } + {body instanceof LabelsAggregatedBreakdownScene && } + + {!loading && options.length > 0 && ( + + )} +
+ ); + }; + + public static ValueMenu = ({ model }: SceneComponentProps) => { + const { loading, search, sort } = model.useState(); + const variable = getLabelGroupByVariable(model); + const { value } = variable.useState(); + const styles = useStyles2(getStyles); + + return ( +
+ {!loading && value !== ALL_VARIABLE_VALUE && ( + <> + + + + )} +
+ ); + }; + + public static Component = ({ model }: SceneComponentProps) => { + const { body, loading, blockingMessage, error } = model.useState(); + const styles = useStyles2(getStyles); + return (
-
- {body instanceof LabelValuesBreakdownScene && } - {body instanceof LabelsAggregatedBreakdownScene && } - {!loading && value !== ALL_VARIABLE_VALUE && ( - <> - - - - )} - {!loading && options.length > 0 && ( - - )} -
{error && ( The labels are not available at this moment. Try using a different time range or check again later. )} + {body instanceof LabelsAggregatedBreakdownScene && model && } +
{body && }
diff --git a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx index 94ce21af..5c5d9a12 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx @@ -14,7 +14,7 @@ import { } from '@grafana/scenes'; import { LayoutSwitcher } from './LayoutSwitcher'; import { getLabelValue } from './SortByScene'; -import { Alert, DrawStyle, LoadingPlaceholder, StackingMode, useStyles2 } from '@grafana/ui'; +import { DrawStyle, LoadingPlaceholder, StackingMode, useStyles2 } from '@grafana/ui'; import { getQueryRunner, setLevelColorOverrides } from '../../../services/panel'; import { getSortByPreference } from '../../../services/store'; import { AppEvents, DataQueryError, LoadingState } from '@grafana/data'; @@ -30,14 +30,21 @@ import { AddFilterEvent } from './AddToFiltersButton'; import { DEFAULT_SORT_BY } from '../../../services/sorting'; import { buildLabelsQuery, LABEL_BREAKDOWN_GRID_TEMPLATE_COLUMNS } from '../../../services/labels'; import { getAppEvents } from '@grafana/runtime'; -import { getLabelGroupByVariable } from '../../../services/variableGetters'; -import { PanelMenu, getPanelWrapperStyles } from '../../Panels/PanelMenu'; +import { + clearVariables, + getLabelGroupByVariable, + getVariablesThatCanBeCleared, +} from '../../../services/variableGetters'; +import { getPanelWrapperStyles, PanelMenu } from '../../Panels/PanelMenu'; +import { ClearFiltersLayoutScene } from './ClearFiltersLayoutScene'; +import { EmptyLayoutScene } from './EmptyLayoutScene'; +import { IndexScene } from '../../IndexScene/IndexScene'; type DisplayError = DataQueryError & { displayed: boolean }; type DisplayErrors = Record; export interface LabelValueBreakdownSceneState extends SceneObjectState { - body?: LayoutSwitcher & SceneObject; + body?: (LayoutSwitcher & SceneObject) | (ClearFiltersLayoutScene & SceneObject) | (EmptyLayoutScene & SceneObject); $data?: SceneDataProvider; lastFilterEvent?: AddFilterEvent; errors: DisplayErrors; @@ -86,21 +93,17 @@ export class LabelValuesBreakdownScene extends SceneObjectBase { - const errorIndex = `${err.status}_${err.traceId}_${err.message}`; - if (errors[errorIndex] === undefined) { - errors[errorIndex] = { ...err, displayed: false }; - } - }); - this.setState({ - errors, - }); + // Set empty states + this.setEmptyStates(newState); - this.showErrorToast(this.state.errors); - } + // Set error states + this.setErrorStates(newState); + + // Navigate back to main page if user reduced cardinality to 1 + this.navigateOnLastFilter(newState); + } + private navigateOnLastFilter(newState: SceneDataState) { if (newState.data?.state === LoadingState.Done || newState.data?.state === LoadingState.Streaming) { // No panels for the user to select, presumably because everything has been excluded const event = this.state.lastFilterEvent; @@ -117,53 +120,74 @@ export class LabelValuesBreakdownScene extends SceneObjectBase { + const errorIndex = `${err.status}_${err.traceId}_${err.message}`; + if (errors[errorIndex] === undefined) { + errors[errorIndex] = { ...err, displayed: false }; + } + }); + this.setState({ + errors, + }); + + this.showErrorToast(this.state.errors); + } + } - // If we're in an error state - if (newState.data?.state === LoadingState.Error && this.activeLayoutContainsNoPanels()) { - const activeLayout = this.getActiveLayout(); - // And the active layout is grid or rows, and doesn't have any panels - if (activeLayout instanceof ByFrameRepeater) { - const errorState = this.getErrorStateAlert(newState.data.errors); - // Replace the loading or error state with new error - activeLayout.state.body.setState({ - children: [errorState], + private setEmptyStates(newState: SceneDataState) { + if (newState.data?.state === LoadingState.Done) { + if (newState.data.series.length > 0 && !(this.state.body instanceof LayoutSwitcher)) { + this.setState({ + body: this.build(), }); + } else if (newState.data.series.length === 0) { + const indexScene = sceneGraph.getAncestor(this, IndexScene); + const variablesToClear = getVariablesThatCanBeCleared(indexScene); + + if (variablesToClear.length > 1) { + this.setState({ + body: new ClearFiltersLayoutScene({ clearCallback: () => clearVariables(this) }), + }); + } else { + this.setState({ + body: new EmptyLayoutScene({ type: 'fields' }), + }); + } } } } - private getActiveLayout(): ByFrameRepeater | SceneFlexLayout | undefined { + private getActiveLayout(): SceneFlexLayout | undefined { const layoutSwitcher = this.state.body; - const activeLayout = layoutSwitcher?.state.layouts.find((layout) => layout.isActive); - if (activeLayout instanceof ByFrameRepeater || activeLayout instanceof SceneFlexLayout) { - return activeLayout; + if (layoutSwitcher instanceof LayoutSwitcher) { + const activeLayout = layoutSwitcher?.state.layouts.find((layout) => layout.isActive); + if (activeLayout instanceof SceneFlexLayout) { + return activeLayout; + } } return undefined; } private activeLayoutContainsNoPanels(): boolean { const activeLayout = this.getActiveLayout(); - - if (activeLayout instanceof ByFrameRepeater) { - const child = activeLayout.state.body.state.children[0]; - if (child instanceof SceneFlexItem || child instanceof SceneReactObject) { - return true; - } + if (activeLayout) { + const byFrameRepeaters = sceneGraph.findDescendents(activeLayout, ByFrameRepeater); + return byFrameRepeaters.some((repeater) => { + const child = repeater.state.body.state.children[0]; + console.log('child', child); + return child instanceof SceneFlexItem || child instanceof SceneReactObject; + }); } return false; } - private getErrorStateAlert(errors: DataQueryError[] | undefined) { - return new SceneReactObject({ - reactNode: ( - - {errors?.map((err, key) => this.renderError(key, err))} - - ), - }); - } - private navigateToLabels() { this.setState({ lastFilterEvent: undefined, @@ -204,58 +228,81 @@ export class LabelValuesBreakdownScene extends SceneObjectBase }), new SceneFlexItem({ minHeight: 300, body, }), ], }), - new ByFrameRepeater({ - body: new SceneCSSGridLayout({ - isLazy: true, - templateColumns: LABEL_BREAKDOWN_GRID_TEMPLATE_COLUMNS, - autoRows: '200px', - children: [ - new SceneFlexItem({ - body: new SceneReactObject({ - reactNode: , - }), + new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneReactObject({ reactNode: }), + new SceneFlexItem({ + minHeight: 300, + body: body.clone(), + }), + new SceneReactObject({ reactNode: }), + new ByFrameRepeater({ + body: new SceneCSSGridLayout({ + isLazy: true, + templateColumns: LABEL_BREAKDOWN_GRID_TEMPLATE_COLUMNS, + autoRows: '200px', + children: [ + new SceneFlexItem({ + body: new SceneReactObject({ + reactNode: , + }), + }), + ], }), - ], - }), - getLayoutChild: getFilterBreakdownValueScene( - getLabelValue, - DrawStyle.Bars, - VAR_LABELS, - sceneGraph.getAncestor(this, LabelBreakdownScene).state.sort, - tagKey - ), - sortBy, - direction, - getFilter, + getLayoutChild: getFilterBreakdownValueScene( + getLabelValue, + DrawStyle.Bars, + VAR_LABELS, + sceneGraph.getAncestor(this, LabelBreakdownScene).state.sort, + tagKey + ), + sortBy, + direction, + getFilter, + }), + ], }), - new ByFrameRepeater({ - body: new SceneCSSGridLayout({ - templateColumns: '1fr', - autoRows: '200px', - children: [ - new SceneFlexItem({ - body: new SceneReactObject({ - reactNode: , - }), + new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneReactObject({ reactNode: }), + new SceneFlexItem({ + minHeight: 300, + body: body.clone(), + }), + new SceneReactObject({ reactNode: }), + new ByFrameRepeater({ + body: new SceneCSSGridLayout({ + templateColumns: '1fr', + autoRows: '200px', + children: [ + new SceneFlexItem({ + body: new SceneReactObject({ + reactNode: , + }), + }), + ], }), - ], - }), - getLayoutChild: getFilterBreakdownValueScene( - getLabelValue, - DrawStyle.Bars, - VAR_LABELS, - sceneGraph.getAncestor(this, LabelBreakdownScene).state.sort, - tagKey - ), - sortBy, - direction, - getFilter, + getLayoutChild: getFilterBreakdownValueScene( + getLabelValue, + DrawStyle.Bars, + VAR_LABELS, + sceneGraph.getAncestor(this, LabelBreakdownScene).state.sort, + tagKey + ), + sortBy, + direction, + getFilter, + }), + ], }), ], }); diff --git a/src/services/variableGetters.ts b/src/services/variableGetters.ts index ffc185fd..1a7c32e4 100644 --- a/src/services/variableGetters.ts +++ b/src/services/variableGetters.ts @@ -4,6 +4,7 @@ import { DataSourceVariable, sceneGraph, SceneObject, + SceneVariable, SceneVariableState, } from '@grafana/scenes'; import { CustomConstantVariable } from './CustomConstantVariable'; @@ -34,10 +35,13 @@ import { VAR_METADATA_EXPR, VAR_METADATA, VAR_LABELS_REPLICA, + SERVICE_UI_LABEL, } from './variables'; import { AdHocVariableFilter } from '@grafana/data'; import { logger } from './logger'; import { narrowFieldValue, NarrowingError } from './narrowing'; +import { IndexScene } from '../Components/IndexScene/IndexScene'; +import { getPrimaryLabelFromUrl } from './routing'; export function getLogsStreamSelector(options: LogsQueryOptions) { const { @@ -237,3 +241,50 @@ export function getDataSourceName(scene: SceneObject) { const dsVariable = getDataSourceVariable(scene); return dsVariable.getValue(); } + +export function getVariablesThatCanBeCleared(indexScene: IndexScene) { + const variables = sceneGraph.getVariables(indexScene); + let variablesToClear: SceneVariable[] = []; + + for (const variable of variables.state.variables) { + if (variable instanceof AdHocFiltersVariable && variable.state.filters.length) { + variablesToClear.push(variable); + } + if (variable instanceof CustomConstantVariable && variable.state.value && variable.state.name !== 'logsFormat') { + variablesToClear.push(variable); + } + } + return variablesToClear; +} + +export function clearVariables(sceneRef: SceneObject) { + // clear patterns: needs to happen first, or it won't work as patterns is split into a variable and a state, and updating the variable triggers a state update + const indexScene = sceneGraph.getAncestor(sceneRef, IndexScene); + indexScene.setState({ + patterns: [], + }); + + const variablesToClear = getVariablesThatCanBeCleared(indexScene); + + variablesToClear.forEach((variable) => { + if (variable instanceof AdHocFiltersVariable && variable.state.key === 'adhoc_service_filter') { + let { labelName } = getPrimaryLabelFromUrl(); + // getPrimaryLabelFromUrl returns the label name that exists in the URL, which is "service" not "service_name" + if (labelName === SERVICE_UI_LABEL) { + labelName = SERVICE_NAME; + } + variable.setState({ + filters: variable.state.filters.filter((filter) => filter.key === labelName), + }); + } else if (variable instanceof AdHocFiltersVariable) { + variable.setState({ + filters: [], + }); + } else if (variable instanceof CustomConstantVariable) { + variable.setState({ + value: '', + text: '', + }); + } + }); +} From 58d4d8d63eb7895711e931174fe0a9bb63f24acf Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 3 Dec 2024 11:01:52 -0600 Subject: [PATCH 04/34] chore: refactor variable methods into new file --- .../Breakdowns/FieldsBreakdownScene.tsx | 8 +-- .../Breakdowns/LabelValuesBreakdownScene.tsx | 7 +-- src/services/variableGetters.ts | 59 ++----------------- src/services/variableHelpers.ts | 52 ++++++++++++++++ 4 files changed, 60 insertions(+), 66 deletions(-) create mode 100644 src/services/variableHelpers.ts diff --git a/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx index 36fba022..6ab2cbbd 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx @@ -34,13 +34,9 @@ import { SortByScene, SortCriteriaChanged } from './SortByScene'; import { StatusWrapper } from './StatusWrapper'; import { getFieldOptions } from 'services/filters'; import { EmptyLayoutScene } from './EmptyLayoutScene'; -import { - clearVariables, - getFieldGroupByVariable, - getLabelsVariable, - getVariablesThatCanBeCleared, -} from '../../../services/variableGetters'; +import { getFieldGroupByVariable, getLabelsVariable } from '../../../services/variableGetters'; import { ClearFiltersLayoutScene } from './ClearFiltersLayoutScene'; +import { clearVariables, getVariablesThatCanBeCleared } from '../../../services/variableHelpers'; export const averageFields = ['duration', 'count', 'total', 'bytes']; export const FIELDS_BREAKDOWN_GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(400px, 1fr))'; diff --git a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx index 5c5d9a12..cebb37da 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx @@ -30,15 +30,12 @@ import { AddFilterEvent } from './AddToFiltersButton'; import { DEFAULT_SORT_BY } from '../../../services/sorting'; import { buildLabelsQuery, LABEL_BREAKDOWN_GRID_TEMPLATE_COLUMNS } from '../../../services/labels'; import { getAppEvents } from '@grafana/runtime'; -import { - clearVariables, - getLabelGroupByVariable, - getVariablesThatCanBeCleared, -} from '../../../services/variableGetters'; +import { getLabelGroupByVariable } from '../../../services/variableGetters'; import { getPanelWrapperStyles, PanelMenu } from '../../Panels/PanelMenu'; import { ClearFiltersLayoutScene } from './ClearFiltersLayoutScene'; import { EmptyLayoutScene } from './EmptyLayoutScene'; import { IndexScene } from '../../IndexScene/IndexScene'; +import { clearVariables, getVariablesThatCanBeCleared } from '../../../services/variableHelpers'; type DisplayError = DataQueryError & { displayed: boolean }; type DisplayErrors = Record; diff --git a/src/services/variableGetters.ts b/src/services/variableGetters.ts index 1a7c32e4..3bdbf392 100644 --- a/src/services/variableGetters.ts +++ b/src/services/variableGetters.ts @@ -4,7 +4,6 @@ import { DataSourceVariable, sceneGraph, SceneObject, - SceneVariable, SceneVariableState, } from '@grafana/scenes'; import { CustomConstantVariable } from './CustomConstantVariable'; @@ -15,8 +14,8 @@ import { LOGS_FORMAT_EXPR, LogsQueryOptions, MIXED_FORMAT_EXPR, - VAR_AGGREGATED_METRICS, SERVICE_NAME, + VAR_AGGREGATED_METRICS, VAR_DATASOURCE, VAR_FIELD_GROUP_BY, VAR_FIELDS, @@ -24,24 +23,21 @@ import { VAR_LABEL_GROUP_BY, VAR_LABELS, VAR_LABELS_EXPR, + VAR_LABELS_REPLICA, VAR_LEVELS, VAR_LEVELS_EXPR, VAR_LINE_FILTER, VAR_LINE_FILTER_EXPR, + VAR_METADATA, + VAR_METADATA_EXPR, VAR_PATTERNS, VAR_PATTERNS_EXPR, VAR_PRIMARY_LABEL, VAR_PRIMARY_LABEL_SEARCH, - VAR_METADATA_EXPR, - VAR_METADATA, - VAR_LABELS_REPLICA, - SERVICE_UI_LABEL, } from './variables'; import { AdHocVariableFilter } from '@grafana/data'; import { logger } from './logger'; import { narrowFieldValue, NarrowingError } from './narrowing'; -import { IndexScene } from '../Components/IndexScene/IndexScene'; -import { getPrimaryLabelFromUrl } from './routing'; export function getLogsStreamSelector(options: LogsQueryOptions) { const { @@ -241,50 +237,3 @@ export function getDataSourceName(scene: SceneObject) { const dsVariable = getDataSourceVariable(scene); return dsVariable.getValue(); } - -export function getVariablesThatCanBeCleared(indexScene: IndexScene) { - const variables = sceneGraph.getVariables(indexScene); - let variablesToClear: SceneVariable[] = []; - - for (const variable of variables.state.variables) { - if (variable instanceof AdHocFiltersVariable && variable.state.filters.length) { - variablesToClear.push(variable); - } - if (variable instanceof CustomConstantVariable && variable.state.value && variable.state.name !== 'logsFormat') { - variablesToClear.push(variable); - } - } - return variablesToClear; -} - -export function clearVariables(sceneRef: SceneObject) { - // clear patterns: needs to happen first, or it won't work as patterns is split into a variable and a state, and updating the variable triggers a state update - const indexScene = sceneGraph.getAncestor(sceneRef, IndexScene); - indexScene.setState({ - patterns: [], - }); - - const variablesToClear = getVariablesThatCanBeCleared(indexScene); - - variablesToClear.forEach((variable) => { - if (variable instanceof AdHocFiltersVariable && variable.state.key === 'adhoc_service_filter') { - let { labelName } = getPrimaryLabelFromUrl(); - // getPrimaryLabelFromUrl returns the label name that exists in the URL, which is "service" not "service_name" - if (labelName === SERVICE_UI_LABEL) { - labelName = SERVICE_NAME; - } - variable.setState({ - filters: variable.state.filters.filter((filter) => filter.key === labelName), - }); - } else if (variable instanceof AdHocFiltersVariable) { - variable.setState({ - filters: [], - }); - } else if (variable instanceof CustomConstantVariable) { - variable.setState({ - value: '', - text: '', - }); - } - }); -} diff --git a/src/services/variableHelpers.ts b/src/services/variableHelpers.ts new file mode 100644 index 00000000..b8778cfd --- /dev/null +++ b/src/services/variableHelpers.ts @@ -0,0 +1,52 @@ +import { AdHocFiltersVariable, sceneGraph, SceneObject, SceneVariable } from '@grafana/scenes'; +import { CustomConstantVariable } from './CustomConstantVariable'; +import { SERVICE_NAME, SERVICE_UI_LABEL } from './variables'; +import { IndexScene } from '../Components/IndexScene/IndexScene'; +import { getPrimaryLabelFromUrl } from './routing'; + +export function getVariablesThatCanBeCleared(indexScene: IndexScene) { + const variables = sceneGraph.getVariables(indexScene); + let variablesToClear: SceneVariable[] = []; + + for (const variable of variables.state.variables) { + if (variable instanceof AdHocFiltersVariable && variable.state.filters.length) { + variablesToClear.push(variable); + } + if (variable instanceof CustomConstantVariable && variable.state.value && variable.state.name !== 'logsFormat') { + variablesToClear.push(variable); + } + } + return variablesToClear; +} + +export function clearVariables(sceneRef: SceneObject) { + // clear patterns: needs to happen first, or it won't work as patterns is split into a variable and a state, and updating the variable triggers a state update + const indexScene = sceneGraph.getAncestor(sceneRef, IndexScene); + indexScene.setState({ + patterns: [], + }); + + const variablesToClear = getVariablesThatCanBeCleared(indexScene); + + variablesToClear.forEach((variable) => { + if (variable instanceof AdHocFiltersVariable && variable.state.key === 'adhoc_service_filter') { + let { labelName } = getPrimaryLabelFromUrl(); + // getPrimaryLabelFromUrl returns the label name that exists in the URL, which is "service" not "service_name" + if (labelName === SERVICE_UI_LABEL) { + labelName = SERVICE_NAME; + } + variable.setState({ + filters: variable.state.filters.filter((filter) => filter.key === labelName), + }); + } else if (variable instanceof AdHocFiltersVariable) { + variable.setState({ + filters: [], + }); + } else if (variable instanceof CustomConstantVariable) { + variable.setState({ + value: '', + text: '', + }); + } + }); +} From 0203e581b70eb7e70888faf1bfbedc47b0bba83f Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 3 Dec 2024 12:59:05 -0600 Subject: [PATCH 05/34] test: fix e2e assertions --- tests/exploreServices.spec.ts | 3 ++- tests/exploreServicesBreakDown.spec.ts | 18 +++++++++++------- tests/exploreServicesJsonBreakDown.spec.ts | 4 ++-- .../exploreServicesJsonMixedBreakDown.spec.ts | 14 +++++++------- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/tests/exploreServices.spec.ts b/tests/exploreServices.spec.ts index 00ef10d6..7319cf61 100644 --- a/tests/exploreServices.spec.ts +++ b/tests/exploreServices.spec.ts @@ -146,10 +146,11 @@ test.describe('explore services page', () => { test('should clear filters and levels when navigating back to previously activated service', async ({ page }) => { await explorePage.addServiceName(); - // Add detected_level filter await page.getByTestId(testIds.exploreServiceDetails.tabLabels).click(); await page.getByLabel('Select detected_level').click(); + await explorePage.assertNotLoading(); + await explorePage.scrollToBottom(); await page.getByTestId(testIds.exploreServiceDetails.buttonFilterInclude).nth(1).click(); await expect(page.getByTestId('AdHocFilter-detected_level')).toBeVisible(); diff --git a/tests/exploreServicesBreakDown.spec.ts b/tests/exploreServicesBreakDown.spec.ts index e48a8e18..e88b750c 100644 --- a/tests/exploreServicesBreakDown.spec.ts +++ b/tests/exploreServicesBreakDown.spec.ts @@ -294,9 +294,11 @@ test.describe('explore services breakdown page', () => { await page.getByPlaceholder('Search for value').click(); const panels = page.getByTestId(/data-testid Panel header/); await expect(panels.first()).toBeVisible(); - await expect(panels).toHaveCount(4); + + await explorePage.scrollToBottom(); + await expect(panels).toHaveCount(5); await page.keyboard.type('errr'); - await expect(panels).toHaveCount(1); + await expect(panels).toHaveCount(2); }); test(`should search fields for ${fieldName}`, async ({ page }) => { @@ -306,12 +308,14 @@ test.describe('explore services breakdown page', () => { await explorePage.click(page.getByPlaceholder('Search for value')); const panels = page.getByTestId(/data-testid Panel header/); await expect(panels.first()).toBeVisible(); - expect(await panels.count()).toBeGreaterThan(1); + await explorePage.assertNotLoading(); + // Assert there is at least 2 panels + await expect(panels.nth(1)).toBeVisible(); + // expect(await panels.count()).toBeGreaterThan(1); await page.keyboard.type('brod'); - await expect(panels).toHaveCount(1); + await expect(panels).toHaveCount(2); }); - // Broken after latest loki update, all fields return both parsers test(`should exclude ${fieldName}, request should contain logfmt`, async ({ page }) => { let requests: PlaywrightRequest[] = []; explorePage.blockAllQueriesExcept({ @@ -325,7 +329,7 @@ test.describe('explore services breakdown page', () => { await page.getByTestId(`data-testid Panel header ${fieldName}`).getByRole('button', { name: 'Select' }).click(); // Should see 8 panels after it's done loading - await expect(allPanels).toHaveCount(8); + await expect(allPanels).toHaveCount(9); // And we'll have 2 requests, one on the aggregation, one for the label values expect(requests).toHaveLength(2); @@ -333,7 +337,7 @@ test.describe('explore services breakdown page', () => { await page.getByRole('button', { name: 'Exclude' }).nth(0).click(); // Should have removed a panel - await expect(allPanels).toHaveCount(7); + await expect(allPanels).toHaveCount(8); // Adhoc content filter should be added await expect(page.getByTestId(`data-testid Dashboard template variables submenu Label ${fieldName}`)).toBeVisible(); await expect(page.getByText('!=')).toBeVisible(); diff --git a/tests/exploreServicesJsonBreakDown.spec.ts b/tests/exploreServicesJsonBreakDown.spec.ts index 3fff44d0..b652509d 100644 --- a/tests/exploreServicesJsonBreakDown.spec.ts +++ b/tests/exploreServicesJsonBreakDown.spec.ts @@ -36,13 +36,13 @@ test.describe('explore nginx-json breakdown pages ', () => { await page.getByTestId(`data-testid Panel header ${fieldName}`).getByRole('button', { name: 'Select' }).click(); const allPanels = explorePage.getAllPanelsLocator(); // We should have 6 panels - await expect(allPanels).toHaveCount(6); + await expect(allPanels).toHaveCount(7); // Should have 2 queries by now expect(requests).toHaveLength(2); // Exclude a panel await page.getByRole('button', { name: 'Exclude' }).nth(0).click(); // Should be removed from the UI, and also lets us know when the query is done loading - await expect(allPanels).toHaveCount(5); + await expect(allPanels).toHaveCount(6); // Adhoc content filter should be added await expect(page.getByTestId(`data-testid Dashboard template variables submenu Label ${fieldName}`)).toBeVisible(); diff --git a/tests/exploreServicesJsonMixedBreakDown.spec.ts b/tests/exploreServicesJsonMixedBreakDown.spec.ts index 00d16a95..b0de3d70 100644 --- a/tests/exploreServicesJsonMixedBreakDown.spec.ts +++ b/tests/exploreServicesJsonMixedBreakDown.spec.ts @@ -42,13 +42,13 @@ test.describe('explore nginx-json-mixed breakdown pages ', () => { .click(); const allPanels = explorePage.getAllPanelsLocator(); // We should have 6 panels - await expect(allPanels).toHaveCount(6); + await expect(allPanels).toHaveCount(7); // Should have 2 queries by now expect(requests).toHaveLength(2); // Exclude a panel await page.getByRole('button', { name: 'Exclude' }).nth(0).click(); // Should be removed from the UI, and also lets us know when the query is done loading - await expect(allPanels).toHaveCount(5); + await expect(allPanels).toHaveCount(6); // Adhoc content filter should be added await expect( @@ -85,8 +85,8 @@ test.describe('explore nginx-json-mixed breakdown pages ', () => { .getByRole('button', { name: 'Select' }) .click(); - // We should have 1 panel for 1 field value - await expect(allPanels).toHaveCount(1); + // We should have 2 panels for 1 field value + await expect(allPanels).toHaveCount(2); // Should have 2 queries by now expect(requests).toHaveLength(2); // Exclude a panel @@ -134,14 +134,14 @@ test.describe('explore nginx-json-mixed breakdown pages ', () => { .getByRole('button', { name: 'Select' }) .click(); const allPanels = explorePage.getAllPanelsLocator(); - // We should have 6 panels - await expect(allPanels).toHaveCount(3); + // We should have 4 panels + await expect(allPanels).toHaveCount(4); // Should have 2 queries by now expect(requests).toHaveLength(2); // Exclude a panel await page.getByRole('button', { name: 'Exclude' }).nth(0).click(); // Should be removed from the UI, and also lets us know when the query is done loading - await expect(allPanels).toHaveCount(2); + await expect(allPanels).toHaveCount(3); // Adhoc content filter should be added await expect( From 9fd65e27523cf233d0a6e94f6c0d16496a9d304d Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 3 Dec 2024 13:34:51 -0600 Subject: [PATCH 06/34] test: fix flakey test --- tests/exploreServicesBreakDown.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/exploreServicesBreakDown.spec.ts b/tests/exploreServicesBreakDown.spec.ts index e88b750c..cf8514dc 100644 --- a/tests/exploreServicesBreakDown.spec.ts +++ b/tests/exploreServicesBreakDown.spec.ts @@ -243,9 +243,11 @@ test.describe('explore services breakdown page', () => { } }); - test('should search for tenant field, changing sort order updates value breakdown position', async ({ page }) => { + test.only('should search for tenant field, changing sort order updates value breakdown position', async ({ + page, + }) => { explorePage.blockAllQueriesExcept({ - refIds: ['logsPanelQuery', fieldName, 'tenant'], + refIds: ['logsPanelQuery'], legendFormats: [`{{${levelName}}}`], }); await explorePage.goToFieldsTab(); @@ -254,6 +256,7 @@ test.describe('explore services breakdown page', () => { await page.getByText('FieldAll').click(); await page.keyboard.type('tenan'); await page.keyboard.press('Enter'); + await explorePage.assertNotLoading(); // Assert loading is done and panels are showing const panels = page.getByTestId(/data-testid Panel header/); @@ -1063,7 +1066,6 @@ test.describe('explore services breakdown page', () => { test('panel menu: label value panel should open links in explore', async ({ page, context }) => { await explorePage.goToLabelsTab(); await page.getByLabel(`Select ${levelName}`).click(); - await page.pause(); await page.getByTestId('data-testid Panel menu error').click(); // Open link From e24225e67c25d1206104f4181ec6fff42d2fb15e Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 3 Dec 2024 13:41:57 -0600 Subject: [PATCH 07/34] chore: remove only --- tests/exploreServicesBreakDown.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/exploreServicesBreakDown.spec.ts b/tests/exploreServicesBreakDown.spec.ts index cf8514dc..83c17b53 100644 --- a/tests/exploreServicesBreakDown.spec.ts +++ b/tests/exploreServicesBreakDown.spec.ts @@ -243,9 +243,7 @@ test.describe('explore services breakdown page', () => { } }); - test.only('should search for tenant field, changing sort order updates value breakdown position', async ({ - page, - }) => { + test('should search for tenant field, changing sort order updates value breakdown position', async ({ page }) => { explorePage.blockAllQueriesExcept({ refIds: ['logsPanelQuery'], legendFormats: [`{{${levelName}}}`], From 530cee03f5ee5384fc7c1d45896c1712ae576a84 Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 3 Dec 2024 15:11:48 -0600 Subject: [PATCH 08/34] chore: sync text search state --- .../Breakdowns/ByFrameRepeater.tsx | 53 +++++++++++++++++++ .../Breakdowns/FieldValuesBreakdownScene.tsx | 10 +++- .../Breakdowns/LabelValuesBreakdownScene.tsx | 4 +- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx b/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx index 4411fa70..d65a7fa3 100644 --- a/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx +++ b/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx @@ -4,6 +4,7 @@ import { DataFrame, LoadingState, PanelData } from '@grafana/data'; import { SceneByFrameRepeater, SceneComponentProps, + SceneDataTransformer, SceneFlexItem, SceneFlexLayout, sceneGraph, @@ -11,6 +12,7 @@ import { SceneObjectBase, SceneObjectState, SceneReactObject, + VizPanel, } from '@grafana/scenes'; import { sortSeries } from 'services/sorting'; import { fuzzySearch } from '../../../services/search'; @@ -18,6 +20,9 @@ import { getLabelValue } from './SortByScene'; import { Alert, Button } from '@grafana/ui'; import { css } from '@emotion/css'; import { BreakdownSearchReset } from './BreakdownSearchScene'; +import { SINGLE_GRAPH_KEY } from './FieldValuesBreakdownScene'; +import { map, Observable } from 'rxjs'; +import { LayoutSwitcher } from './LayoutSwitcher'; interface ByFrameRepeaterState extends SceneObjectState { body: SceneLayout; @@ -123,9 +128,37 @@ export class ByFrameRepeater extends SceneObjectBase { // reset search this.filterFrames(() => true); } + + this.filterSummaryChart(data); }); }; + /** + * Filters the summary panel rendered above the breakdown panels by adding a transformation to the panel + * @param data + * @private + */ + private filterSummaryChart(data: string[][]) { + const layoutSwitcher = sceneGraph.getAncestor(this, LayoutSwitcher); + + if (layoutSwitcher) { + const singleGraphParent = sceneGraph.findAllObjects( + layoutSwitcher, + (obj) => obj.isActive && obj.state.key === SINGLE_GRAPH_KEY + ); + if (singleGraphParent[0] instanceof SceneFlexItem) { + const panel = singleGraphParent[0].state.body; + if (panel instanceof VizPanel) { + panel.setState({ + $data: new SceneDataTransformer({ + transformations: [() => limitFramesByName(data[0])], + }), + }); + } + } + } + } + public filterFrames = (filterFn: FrameFilterCallback) => { const newChildren: SceneFlexItem[] = []; this.iterateFrames((frames, seriesIndex) => { @@ -189,3 +222,23 @@ const styles = { marginLeft: '1.5rem', }), }; + +export function limitFramesByName(matches: string[]) { + return (source: Observable) => { + return source.pipe( + map((frames) => { + if (!matches || !matches.length) { + return frames; + } + let newFrames: DataFrame[] = []; + frames.forEach((f) => { + const label = getLabelValue(f); + if (matches.includes(label)) { + newFrames.push(f); + } + }); + return newFrames; + }) + ); + }; +} diff --git a/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx index 94433c86..988e60aa 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx @@ -4,6 +4,7 @@ import { SceneCSSGridLayout, SceneDataProvider, SceneDataState, + SceneDataTransformer, SceneFlexItem, SceneFlexLayout, sceneGraph, @@ -39,6 +40,8 @@ export interface FieldValuesBreakdownSceneState extends SceneObjectState { lastFilterEvent?: AddFilterEvent; } +export const SINGLE_GRAPH_KEY = 'single_graph_key'; + export class FieldValuesBreakdownScene extends SceneObjectBase { constructor(state: Partial) { super(state); @@ -75,7 +78,10 @@ export class FieldValuesBreakdownScene extends SceneObjectBase, }), new SceneFlexItem({ + key: SINGLE_GRAPH_KEY, minHeight: 300, body: PanelBuilders.timeseries().setTitle(optionValue).setMenu(new PanelMenu({})).build(), }), @@ -240,6 +247,7 @@ export class FieldValuesBreakdownScene extends SceneObjectBase, }), new SceneFlexItem({ + key: SINGLE_GRAPH_KEY, minHeight: 300, body: PanelBuilders.timeseries().setTitle(optionValue).setMenu(new PanelMenu({})).build(), }), diff --git a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx index cebb37da..f3c8f9c6 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx @@ -36,6 +36,7 @@ import { ClearFiltersLayoutScene } from './ClearFiltersLayoutScene'; import { EmptyLayoutScene } from './EmptyLayoutScene'; import { IndexScene } from '../../IndexScene/IndexScene'; import { clearVariables, getVariablesThatCanBeCleared } from '../../../services/variableHelpers'; +import { SINGLE_GRAPH_KEY } from './FieldValuesBreakdownScene'; type DisplayError = DataQueryError & { displayed: boolean }; type DisplayErrors = Record; @@ -177,7 +178,6 @@ export class LabelValuesBreakdownScene extends SceneObjectBase { const child = repeater.state.body.state.children[0]; - console.log('child', child); return child instanceof SceneFlexItem || child instanceof SceneReactObject; }); } @@ -238,6 +238,7 @@ export class LabelValuesBreakdownScene extends SceneObjectBase }), new SceneFlexItem({ minHeight: 300, + key: SINGLE_GRAPH_KEY, body: body.clone(), }), new SceneReactObject({ reactNode: }), @@ -273,6 +274,7 @@ export class LabelValuesBreakdownScene extends SceneObjectBase }), new SceneFlexItem({ minHeight: 300, + key: SINGLE_GRAPH_KEY, body: body.clone(), }), new SceneReactObject({ reactNode: }), From 8285424c4969d28bf032014656749c90081e7b63 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 4 Dec 2024 09:39:16 -0600 Subject: [PATCH 09/34] feat: make panel collapsible --- src/Components/Panels/PanelMenu.tsx | 152 +++++++++++------- .../Breakdowns/ByFrameRepeater.tsx | 4 +- .../Breakdowns/FieldValuesBreakdownScene.tsx | 15 +- .../Breakdowns/LabelValuesBreakdownScene.tsx | 14 +- .../Breakdowns/Panels/ValueSummary.ts | 55 +++++++ src/services/store.ts | 3 +- 6 files changed, 163 insertions(+), 80 deletions(-) create mode 100644 src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts diff --git a/src/Components/Panels/PanelMenu.tsx b/src/Components/Panels/PanelMenu.tsx index 72c91892..2e5512cd 100644 --- a/src/Components/Panels/PanelMenu.tsx +++ b/src/Components/Panels/PanelMenu.tsx @@ -3,6 +3,7 @@ import { PanelBuilders, SceneComponentProps, SceneCSSGridItem, + SceneFlexItem, sceneGraph, SceneObject, SceneObjectBase, @@ -24,6 +25,7 @@ import { ExtensionPoints } from '../../services/extensions/links'; import { setLevelColorOverrides } from '../../services/panel'; import { setPanelOption } from '../../services/store'; import { FieldsAggregatedBreakdownScene } from '../ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene'; +import { setValueSummaryHeight } from '../ServiceScene/Breakdowns/Panels/ValueSummary'; const ADD_TO_INVESTIGATION_MENU_TEXT = 'Add to investigation'; const ADD_TO_INVESTIGATION_MENU_DIVIDER_TEXT = 'Investigations'; @@ -33,6 +35,11 @@ export enum AvgFieldPanelType { 'histogram' = 'histogram', } +export enum CollapsablePanelType { + collapse = 'Collapse', + expand = 'Expand', +} + interface PanelMenuState extends SceneObjectState { body?: VizPanelMenu; frame?: DataFrame; @@ -40,60 +47,7 @@ interface PanelMenuState extends SceneObjectState { fieldName?: string; addToExplorations?: AddToExplorationButton; panelType?: AvgFieldPanelType; -} - -function addHistogramItem(items: PanelMenuItem[], sceneRef: PanelMenu) { - items.push({ - text: '', - type: 'divider', - }); - items.push({ - text: 'Visualization', - type: 'group', - }); - items.push({ - text: sceneRef.state.panelType !== AvgFieldPanelType.histogram ? 'Histogram' : 'Time series', - iconClassName: sceneRef.state.panelType !== AvgFieldPanelType.histogram ? 'graph-bar' : 'chart-line', - - onClick: () => { - const gridItem = sceneGraph.getAncestor(sceneRef, SceneCSSGridItem); - const viz = sceneGraph.getAncestor(sceneRef, VizPanel).clone(); - const $data = sceneGraph.getData(sceneRef).clone(); - const menu = sceneRef.clone(); - const headerActions = Array.isArray(viz.state.headerActions) - ? viz.state.headerActions.map((o) => o.clone()) - : viz.state.headerActions; - let body; - - if (sceneRef.state.panelType !== AvgFieldPanelType.histogram) { - body = PanelBuilders.timeseries().setOverrides(setLevelColorOverrides); - } else { - body = PanelBuilders.histogram(); - } - - gridItem.setState({ - body: body.setMenu(menu).setTitle(viz.state.title).setHeaderActions(headerActions).setData($data).build(), - }); - - // @todo extend findObject and use templates to avoid type assertions - const newPanelType = - sceneRef.state.panelType !== AvgFieldPanelType.timeseries - ? AvgFieldPanelType.timeseries - : AvgFieldPanelType.histogram; - setPanelOption('panelType', newPanelType); - menu.setState({ panelType: newPanelType }); - - const fieldsAggregatedBreakdownScene = sceneGraph.findObject( - gridItem, - (o) => o instanceof FieldsAggregatedBreakdownScene - ) as FieldsAggregatedBreakdownScene | null; - if (fieldsAggregatedBreakdownScene) { - fieldsAggregatedBreakdownScene.rebuildAvgFields(); - } - - onSwitchVizTypeTracking(newPanelType); - }, - }); + collapsable?: CollapsablePanelType; } /** @@ -115,6 +69,7 @@ export class PanelMenu extends SceneObjectBase implements VizPan // Manually activate scene this.state.addToExplorations?.activate(); + // Navigation options (all panels) const items: PanelMenuItem[] = [ { text: 'Navigation', @@ -128,6 +83,15 @@ export class PanelMenu extends SceneObjectBase implements VizPan }, ]; + // Visualization options + if (this.state.panelType || this.state.collapsable) { + addVisualizationHeader(items, this); + } + + if (this.state.collapsable) { + addCollapsableItem(items, this); + } + if (this.state.panelType) { addHistogramItem(items, this); } @@ -166,6 +130,86 @@ export class PanelMenu extends SceneObjectBase implements VizPan }; } +function addVisualizationHeader(items: PanelMenuItem[], sceneRef: PanelMenu) { + items.push({ + text: '', + type: 'divider', + }); + items.push({ + text: 'Visualization', + type: 'group', + }); +} + +function addCollapsableItem(items: PanelMenuItem[], menu: PanelMenu) { + items.push({ + text: menu.state.collapsable ?? CollapsablePanelType.expand, + iconClassName: menu.state.collapsable === CollapsablePanelType.collapse ? 'table-collapse-all' : 'table-expand-all', + onClick: () => { + const newCollapsableState = + menu.state.collapsable === CollapsablePanelType.expand + ? CollapsablePanelType.collapse + : CollapsablePanelType.expand; + + console.log('newCollapsableState', { newCollapsableState, currentState: menu.state.collapsable }); + + // Update the viz + const vizPanelFlexItem = sceneGraph.getAncestor(menu, SceneFlexItem); + setValueSummaryHeight(vizPanelFlexItem, newCollapsableState); + + // Set state and update local storage + menu.setState({ collapsable: newCollapsableState }); + setPanelOption('collapsable', newCollapsableState); + }, + }); +} + +function addHistogramItem(items: PanelMenuItem[], sceneRef: PanelMenu) { + items.push({ + text: sceneRef.state.panelType !== AvgFieldPanelType.histogram ? 'Histogram' : 'Time series', + iconClassName: sceneRef.state.panelType !== AvgFieldPanelType.histogram ? 'graph-bar' : 'chart-line', + + onClick: () => { + const gridItem = sceneGraph.getAncestor(sceneRef, SceneCSSGridItem); + const viz = sceneGraph.getAncestor(sceneRef, VizPanel).clone(); + const $data = sceneGraph.getData(sceneRef).clone(); + const menu = sceneRef.clone(); + const headerActions = Array.isArray(viz.state.headerActions) + ? viz.state.headerActions.map((o) => o.clone()) + : viz.state.headerActions; + let body; + + if (sceneRef.state.panelType !== AvgFieldPanelType.histogram) { + body = PanelBuilders.timeseries().setOverrides(setLevelColorOverrides); + } else { + body = PanelBuilders.histogram(); + } + + gridItem.setState({ + body: body.setMenu(menu).setTitle(viz.state.title).setHeaderActions(headerActions).setData($data).build(), + }); + + // @todo extend findObject and use templates to avoid type assertions + const newPanelType = + sceneRef.state.panelType !== AvgFieldPanelType.timeseries + ? AvgFieldPanelType.timeseries + : AvgFieldPanelType.histogram; + setPanelOption('panelType', newPanelType); + menu.setState({ panelType: newPanelType }); + + const fieldsAggregatedBreakdownScene = sceneGraph.findObject( + gridItem, + (o) => o instanceof FieldsAggregatedBreakdownScene + ) as FieldsAggregatedBreakdownScene | null; + if (fieldsAggregatedBreakdownScene) { + fieldsAggregatedBreakdownScene.rebuildAvgFields(); + } + + onSwitchVizTypeTracking(newPanelType); + }, + }); +} + const getExploreLink = (sceneRef: SceneObject) => { const indexScene = sceneGraph.getAncestor(sceneRef, IndexScene); const $data = sceneGraph.getData(sceneRef); diff --git a/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx b/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx index d65a7fa3..6471ab56 100644 --- a/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx +++ b/src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx @@ -20,9 +20,9 @@ import { getLabelValue } from './SortByScene'; import { Alert, Button } from '@grafana/ui'; import { css } from '@emotion/css'; import { BreakdownSearchReset } from './BreakdownSearchScene'; -import { SINGLE_GRAPH_KEY } from './FieldValuesBreakdownScene'; import { map, Observable } from 'rxjs'; import { LayoutSwitcher } from './LayoutSwitcher'; +import { VALUE_SUMMARY_PANEL_KEY } from './Panels/ValueSummary'; interface ByFrameRepeaterState extends SceneObjectState { body: SceneLayout; @@ -144,7 +144,7 @@ export class ByFrameRepeater extends SceneObjectBase { if (layoutSwitcher) { const singleGraphParent = sceneGraph.findAllObjects( layoutSwitcher, - (obj) => obj.isActive && obj.state.key === SINGLE_GRAPH_KEY + (obj) => obj.isActive && obj.state.key === VALUE_SUMMARY_PANEL_KEY ); if (singleGraphParent[0] instanceof SceneFlexItem) { const panel = singleGraphParent[0].state.body; diff --git a/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx index 988e60aa..be116e69 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx @@ -33,6 +33,7 @@ import { DEFAULT_SORT_BY } from '../../../services/sorting'; import { getFieldGroupByVariable, getFieldsVariable } from '../../../services/variableGetters'; import { LokiQuery } from '../../../services/lokiQuery'; import { PanelMenu, getPanelWrapperStyles } from '../../Panels/PanelMenu'; +import { getValueSummaryPanel } from './Panels/ValueSummary'; export interface FieldValuesBreakdownSceneState extends SceneObjectState { body?: (LayoutSwitcher & SceneObject) | (SceneReactObject & SceneObject); @@ -40,8 +41,6 @@ export interface FieldValuesBreakdownSceneState extends SceneObjectState { lastFilterEvent?: AddFilterEvent; } -export const SINGLE_GRAPH_KEY = 'single_graph_key'; - export class FieldValuesBreakdownScene extends SceneObjectBase { constructor(state: Partial) { super(state); @@ -204,11 +203,7 @@ export class FieldValuesBreakdownScene extends SceneObjectBase, }), - new SceneFlexItem({ - key: SINGLE_GRAPH_KEY, - minHeight: 300, - body: PanelBuilders.timeseries().setTitle(optionValue).setMenu(new PanelMenu({})).build(), - }), + getValueSummaryPanel(optionValue), new SceneReactObject({ reactNode: , }), @@ -246,11 +241,7 @@ export class FieldValuesBreakdownScene extends SceneObjectBase, }), - new SceneFlexItem({ - key: SINGLE_GRAPH_KEY, - minHeight: 300, - body: PanelBuilders.timeseries().setTitle(optionValue).setMenu(new PanelMenu({})).build(), - }), + getValueSummaryPanel(optionValue), new SceneReactObject({ reactNode: , }), diff --git a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx index f3c8f9c6..45118d96 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx @@ -36,7 +36,7 @@ import { ClearFiltersLayoutScene } from './ClearFiltersLayoutScene'; import { EmptyLayoutScene } from './EmptyLayoutScene'; import { IndexScene } from '../../IndexScene/IndexScene'; import { clearVariables, getVariablesThatCanBeCleared } from '../../../services/variableHelpers'; -import { SINGLE_GRAPH_KEY } from './FieldValuesBreakdownScene'; +import { getValueSummaryPanel } from './Panels/ValueSummary'; type DisplayError = DataQueryError & { displayed: boolean }; type DisplayErrors = Record; @@ -236,11 +236,7 @@ export class LabelValuesBreakdownScene extends SceneObjectBase }), - new SceneFlexItem({ - minHeight: 300, - key: SINGLE_GRAPH_KEY, - body: body.clone(), - }), + getValueSummaryPanel(tagKey, { levelColor: true }), new SceneReactObject({ reactNode: }), new ByFrameRepeater({ body: new SceneCSSGridLayout({ @@ -272,11 +268,7 @@ export class LabelValuesBreakdownScene extends SceneObjectBase }), - new SceneFlexItem({ - minHeight: 300, - key: SINGLE_GRAPH_KEY, - body: body.clone(), - }), + getValueSummaryPanel(tagKey, { levelColor: true }), new SceneReactObject({ reactNode: }), new ByFrameRepeater({ body: new SceneCSSGridLayout({ diff --git a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts new file mode 100644 index 00000000..d3160c09 --- /dev/null +++ b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts @@ -0,0 +1,55 @@ +import { PanelBuilders, SceneFlexItem, VizPanel } from '@grafana/scenes'; +import { CollapsablePanelType, PanelMenu } from '../../../Panels/PanelMenu'; +import { DrawStyle, StackingMode } from '@grafana/ui'; +import { setLevelColorOverrides } from '../../../../services/panel'; +import { getPanelOption } from '../../../../services/store'; +import { Options } from '@grafana/schema/dist/esm/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen'; + +export function getValueSummaryPanel(title: string, options?: { levelColor?: boolean }) { + console.log('getValueSummaryPanel', title, options); + + const collapsable = + getPanelOption('collapsable', [CollapsablePanelType.collapse, CollapsablePanelType.expand]) ?? + CollapsablePanelType.collapse; + + const body = PanelBuilders.timeseries() + .setTitle(title) + .setMenu( + new PanelMenu({ + collapsable, + }) + ) + .setCustomFieldConfig('stacking', { mode: StackingMode.Normal }) + .setCustomFieldConfig('fillOpacity', 100) + .setCustomFieldConfig('lineWidth', 0) + .setCustomFieldConfig('pointSize', 0) + .setCustomFieldConfig('drawStyle', DrawStyle.Bars); + + if (options?.levelColor) { + body.setOverrides(setLevelColorOverrides); + } + const build: VizPanel = body.build(); + + return new SceneFlexItem({ + key: VALUE_SUMMARY_PANEL_KEY, + minHeight: getValueSummaryHeight(collapsable), + height: getValueSummaryHeight(collapsable), + maxHeight: getValueSummaryHeight(collapsable), + + body: build, + }); +} + +export function setValueSummaryHeight(vizPanelFlexItem: SceneFlexItem, collapsableState: CollapsablePanelType) { + vizPanelFlexItem.setState({ + minHeight: getValueSummaryHeight(collapsableState), + height: getValueSummaryHeight(collapsableState), + maxHeight: getValueSummaryHeight(collapsableState), + }); +} + +function getValueSummaryHeight(collapsableState: CollapsablePanelType) { + return collapsableState === CollapsablePanelType.collapse ? 300 : 35; +} + +export const VALUE_SUMMARY_PANEL_KEY = 'value_summary_panel'; diff --git a/src/services/store.ts b/src/services/store.ts index cfea955e..5b64c7a1 100644 --- a/src/services/store.ts +++ b/src/services/store.ts @@ -6,7 +6,7 @@ import { logger } from './logger'; import { SERVICE_NAME } from './variables'; import { Options } from '@grafana/schema/dist/esm/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen'; import { unknownToStrings } from './narrowing'; -import { AvgFieldPanelType } from '../Components/Panels/PanelMenu'; +import { AvgFieldPanelType, CollapsablePanelType } from '../Components/Panels/PanelMenu'; const FAVORITE_PRIMARY_LABEL_VALUES_LOCALSTORAGE_KEY = `${pluginJson.id}.services.favorite`; const FAVORITE_PRIMARY_LABEL_NAME_LOCALSTORAGE_KEY = `${pluginJson.id}.primarylabels.tabs.favorite`; @@ -240,6 +240,7 @@ export function setLogsVisualizationType(type: string) { const PANEL_OPTIONS_LOCALSTORAGE_KEY = `${pluginJson.id}.panel.option`; export interface PanelOptions { panelType: AvgFieldPanelType; + collapsable: CollapsablePanelType; } export function getPanelOption( option: K, From db1f4cd4674a0f15391d9297c058da4859b4105f Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 4 Dec 2024 13:39:39 -0600 Subject: [PATCH 10/34] chore: wip map field name to color --- src/Components/ServiceScene/ServiceScene.tsx | 2 +- src/services/fields.ts | 2 +- src/services/panel.ts | 93 +++++++++++++++++++- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/Components/ServiceScene/ServiceScene.tsx b/src/Components/ServiceScene/ServiceScene.tsx index 6880e915..76347fc6 100644 --- a/src/Components/ServiceScene/ServiceScene.tsx +++ b/src/Components/ServiceScene/ServiceScene.tsx @@ -56,7 +56,7 @@ import { import { replaceSlash } from '../../services/extensions/links'; import { ShowLogsButtonScene } from '../IndexScene/ShowLogsButtonScene'; -const LOGS_PANEL_QUERY_REFID = 'logsPanelQuery'; +export const LOGS_PANEL_QUERY_REFID = 'logsPanelQuery'; const PATTERNS_QUERY_REFID = 'patterns'; const DETECTED_LABELS_QUERY_REFID = 'detectedLabels'; const DETECTED_FIELDS_QUERY_REFID = 'detectedFields'; diff --git a/src/services/fields.ts b/src/services/fields.ts index efeb63bf..91754863 100644 --- a/src/services/fields.ts +++ b/src/services/fields.ts @@ -146,7 +146,7 @@ export function getFilterBreakdownValueScene( transformations: [() => selectFrameTransformation(frame)], }) ) - .setColor({ mode: 'fixed', fixedColor: getColorByIndex(frameIndex) }) + // .setColor({ mode: 'fixed', fixedColor: getColorByIndex(frameIndex) }) .setOverrides(setLevelColorOverrides) .setMenu(new PanelMenu({ frame, fieldName: getTitle(frame), labelName: labelKey })) .setHeaderActions([new AddToFiltersButton({ frame, variableName })]); diff --git a/src/services/panel.ts b/src/services/panel.ts index d1f3f18a..17336fcf 100644 --- a/src/services/panel.ts +++ b/src/services/panel.ts @@ -1,4 +1,4 @@ -import { DataFrame, FieldConfig, FieldMatcherID } from '@grafana/data'; +import { DataFrame, FieldConfig, FieldMatcherID, FieldType, getFieldDisplayName } from '@grafana/data'; import { FieldConfigBuilder, FieldConfigBuilders, @@ -18,6 +18,8 @@ import { LogsSceneQueryRunner } from './LogsSceneQueryRunner'; import { DrawStyle, StackingMode } from '@grafana/ui'; import { getLabelsFromSeries, getVisibleLevels } from './levels'; import { LokiQuery } from './lokiQuery'; +import { config } from '@grafana/runtime'; +import { LOGS_PANEL_QUERY_REFID } from '../Components/ServiceScene/ServiceScene'; const UNKNOWN_LEVEL_LOGS = 'logs'; export function setLevelColorOverrides(overrides: FieldConfigOverridesBuilder) { @@ -94,6 +96,85 @@ export function syncLogsPanelVisibleSeries(panel: VizPanel, series: DataFrame[], } } +export function getColorByName(name: string, paletteSet: Set) { + const visTheme = config.theme2.visualization; + const hash = Math.abs(getHash(name)); + const paletteSize = 57; + // There are 56 colors in the palette, if we use more of the palette we're less likely to get duplicates, but the colors are harder to differntiate + // Also, it's possible that a panel with N series has the same color for each series + // See the birthday problem for more: 8 series will have ~40% at least 2 series get the same color + // Opposed to the previous implementation, we cycled between 8 series, so every panel with 8+ series was guaranteed to have 1 duplicate, but the color of each series would change depending on the sort order + // Also the first 8 colors in the palette are visually dissimilar, but some of the full set of 56 are pretty hard to differentiate visually + // If the color is out of bounds (i.e. 57) we get a gray color, which looks different enough from the others + let paletteIndex = hash % paletteSize; + // const initialPaletteIndex = paletteIndex + + // function grabNextBucket(index: number) { + // // console.log('grabNextBucket', {index, size: paletteSet.size}) + // if (paletteSet.has(index)) { + // return true + // } else { + // paletteSet.add(index) + // return false + // } + // } + // + // while(grabNextBucket(paletteIndex) && paletteSet.size < paletteSize){ + // paletteIndex = paletteIndex + 1 % paletteSize + // } + // if(paletteSet.size === paletteSize){ + // // console.log('clear palette index set') + // paletteSet.clear() + // } + + return visTheme.getColorByName(visTheme.palette[paletteIndex]); +} + +function getHash(input: string) { + let hash = 0, + len = input.length; + for (let i = 0; i < len; i++) { + hash = (hash << 5) - hash + input.charCodeAt(i); + hash |= 0; // to 32bit integer + } + return hash; +} + +export function setFixedColorByDisplayNameTransformation() { + return (source: Observable) => { + return source.pipe( + map((data: DataFrame[]) => { + if (data?.[0]?.refId === LOGS_PANEL_QUERY_REFID) { + return data; + } + const paletteSet = new Set(); + return data.map((frame, frameIndex) => { + return { + ...frame, + fields: frame.fields.map((f, fieldIndex) => { + // Time fields do not have color config + if (f.type === FieldType.time) { + return f; + } + const displayName = getFieldDisplayName(f, frame, data); + return { + ...f, + config: { + ...f.config, + color: { + fixedColor: getColorByName(displayName, paletteSet), + mode: 'fixed', + }, + }, + }; + }), + }; + }); + }) + ); + }; +} + export function sortLevelTransformation() { return (source: Observable) => { return source.pipe( @@ -149,9 +230,13 @@ export function getQueryRunner(queries: LokiQuery[], queryRunnerOptions?: Partia }); } - return getSceneQueryRunner({ - queries: queries, - ...queryRunnerOptions, + return new SceneDataTransformer({ + $data: getSceneQueryRunner({ + datasource: { uid: WRAPPED_LOKI_DS_UID }, + queries: queries, + ...queryRunnerOptions, + }), + transformations: [setFixedColorByDisplayNameTransformation], }); } From 06c4d9dfed72a342a1f66544bb5f1097c6ee06da Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 4 Dec 2024 13:50:15 -0600 Subject: [PATCH 11/34] chore: remove unused --- src/services/fields.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/fields.ts b/src/services/fields.ts index 91754863..3c5f26dc 100644 --- a/src/services/fields.ts +++ b/src/services/fields.ts @@ -7,7 +7,6 @@ import { SceneDataTransformer, SceneObject, } from '@grafana/scenes'; -import { getColorByIndex } from './scenes'; import { AddToFiltersButton, VariableFilterType } from 'Components/ServiceScene/Breakdowns/AddToFiltersButton'; import { DetectedFieldType, @@ -146,7 +145,6 @@ export function getFilterBreakdownValueScene( transformations: [() => selectFrameTransformation(frame)], }) ) - // .setColor({ mode: 'fixed', fixedColor: getColorByIndex(frameIndex) }) .setOverrides(setLevelColorOverrides) .setMenu(new PanelMenu({ frame, fieldName: getTitle(frame), labelName: labelKey })) .setHeaderActions([new AddToFiltersButton({ frame, variableName })]); From 4bce44d3244a38dc08ce536d9eede6418681e315 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 4 Dec 2024 13:55:58 -0600 Subject: [PATCH 12/34] chore: spellcheck --- src/services/panel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/panel.ts b/src/services/panel.ts index 17336fcf..aa93a08c 100644 --- a/src/services/panel.ts +++ b/src/services/panel.ts @@ -100,7 +100,7 @@ export function getColorByName(name: string, paletteSet: Set) { const visTheme = config.theme2.visualization; const hash = Math.abs(getHash(name)); const paletteSize = 57; - // There are 56 colors in the palette, if we use more of the palette we're less likely to get duplicates, but the colors are harder to differntiate + // There are 56 colors in the palette, if we use more of the palette we're less likely to get duplicates, but the colors are harder to differentiate // Also, it's possible that a panel with N series has the same color for each series // See the birthday problem for more: 8 series will have ~40% at least 2 series get the same color // Opposed to the previous implementation, we cycled between 8 series, so every panel with 8+ series was guaranteed to have 1 duplicate, but the color of each series would change depending on the sort order From 1b8eaff85e0dd4686002d216b091fc6064a82f34 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 4 Dec 2024 14:09:27 -0600 Subject: [PATCH 13/34] fix: broken panel menu on values breakdown --- src/Components/Panels/PanelMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Panels/PanelMenu.tsx b/src/Components/Panels/PanelMenu.tsx index 2e5512cd..a494fb34 100644 --- a/src/Components/Panels/PanelMenu.tsx +++ b/src/Components/Panels/PanelMenu.tsx @@ -223,7 +223,7 @@ const getExploreLink = (sceneRef: SceneObject) => { if (queryProvider instanceof SceneQueryRunner) { queryRunner = queryProvider; } else { - logger.error(new Error('query provider not found!')); + queryRunner = sceneGraph.findDescendents(queryProvider, SceneQueryRunner)[0]; } } const uninterpolatedExpr: string | undefined = queryRunner.state.queries[0].expr; From cdd85dbb1cc7fb30e3077d27cfdfbc08a0331ea4 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 4 Dec 2024 14:10:43 -0600 Subject: [PATCH 14/34] chore: use helper --- src/Components/Panels/PanelMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Panels/PanelMenu.tsx b/src/Components/Panels/PanelMenu.tsx index a494fb34..6a0683ed 100644 --- a/src/Components/Panels/PanelMenu.tsx +++ b/src/Components/Panels/PanelMenu.tsx @@ -223,7 +223,7 @@ const getExploreLink = (sceneRef: SceneObject) => { if (queryProvider instanceof SceneQueryRunner) { queryRunner = queryProvider; } else { - queryRunner = sceneGraph.findDescendents(queryProvider, SceneQueryRunner)[0]; + queryRunner = getQueryRunnerFromChildren(queryProvider)[0]; } } const uninterpolatedExpr: string | undefined = queryRunner.state.queries[0].expr; From 4cbb0e509ad3c27647b001fa59f95cfcfebf2fc5 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 4 Dec 2024 15:10:39 -0600 Subject: [PATCH 15/34] chore: wip - refactoring series limit --- src/Components/Panels/PanelMenu.tsx | 25 ++++-- .../FieldsAggregatedBreakdownScene.tsx | 17 +++- .../LabelsAggregatedBreakdownScene.tsx | 17 +++- .../Breakdowns/Panels/ValueSummary.ts | 2 - .../TimeSeriesLimitSeriesTitleItem.tsx | 84 ++++++++++++------- 5 files changed, 100 insertions(+), 45 deletions(-) diff --git a/src/Components/Panels/PanelMenu.tsx b/src/Components/Panels/PanelMenu.tsx index 2e5512cd..34c0f01a 100644 --- a/src/Components/Panels/PanelMenu.tsx +++ b/src/Components/Panels/PanelMenu.tsx @@ -26,6 +26,8 @@ import { setLevelColorOverrides } from '../../services/panel'; import { setPanelOption } from '../../services/store'; import { FieldsAggregatedBreakdownScene } from '../ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene'; import { setValueSummaryHeight } from '../ServiceScene/Breakdowns/Panels/ValueSummary'; +import { FieldValuesBreakdownScene } from '../ServiceScene/Breakdowns/FieldValuesBreakdownScene'; +import { LabelValuesBreakdownScene } from '../ServiceScene/Breakdowns/LabelValuesBreakdownScene'; const ADD_TO_INVESTIGATION_MENU_TEXT = 'Add to investigation'; const ADD_TO_INVESTIGATION_MENU_DIVIDER_TEXT = 'Investigations'; @@ -151,8 +153,6 @@ function addCollapsableItem(items: PanelMenuItem[], menu: PanelMenu) { ? CollapsablePanelType.collapse : CollapsablePanelType.expand; - console.log('newCollapsableState', { newCollapsableState, currentState: menu.state.collapsable }); - // Update the viz const vizPanelFlexItem = sceneGraph.getAncestor(menu, SceneFlexItem); setValueSummaryHeight(vizPanelFlexItem, newCollapsableState); @@ -217,13 +217,22 @@ const getExploreLink = (sceneRef: SceneObject) => { // If we don't have a query runner, then our panel is within a SceneCSSGridItem, we need to get the query runner from there if (!queryRunner) { - const sceneGridItem = sceneGraph.getAncestor(sceneRef, SceneCSSGridItem); - const queryProvider = sceneGraph.getData(sceneGridItem); - - if (queryProvider instanceof SceneQueryRunner) { - queryRunner = queryProvider; + const breakdownScene = sceneGraph.findObject( + sceneRef, + (o) => o instanceof FieldValuesBreakdownScene || o instanceof LabelValuesBreakdownScene + ); + if (breakdownScene) { + const queryProvider = sceneGraph.getData(breakdownScene); + + if (queryProvider instanceof SceneQueryRunner) { + queryRunner = queryProvider; + } else { + queryRunner = getQueryRunnerFromChildren(queryProvider)[0]; + } } else { - logger.error(new Error('query provider not found!')); + logger.error(new Error('Unable to locate query runner!'), { + msg: 'PanelMenu - getExploreLink: Unable to locate query runner!', + }); } } const uninterpolatedExpr: string | undefined = queryRunner.state.queries[0].expr; diff --git a/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx index 111b22d1..4ada1d43 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx @@ -8,6 +8,7 @@ import { sceneGraph, SceneObjectBase, SceneObjectState, + SceneQueryRunner, VizPanel, } from '@grafana/scenes'; import { ALL_VARIABLE_VALUE, DetectedFieldType, ParserType } from '../../../services/variables'; @@ -123,7 +124,7 @@ export class FieldsAggregatedBreakdownScene extends SceneObjectBase { - limitMaxNumberOfSeriesForPanel(child); + this.addLimitUIToChild(child); this.subscribeToPanel(child); }); @@ -136,6 +137,18 @@ export class FieldsAggregatedBreakdownScene extends SceneObjectBase) { return (a: SceneCSSGridItem, b: SceneCSSGridItem) => { const aPanel = a.state.body as VizPanel; @@ -209,7 +222,7 @@ export class FieldsAggregatedBreakdownScene extends SceneObjectBase { - limitMaxNumberOfSeriesForPanel(child); + this.addLimitUIToChild(child); this.subscribeToPanel(child); }); diff --git a/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx index 384ca870..90ca0aea 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx @@ -28,6 +28,7 @@ import { LokiQuery } from '../../../services/lokiQuery'; import { ServiceScene } from '../ServiceScene'; import { DataFrame, LoadingState } from '@grafana/data'; import { PanelMenu, getPanelWrapperStyles } from '../../Panels/PanelMenu'; +import { logger } from '../../../services/logger'; export interface LabelsAggregatedBreakdownSceneState extends SceneObjectState { body?: LayoutSwitcher; @@ -143,7 +144,7 @@ export class LabelsAggregatedBreakdownScene extends SceneObjectBase { - limitMaxNumberOfSeriesForPanel(child); + this.addLimitUIToChild(child); }); layout.setState({ @@ -152,6 +153,18 @@ export class LabelsAggregatedBreakdownScene extends SceneObjectBase(); if (detectedLabels?.length) { @@ -182,7 +195,7 @@ export class LabelsAggregatedBreakdownScene extends SceneObjectBase { - limitMaxNumberOfSeriesForPanel(child); + this.addLimitUIToChild(child); }); return new LayoutSwitcher({ diff --git a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts index d3160c09..81846900 100644 --- a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts +++ b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts @@ -6,8 +6,6 @@ import { getPanelOption } from '../../../../services/store'; import { Options } from '@grafana/schema/dist/esm/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen'; export function getValueSummaryPanel(title: string, options?: { levelColor?: boolean }) { - console.log('getValueSummaryPanel', title, options); - const collapsable = getPanelOption('collapsable', [CollapsablePanelType.collapse, CollapsablePanelType.expand]) ?? CollapsablePanelType.collapse; diff --git a/src/Components/ServiceScene/Breakdowns/TimeSeriesLimitSeriesTitleItem.tsx b/src/Components/ServiceScene/Breakdowns/TimeSeriesLimitSeriesTitleItem.tsx index 9d5b8425..a3bf2058 100644 --- a/src/Components/ServiceScene/Breakdowns/TimeSeriesLimitSeriesTitleItem.tsx +++ b/src/Components/ServiceScene/Breakdowns/TimeSeriesLimitSeriesTitleItem.tsx @@ -4,11 +4,11 @@ import { GrafanaTheme2, LoadingState, PanelData } from '@grafana/data'; import { css } from '@emotion/css'; import { SceneComponentProps, - SceneCSSGridItem, SceneDataTransformer, sceneGraph, SceneObjectBase, SceneObjectState, + SceneQueryRunner, VizPanel, } from '@grafana/scenes'; @@ -18,6 +18,8 @@ export interface TimeSeriesLimitSeriesTitleItemSceneState extends SceneObjectSta toggleShowAllSeries: (model: TimeSeriesLimitSeriesTitleItemScene) => void; showAllSeries: boolean; currentSeriesCount?: number; + defaultSeriesLimit: number; + dataTransformer: SceneDataTransformer; } export class TimeSeriesLimitSeriesTitleItemScene extends SceneObjectBase { @@ -41,8 +43,9 @@ export class TimeSeriesLimitSeriesTitleItemScene extends SceneObjectBase) => { - const { toggleShowAllSeries, showAllSeries, currentSeriesCount } = model.useState(); - const $data = sceneGraph.getData(model); + const { toggleShowAllSeries, showAllSeries, currentSeriesCount, defaultSeriesLimit, dataTransformer } = + model.useState(); + const $data = dataTransformer; const { data } = $data.useState(); const styles = useStyles2(getStyles); @@ -51,7 +54,7 @@ export class TimeSeriesLimitSeriesTitleItemScene extends SceneObjectBase <> - { - dataTransformer.setState({ - transformations: [], - }); - timeSeriesLimiter.setState({ - showAllSeries: true, - }); - dataTransformer.reprocessTransformations(); - }, - }), - ], - }); - } +export function limitMaxNumberOfSeriesForPanel( + panel: VizPanel, + dataTransformer: SceneDataTransformer, + defaultSeriesLimit = MAX_NUMBER_OF_TIME_SERIES +) { + panel?.setState({ + titleItems: [ + new TimeSeriesLimitSeriesTitleItemScene({ + dataTransformer: dataTransformer, + defaultSeriesLimit: defaultSeriesLimit, + showAllSeries: false, + toggleShowAllSeries: (timeSeriesLimiter) => { + dataTransformer.setState({ + transformations: [], + }); + timeSeriesLimiter.setState({ + showAllSeries: true, + }); + dataTransformer.reprocessTransformations(); + }, + }), + ], + }); } +// export function limitMaxNumberOfSeriesForPanel(child: SceneCSSGridItem) { +// const panel = child.state.body as VizPanel | undefined; +// const dataTransformer = child.state.body?.state.$data; +// if (dataTransformer instanceof SceneDataTransformer) { +// panel?.setState({ +// titleItems: [ +// new TimeSeriesLimitSeriesTitleItemScene({ +// showAllSeries: false, +// toggleShowAllSeries: (timeSeriesLimiter) => { +// dataTransformer.setState({ +// transformations: [], +// }); +// timeSeriesLimiter.setState({ +// showAllSeries: true, +// }); +// dataTransformer.reprocessTransformations(); +// }, +// }), +// ], +// }); +// } +// } const getStyles = (theme: GrafanaTheme2) => ({ timeSeriesDisclaimer: css({ From c086f6147d778b25af059a14b8e9a46ee8504ebc Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 4 Dec 2024 15:51:23 -0600 Subject: [PATCH 16/34] chore: add limit to summary panel --- .../Breakdowns/Panels/ValueSummary.ts | 14 +++++- .../TimeSeriesLimitSeriesTitleItem.tsx | 44 ++++++------------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts index 81846900..fd78917d 100644 --- a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts +++ b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts @@ -1,17 +1,26 @@ -import { PanelBuilders, SceneFlexItem, VizPanel } from '@grafana/scenes'; +import { PanelBuilders, SceneDataTransformer, SceneFlexItem, VizPanel } from '@grafana/scenes'; import { CollapsablePanelType, PanelMenu } from '../../../Panels/PanelMenu'; import { DrawStyle, StackingMode } from '@grafana/ui'; import { setLevelColorOverrides } from '../../../../services/panel'; import { getPanelOption } from '../../../../services/store'; import { Options } from '@grafana/schema/dist/esm/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen'; +import { limitMaxNumberOfSeriesForPanel } from '../TimeSeriesLimitSeriesTitleItem'; +import { limitFramesTransformation } from '../FieldsAggregatedBreakdownScene'; + +const SUMMARY_PANEL_SERIES_LIMIT = 100; export function getValueSummaryPanel(title: string, options?: { levelColor?: boolean }) { const collapsable = getPanelOption('collapsable', [CollapsablePanelType.collapse, CollapsablePanelType.expand]) ?? CollapsablePanelType.collapse; + const $data = new SceneDataTransformer({ + transformations: [() => limitFramesTransformation(SUMMARY_PANEL_SERIES_LIMIT)], + }); + const body = PanelBuilders.timeseries() .setTitle(title) + .setData($data) .setMenu( new PanelMenu({ collapsable, @@ -27,6 +36,9 @@ export function getValueSummaryPanel(title: string, options?: { levelColor?: boo body.setOverrides(setLevelColorOverrides); } const build: VizPanel = body.build(); + build.addActivationHandler(() => { + limitMaxNumberOfSeriesForPanel(build, $data, SUMMARY_PANEL_SERIES_LIMIT); + }); return new SceneFlexItem({ key: VALUE_SUMMARY_PANEL_KEY, diff --git a/src/Components/ServiceScene/Breakdowns/TimeSeriesLimitSeriesTitleItem.tsx b/src/Components/ServiceScene/Breakdowns/TimeSeriesLimitSeriesTitleItem.tsx index a3bf2058..b7b6b8e4 100644 --- a/src/Components/ServiceScene/Breakdowns/TimeSeriesLimitSeriesTitleItem.tsx +++ b/src/Components/ServiceScene/Breakdowns/TimeSeriesLimitSeriesTitleItem.tsx @@ -11,6 +11,8 @@ import { SceneQueryRunner, VizPanel, } from '@grafana/scenes'; +import { LabelValuesBreakdownScene } from './LabelValuesBreakdownScene'; +import { FieldValuesBreakdownScene } from './FieldValuesBreakdownScene'; export const MAX_NUMBER_OF_TIME_SERIES = 20; @@ -19,7 +21,6 @@ export interface TimeSeriesLimitSeriesTitleItemSceneState extends SceneObjectSta showAllSeries: boolean; currentSeriesCount?: number; defaultSeriesLimit: number; - dataTransformer: SceneDataTransformer; } export class TimeSeriesLimitSeriesTitleItemScene extends SceneObjectBase { @@ -30,10 +31,17 @@ export class TimeSeriesLimitSeriesTitleItemScene extends SceneObjectBase o instanceof LabelValuesBreakdownScene || o instanceof FieldValuesBreakdownScene + ); + const $data = sceneGraph.findDescendents( + valueBreakdown ?? sceneGraph.getAncestor(this, VizPanel), + SceneQueryRunner + )[0]; + this._subs.add( - panel.subscribeToState((newState, prevState) => { - const $data = sceneGraph.getData(this); + $data.subscribeToState((newState, prevState) => { if ($data.state.data?.state === LoadingState.Done) { this.setState({ currentSeriesCount: $data.state.data?.series.length, @@ -43,9 +51,8 @@ export class TimeSeriesLimitSeriesTitleItemScene extends SceneObjectBase) => { - const { toggleShowAllSeries, showAllSeries, currentSeriesCount, defaultSeriesLimit, dataTransformer } = - model.useState(); - const $data = dataTransformer; + const { toggleShowAllSeries, showAllSeries, currentSeriesCount, defaultSeriesLimit } = model.useState(); + const $data = sceneGraph.getData(model); const { data } = $data.useState(); const styles = useStyles2(getStyles); @@ -91,7 +98,6 @@ export function limitMaxNumberOfSeriesForPanel( panel?.setState({ titleItems: [ new TimeSeriesLimitSeriesTitleItemScene({ - dataTransformer: dataTransformer, defaultSeriesLimit: defaultSeriesLimit, showAllSeries: false, toggleShowAllSeries: (timeSeriesLimiter) => { @@ -107,28 +113,6 @@ export function limitMaxNumberOfSeriesForPanel( ], }); } -// export function limitMaxNumberOfSeriesForPanel(child: SceneCSSGridItem) { -// const panel = child.state.body as VizPanel | undefined; -// const dataTransformer = child.state.body?.state.$data; -// if (dataTransformer instanceof SceneDataTransformer) { -// panel?.setState({ -// titleItems: [ -// new TimeSeriesLimitSeriesTitleItemScene({ -// showAllSeries: false, -// toggleShowAllSeries: (timeSeriesLimiter) => { -// dataTransformer.setState({ -// transformations: [], -// }); -// timeSeriesLimiter.setState({ -// showAllSeries: true, -// }); -// dataTransformer.reprocessTransformations(); -// }, -// }), -// ], -// }); -// } -// } const getStyles = (theme: GrafanaTheme2) => ({ timeSeriesDisclaimer: css({ From cdbb9f251cfed47bc2dbe9630db26f26e776f9c7 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 4 Dec 2024 15:55:28 -0600 Subject: [PATCH 17/34] chore: unused import --- .../ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx index 4ada1d43..b51afe4c 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx @@ -8,7 +8,6 @@ import { sceneGraph, SceneObjectBase, SceneObjectState, - SceneQueryRunner, VizPanel, } from '@grafana/scenes'; import { ALL_VARIABLE_VALUE, DetectedFieldType, ParserType } from '../../../services/variables'; From d32818e0cfbe10bc0b52ef3bc0503eecf736fd86 Mon Sep 17 00:00:00 2001 From: Galen Date: Thu, 5 Dec 2024 10:41:33 -0600 Subject: [PATCH 18/34] chore: clean up, add series limit to summary panel --- .../Breakdowns/LabelsAggregatedBreakdownScene.tsx | 1 - .../ServiceScene/Breakdowns/Panels/ValueSummary.ts | 14 ++------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx index ca6bdba1..1328c6e2 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx @@ -25,7 +25,6 @@ import { ServiceScene } from '../ServiceScene'; import { DataFrame, LoadingState } from '@grafana/data'; import { getPanelWrapperStyles, PanelMenu } from '../../Panels/PanelMenu'; import { MAX_NUMBER_OF_TIME_SERIES } from './TimeSeriesLimit'; -import { logger } from '../../../services/logger'; export interface LabelsAggregatedBreakdownSceneState extends SceneObjectState { body?: LayoutSwitcher; diff --git a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts index fd78917d..97cec748 100644 --- a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts +++ b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts @@ -1,11 +1,9 @@ -import { PanelBuilders, SceneDataTransformer, SceneFlexItem, VizPanel } from '@grafana/scenes'; +import { PanelBuilders, SceneFlexItem, VizPanel } from '@grafana/scenes'; import { CollapsablePanelType, PanelMenu } from '../../../Panels/PanelMenu'; import { DrawStyle, StackingMode } from '@grafana/ui'; import { setLevelColorOverrides } from '../../../../services/panel'; import { getPanelOption } from '../../../../services/store'; import { Options } from '@grafana/schema/dist/esm/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen'; -import { limitMaxNumberOfSeriesForPanel } from '../TimeSeriesLimitSeriesTitleItem'; -import { limitFramesTransformation } from '../FieldsAggregatedBreakdownScene'; const SUMMARY_PANEL_SERIES_LIMIT = 100; @@ -14,13 +12,8 @@ export function getValueSummaryPanel(title: string, options?: { levelColor?: boo getPanelOption('collapsable', [CollapsablePanelType.collapse, CollapsablePanelType.expand]) ?? CollapsablePanelType.collapse; - const $data = new SceneDataTransformer({ - transformations: [() => limitFramesTransformation(SUMMARY_PANEL_SERIES_LIMIT)], - }); - const body = PanelBuilders.timeseries() .setTitle(title) - .setData($data) .setMenu( new PanelMenu({ collapsable, @@ -30,22 +23,19 @@ export function getValueSummaryPanel(title: string, options?: { levelColor?: boo .setCustomFieldConfig('fillOpacity', 100) .setCustomFieldConfig('lineWidth', 0) .setCustomFieldConfig('pointSize', 0) + .setSeriesLimit(SUMMARY_PANEL_SERIES_LIMIT) .setCustomFieldConfig('drawStyle', DrawStyle.Bars); if (options?.levelColor) { body.setOverrides(setLevelColorOverrides); } const build: VizPanel = body.build(); - build.addActivationHandler(() => { - limitMaxNumberOfSeriesForPanel(build, $data, SUMMARY_PANEL_SERIES_LIMIT); - }); return new SceneFlexItem({ key: VALUE_SUMMARY_PANEL_KEY, minHeight: getValueSummaryHeight(collapsable), height: getValueSummaryHeight(collapsable), maxHeight: getValueSummaryHeight(collapsable), - body: build, }); } From 2300cead4881463f3acf309663f87386f61c9aa1 Mon Sep 17 00:00:00 2001 From: Galen Date: Thu, 5 Dec 2024 11:14:18 -0600 Subject: [PATCH 19/34] chore: update collapsed --- src/Components/Panels/PanelMenu.tsx | 27 ++++++------- .../Breakdowns/Panels/ValueSummary.ts | 39 ++++++++++++------- src/services/store.ts | 2 +- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/Components/Panels/PanelMenu.tsx b/src/Components/Panels/PanelMenu.tsx index 34c0f01a..2c58866a 100644 --- a/src/Components/Panels/PanelMenu.tsx +++ b/src/Components/Panels/PanelMenu.tsx @@ -38,8 +38,8 @@ export enum AvgFieldPanelType { } export enum CollapsablePanelType { - collapse = 'Collapse', - expand = 'Expand', + collapsed = 'Collapse', + expanded = 'Expand', } interface PanelMenuState extends SceneObjectState { @@ -49,7 +49,6 @@ interface PanelMenuState extends SceneObjectState { fieldName?: string; addToExplorations?: AddToExplorationButton; panelType?: AvgFieldPanelType; - collapsable?: CollapsablePanelType; } /** @@ -59,6 +58,8 @@ export class PanelMenu extends SceneObjectBase implements VizPan constructor(state: Partial) { super(state); this.addActivationHandler(() => { + const viz = sceneGraph.getAncestor(this, VizPanel); + this.setState({ addToExplorations: new AddToExplorationButton({ labelName: this.state.labelName, @@ -86,11 +87,11 @@ export class PanelMenu extends SceneObjectBase implements VizPan ]; // Visualization options - if (this.state.panelType || this.state.collapsable) { + if (this.state.panelType || viz.state.collapsible) { addVisualizationHeader(items, this); } - if (this.state.collapsable) { + if (viz.state.collapsible) { addCollapsableItem(items, this); } @@ -144,22 +145,22 @@ function addVisualizationHeader(items: PanelMenuItem[], sceneRef: PanelMenu) { } function addCollapsableItem(items: PanelMenuItem[], menu: PanelMenu) { + const viz = sceneGraph.getAncestor(menu, VizPanel); items.push({ - text: menu.state.collapsable ?? CollapsablePanelType.expand, - iconClassName: menu.state.collapsable === CollapsablePanelType.collapse ? 'table-collapse-all' : 'table-expand-all', + text: viz.state.collapsed ? CollapsablePanelType.expanded : CollapsablePanelType.collapsed, + iconClassName: viz.state.collapsed ? 'table-collapse-all' : 'table-expand-all', onClick: () => { - const newCollapsableState = - menu.state.collapsable === CollapsablePanelType.expand - ? CollapsablePanelType.collapse - : CollapsablePanelType.expand; + const newCollapsableState = viz.state.collapsed ? CollapsablePanelType.expanded : CollapsablePanelType.collapsed; // Update the viz const vizPanelFlexItem = sceneGraph.getAncestor(menu, SceneFlexItem); setValueSummaryHeight(vizPanelFlexItem, newCollapsableState); // Set state and update local storage - menu.setState({ collapsable: newCollapsableState }); - setPanelOption('collapsable', newCollapsableState); + viz.setState({ + collapsed: !viz.state.collapsed, + }); + setPanelOption('collapsed', newCollapsableState); }, }); } diff --git a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts index 97cec748..713f77d5 100644 --- a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts +++ b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts @@ -1,4 +1,4 @@ -import { PanelBuilders, SceneFlexItem, VizPanel } from '@grafana/scenes'; +import { PanelBuilders, SceneFlexItem, sceneGraph, VizPanel } from '@grafana/scenes'; import { CollapsablePanelType, PanelMenu } from '../../../Panels/PanelMenu'; import { DrawStyle, StackingMode } from '@grafana/ui'; import { setLevelColorOverrides } from '../../../../services/panel'; @@ -8,17 +8,15 @@ import { Options } from '@grafana/schema/dist/esm/raw/composable/timeseries/pane const SUMMARY_PANEL_SERIES_LIMIT = 100; export function getValueSummaryPanel(title: string, options?: { levelColor?: boolean }) { - const collapsable = - getPanelOption('collapsable', [CollapsablePanelType.collapse, CollapsablePanelType.expand]) ?? - CollapsablePanelType.collapse; + const collapsed = + getPanelOption('collapsed', [CollapsablePanelType.collapsed, CollapsablePanelType.expanded]) ?? + CollapsablePanelType.collapsed; const body = PanelBuilders.timeseries() .setTitle(title) - .setMenu( - new PanelMenu({ - collapsable, - }) - ) + .setMenu(new PanelMenu({})) + .setCollapsible(true) + .setCollapsed(collapsed === CollapsablePanelType.collapsed) .setCustomFieldConfig('stacking', { mode: StackingMode.Normal }) .setCustomFieldConfig('fillOpacity', 100) .setCustomFieldConfig('lineWidth', 0) @@ -31,11 +29,26 @@ export function getValueSummaryPanel(title: string, options?: { levelColor?: boo } const build: VizPanel = body.build(); + build.addActivationHandler(() => { + if (build.state.collapsible) { + // @todo handle unsub + build.subscribeToState((newState, prevState) => { + if (newState.collapsed !== prevState.collapsed) { + const vizPanelFlexItem = sceneGraph.getAncestor(build, SceneFlexItem); + setValueSummaryHeight( + vizPanelFlexItem, + newState.collapsed ? CollapsablePanelType.collapsed : CollapsablePanelType.expanded + ); + } + }); + } + }); + return new SceneFlexItem({ key: VALUE_SUMMARY_PANEL_KEY, - minHeight: getValueSummaryHeight(collapsable), - height: getValueSummaryHeight(collapsable), - maxHeight: getValueSummaryHeight(collapsable), + minHeight: getValueSummaryHeight(collapsed), + height: getValueSummaryHeight(collapsed), + maxHeight: getValueSummaryHeight(collapsed), body: build, }); } @@ -49,7 +62,7 @@ export function setValueSummaryHeight(vizPanelFlexItem: SceneFlexItem, collapsab } function getValueSummaryHeight(collapsableState: CollapsablePanelType) { - return collapsableState === CollapsablePanelType.collapse ? 300 : 35; + return collapsableState === CollapsablePanelType.collapsed ? 35 : 300; } export const VALUE_SUMMARY_PANEL_KEY = 'value_summary_panel'; diff --git a/src/services/store.ts b/src/services/store.ts index 5b64c7a1..4badff07 100644 --- a/src/services/store.ts +++ b/src/services/store.ts @@ -240,7 +240,7 @@ export function setLogsVisualizationType(type: string) { const PANEL_OPTIONS_LOCALSTORAGE_KEY = `${pluginJson.id}.panel.option`; export interface PanelOptions { panelType: AvgFieldPanelType; - collapsable: CollapsablePanelType; + collapsed: CollapsablePanelType; } export function getPanelOption( option: K, From b87efa64c09eb6df09e54d73004b106f7589f991 Mon Sep 17 00:00:00 2001 From: Galen Date: Thu, 5 Dec 2024 11:48:34 -0600 Subject: [PATCH 20/34] chore: refactor collapsable states in panel menus --- src/Components/Panels/PanelMenu.tsx | 6 +- .../Breakdowns/FieldValuesBreakdownScene.tsx | 7 +- .../Breakdowns/LabelValuesBreakdownScene.tsx | 6 +- .../Breakdowns/Panels/ValueSummary.ts | 68 ----------- .../Breakdowns/Panels/ValueSummary.tsx | 106 ++++++++++++++++++ 5 files changed, 116 insertions(+), 77 deletions(-) delete mode 100644 src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts create mode 100644 src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.tsx diff --git a/src/Components/Panels/PanelMenu.tsx b/src/Components/Panels/PanelMenu.tsx index 2c58866a..40453af8 100644 --- a/src/Components/Panels/PanelMenu.tsx +++ b/src/Components/Panels/PanelMenu.tsx @@ -3,7 +3,7 @@ import { PanelBuilders, SceneComponentProps, SceneCSSGridItem, - SceneFlexItem, + SceneFlexLayout, sceneGraph, SceneObject, SceneObjectBase, @@ -153,8 +153,8 @@ function addCollapsableItem(items: PanelMenuItem[], menu: PanelMenu) { const newCollapsableState = viz.state.collapsed ? CollapsablePanelType.expanded : CollapsablePanelType.collapsed; // Update the viz - const vizPanelFlexItem = sceneGraph.getAncestor(menu, SceneFlexItem); - setValueSummaryHeight(vizPanelFlexItem, newCollapsableState); + const vizPanelFlexLayout = sceneGraph.getAncestor(menu, SceneFlexLayout); + setValueSummaryHeight(vizPanelFlexLayout, newCollapsableState); // Set state and update local storage viz.setState({ diff --git a/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx index be116e69..faba4643 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldValuesBreakdownScene.tsx @@ -33,7 +33,7 @@ import { DEFAULT_SORT_BY } from '../../../services/sorting'; import { getFieldGroupByVariable, getFieldsVariable } from '../../../services/variableGetters'; import { LokiQuery } from '../../../services/lokiQuery'; import { PanelMenu, getPanelWrapperStyles } from '../../Panels/PanelMenu'; -import { getValueSummaryPanel } from './Panels/ValueSummary'; +import { ValueSummaryPanelScene } from './Panels/ValueSummary'; export interface FieldValuesBreakdownSceneState extends SceneObjectState { body?: (LayoutSwitcher & SceneObject) | (SceneReactObject & SceneObject); @@ -203,7 +203,8 @@ export class FieldValuesBreakdownScene extends SceneObjectBase, }), - getValueSummaryPanel(optionValue), + new ValueSummaryPanelScene({ title: optionValue }), + new SceneReactObject({ reactNode: , }), @@ -241,7 +242,7 @@ export class FieldValuesBreakdownScene extends SceneObjectBase, }), - getValueSummaryPanel(optionValue), + new ValueSummaryPanelScene({ title: optionValue }), new SceneReactObject({ reactNode: , }), diff --git a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx index 45118d96..fbdd68f4 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx @@ -36,7 +36,7 @@ import { ClearFiltersLayoutScene } from './ClearFiltersLayoutScene'; import { EmptyLayoutScene } from './EmptyLayoutScene'; import { IndexScene } from '../../IndexScene/IndexScene'; import { clearVariables, getVariablesThatCanBeCleared } from '../../../services/variableHelpers'; -import { getValueSummaryPanel } from './Panels/ValueSummary'; +import { ValueSummaryPanelScene } from './Panels/ValueSummary'; type DisplayError = DataQueryError & { displayed: boolean }; type DisplayErrors = Record; @@ -236,7 +236,7 @@ export class LabelValuesBreakdownScene extends SceneObjectBase }), - getValueSummaryPanel(tagKey, { levelColor: true }), + new ValueSummaryPanelScene({ title: tagKey, levelColor: true }), new SceneReactObject({ reactNode: }), new ByFrameRepeater({ body: new SceneCSSGridLayout({ @@ -268,7 +268,7 @@ export class LabelValuesBreakdownScene extends SceneObjectBase }), - getValueSummaryPanel(tagKey, { levelColor: true }), + new ValueSummaryPanelScene({ title: tagKey, levelColor: true }), new SceneReactObject({ reactNode: }), new ByFrameRepeater({ body: new SceneCSSGridLayout({ diff --git a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts deleted file mode 100644 index 713f77d5..00000000 --- a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { PanelBuilders, SceneFlexItem, sceneGraph, VizPanel } from '@grafana/scenes'; -import { CollapsablePanelType, PanelMenu } from '../../../Panels/PanelMenu'; -import { DrawStyle, StackingMode } from '@grafana/ui'; -import { setLevelColorOverrides } from '../../../../services/panel'; -import { getPanelOption } from '../../../../services/store'; -import { Options } from '@grafana/schema/dist/esm/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen'; - -const SUMMARY_PANEL_SERIES_LIMIT = 100; - -export function getValueSummaryPanel(title: string, options?: { levelColor?: boolean }) { - const collapsed = - getPanelOption('collapsed', [CollapsablePanelType.collapsed, CollapsablePanelType.expanded]) ?? - CollapsablePanelType.collapsed; - - const body = PanelBuilders.timeseries() - .setTitle(title) - .setMenu(new PanelMenu({})) - .setCollapsible(true) - .setCollapsed(collapsed === CollapsablePanelType.collapsed) - .setCustomFieldConfig('stacking', { mode: StackingMode.Normal }) - .setCustomFieldConfig('fillOpacity', 100) - .setCustomFieldConfig('lineWidth', 0) - .setCustomFieldConfig('pointSize', 0) - .setSeriesLimit(SUMMARY_PANEL_SERIES_LIMIT) - .setCustomFieldConfig('drawStyle', DrawStyle.Bars); - - if (options?.levelColor) { - body.setOverrides(setLevelColorOverrides); - } - const build: VizPanel = body.build(); - - build.addActivationHandler(() => { - if (build.state.collapsible) { - // @todo handle unsub - build.subscribeToState((newState, prevState) => { - if (newState.collapsed !== prevState.collapsed) { - const vizPanelFlexItem = sceneGraph.getAncestor(build, SceneFlexItem); - setValueSummaryHeight( - vizPanelFlexItem, - newState.collapsed ? CollapsablePanelType.collapsed : CollapsablePanelType.expanded - ); - } - }); - } - }); - - return new SceneFlexItem({ - key: VALUE_SUMMARY_PANEL_KEY, - minHeight: getValueSummaryHeight(collapsed), - height: getValueSummaryHeight(collapsed), - maxHeight: getValueSummaryHeight(collapsed), - body: build, - }); -} - -export function setValueSummaryHeight(vizPanelFlexItem: SceneFlexItem, collapsableState: CollapsablePanelType) { - vizPanelFlexItem.setState({ - minHeight: getValueSummaryHeight(collapsableState), - height: getValueSummaryHeight(collapsableState), - maxHeight: getValueSummaryHeight(collapsableState), - }); -} - -function getValueSummaryHeight(collapsableState: CollapsablePanelType) { - return collapsableState === CollapsablePanelType.collapsed ? 35 : 300; -} - -export const VALUE_SUMMARY_PANEL_KEY = 'value_summary_panel'; diff --git a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.tsx b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.tsx new file mode 100644 index 00000000..9456ff04 --- /dev/null +++ b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.tsx @@ -0,0 +1,106 @@ +import { + PanelBuilders, + SceneComponentProps, + SceneFlexItem, + SceneFlexLayout, + sceneGraph, + SceneObjectBase, + SceneObjectState, + VizPanel, +} from '@grafana/scenes'; +import { CollapsablePanelType, PanelMenu } from '../../../Panels/PanelMenu'; +import { DrawStyle, StackingMode } from '@grafana/ui'; +import { setLevelColorOverrides } from '../../../../services/panel'; +import { getPanelOption } from '../../../../services/store'; +import React from 'react'; + +const SUMMARY_PANEL_SERIES_LIMIT = 100; + +interface ValueSummaryPanelSceneState extends SceneObjectState { + body?: SceneFlexLayout; + title: string; + levelColor?: boolean; +} +export class ValueSummaryPanelScene extends SceneObjectBase { + constructor(state: ValueSummaryPanelSceneState) { + super(state); + this.addActivationHandler(this.onActivate.bind(this)); + } + + public static Component = ({ model }: SceneComponentProps) => { + const { body } = model.useState(); + if (body) { + return ; + } + + return null; + }; + + onActivate() { + const collapsed = + getPanelOption('collapsed', [CollapsablePanelType.collapsed, CollapsablePanelType.expanded]) ?? + CollapsablePanelType.collapsed; + const viz = buildValueSummaryPanel(this.state.title, { levelColor: this.state.levelColor }); + const height = getValueSummaryHeight(collapsed); + + this.setState({ + body: new SceneFlexLayout({ + key: VALUE_SUMMARY_PANEL_KEY, + minHeight: height, + children: [ + new SceneFlexItem({ + body: viz, + }), + ], + }), + }); + + this._subs.add( + viz.subscribeToState((newState, prevState) => { + if (newState.collapsed !== prevState.collapsed) { + const vizPanelFlexLayout = sceneGraph.getAncestor(viz, SceneFlexLayout); + setValueSummaryHeight( + vizPanelFlexLayout, + newState.collapsed ? CollapsablePanelType.collapsed : CollapsablePanelType.expanded + ); + } + }) + ); + } +} + +export function setValueSummaryHeight(vizPanelFlexLayout: SceneFlexLayout, collapsableState: CollapsablePanelType) { + const height = getValueSummaryHeight(collapsableState); + vizPanelFlexLayout.setState({ + minHeight: height, + }); +} + +function getValueSummaryHeight(collapsableState: CollapsablePanelType) { + return collapsableState === CollapsablePanelType.collapsed ? 35 : 300; +} + +function buildValueSummaryPanel(title: string, options?: { levelColor?: boolean }): VizPanel { + const collapsed = + getPanelOption('collapsed', [CollapsablePanelType.collapsed, CollapsablePanelType.expanded]) ?? + CollapsablePanelType.collapsed; + + const body = PanelBuilders.timeseries() + .setTitle(title) + .setMenu(new PanelMenu({})) + .setCollapsible(true) + .setCollapsed(collapsed === CollapsablePanelType.collapsed) + .setCustomFieldConfig('stacking', { mode: StackingMode.Normal }) + .setCustomFieldConfig('fillOpacity', 100) + .setCustomFieldConfig('lineWidth', 0) + .setCustomFieldConfig('pointSize', 0) + .setSeriesLimit(SUMMARY_PANEL_SERIES_LIMIT) + .setCustomFieldConfig('drawStyle', DrawStyle.Bars); + + if (options?.levelColor) { + body.setOverrides(setLevelColorOverrides); + } + return body.build(); +} + +export const VALUE_SUMMARY_PANEL_KEY = 'value_summary_panel'; From 7f64aa344f6b4afcb1a017fdceeb2c5a7ae8a189 Mon Sep 17 00:00:00 2001 From: Galen Date: Thu, 5 Dec 2024 13:25:57 -0600 Subject: [PATCH 21/34] chore: add findObjectOfType scenes helper method, remove type assertions --- src/Components/Panels/PanelMenu.tsx | 10 ++++----- .../Breakdowns/AddToExplorationButton.tsx | 4 ++-- .../Breakdowns/SelectLabelActionScene.tsx | 3 ++- src/services/scenes.ts | 22 ++++++++++++++++++- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/Components/Panels/PanelMenu.tsx b/src/Components/Panels/PanelMenu.tsx index 40453af8..b4ed13ab 100644 --- a/src/Components/Panels/PanelMenu.tsx +++ b/src/Components/Panels/PanelMenu.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { css } from '@emotion/css'; import { onExploreLinkClick } from '../ServiceScene/GoToExploreButton'; import { IndexScene } from '../IndexScene/IndexScene'; -import { getQueryRunnerFromChildren } from '../../services/scenes'; +import { findObjectOfType, getQueryRunnerFromChildren } from '../../services/scenes'; import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from '../../services/analytics'; import { logger } from '../../services/logger'; import { AddToExplorationButton } from '../ServiceScene/Breakdowns/AddToExplorationButton'; @@ -190,7 +190,6 @@ function addHistogramItem(items: PanelMenuItem[], sceneRef: PanelMenu) { body: body.setMenu(menu).setTitle(viz.state.title).setHeaderActions(headerActions).setData($data).build(), }); - // @todo extend findObject and use templates to avoid type assertions const newPanelType = sceneRef.state.panelType !== AvgFieldPanelType.timeseries ? AvgFieldPanelType.timeseries @@ -198,10 +197,11 @@ function addHistogramItem(items: PanelMenuItem[], sceneRef: PanelMenu) { setPanelOption('panelType', newPanelType); menu.setState({ panelType: newPanelType }); - const fieldsAggregatedBreakdownScene = sceneGraph.findObject( + const fieldsAggregatedBreakdownScene = findObjectOfType( gridItem, - (o) => o instanceof FieldsAggregatedBreakdownScene - ) as FieldsAggregatedBreakdownScene | null; + (o) => o instanceof FieldsAggregatedBreakdownScene, + FieldsAggregatedBreakdownScene + ); if (fieldsAggregatedBreakdownScene) { fieldsAggregatedBreakdownScene.rebuildAvgFields(); } diff --git a/src/Components/ServiceScene/Breakdowns/AddToExplorationButton.tsx b/src/Components/ServiceScene/Breakdowns/AddToExplorationButton.tsx index daec921c..29360cb0 100644 --- a/src/Components/ServiceScene/Breakdowns/AddToExplorationButton.tsx +++ b/src/Components/ServiceScene/Breakdowns/AddToExplorationButton.tsx @@ -5,7 +5,7 @@ import { DataQuery, DataSourceRef } from '@grafana/schema'; import { IconButton } from '@grafana/ui'; import React from 'react'; import { ExtensionPoints } from 'services/extensions/links'; -import { getLokiDatasource } from 'services/scenes'; +import { findObjectOfType, getLokiDatasource } from 'services/scenes'; import LokiLogo from '../../../img/logo.svg'; @@ -59,7 +59,7 @@ export class AddToExplorationButton extends SceneObjectBase { const data = sceneGraph.getData(this); - const queryRunner = sceneGraph.findObject(data, (o) => o instanceof SceneQueryRunner) as SceneQueryRunner; + const queryRunner = findObjectOfType(data, (o) => o instanceof SceneQueryRunner, SceneQueryRunner); if (queryRunner) { const filter = this.state.frame ? getFilter(this.state.frame) : null; const queries = queryRunner.state.queries.map((q) => ({ diff --git a/src/Components/ServiceScene/Breakdowns/SelectLabelActionScene.tsx b/src/Components/ServiceScene/Breakdowns/SelectLabelActionScene.tsx index f98725c2..212f496e 100644 --- a/src/Components/ServiceScene/Breakdowns/SelectLabelActionScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/SelectLabelActionScene.tsx @@ -30,6 +30,7 @@ import { NumericFilterPopoverScene } from './NumericFilterPopoverScene'; import { getDetectedFieldType } from '../../../services/fields'; import { logger } from '../../../services/logger'; import { testIds } from '../../../services/testIds'; +import { findObjectOfType } from '../../../services/scenes'; interface SelectLabelActionSceneState extends SceneObjectState { labelName: string; @@ -311,7 +312,7 @@ export class SelectLabelActionScene extends SceneObjectBase | undefined = logsPanelData?.fields.find((field) => field.name === 'labels'); const data = sceneGraph.getData(this); - const queryRunner = sceneGraph.findObject(data, (o) => o instanceof SceneQueryRunner) as SceneQueryRunner; + const queryRunner = findObjectOfType(data, (o) => o instanceof SceneQueryRunner, SceneQueryRunner); if (queryRunner) { const queries = queryRunner.state.queries; const query = queries[0] as LokiQuery | undefined; diff --git a/src/services/scenes.ts b/src/services/scenes.ts index 637d0efc..cc37f5b5 100644 --- a/src/services/scenes.ts +++ b/src/services/scenes.ts @@ -1,9 +1,10 @@ import { AdHocVariableFilter, urlUtil } from '@grafana/data'; import { config, DataSourceWithBackend, getDataSourceSrv } from '@grafana/runtime'; -import { sceneGraph, SceneObject, SceneObjectUrlValues, SceneQueryRunner } from '@grafana/scenes'; +import { sceneGraph, SceneObject, SceneObjectState, SceneObjectUrlValues, SceneQueryRunner } from '@grafana/scenes'; import { LOG_STREAM_SELECTOR_EXPR, VAR_DATASOURCE_EXPR, VAR_LABELS_EXPR } from './variables'; import { EXPLORATIONS_ROUTE } from './routing'; import { IndexScene } from 'Components/IndexScene/IndexScene'; +import { logger } from './logger'; export function getExplorationFor(model: SceneObject): IndexScene { return sceneGraph.getAncestor(model, IndexScene); @@ -50,3 +51,22 @@ export interface AdHocFilterWithLabels extends AdHocVariableFilter { keyLabel?: string; valueLabels?: string[]; } + +interface SceneType extends Function { + new (...args: never[]): T; +} + +export function findObjectOfType( + scene: SceneObject, + check: (obj: SceneObject) => boolean, + returnType: SceneType +) { + const obj = sceneGraph.findObject(scene, check); + if (obj instanceof returnType) { + return obj; + } else if (obj !== null) { + logger.warn(`invalid return type: ${returnType.toString()}`); + } + + return null; +} From 01677e9e12446c5b651706a6c3a3c124d12c8829 Mon Sep 17 00:00:00 2001 From: Galen Date: Thu, 5 Dec 2024 13:32:39 -0600 Subject: [PATCH 22/34] chore: remove unused import --- src/services/scenes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/scenes.ts b/src/services/scenes.ts index cc37f5b5..31aba0dd 100644 --- a/src/services/scenes.ts +++ b/src/services/scenes.ts @@ -1,6 +1,6 @@ import { AdHocVariableFilter, urlUtil } from '@grafana/data'; import { config, DataSourceWithBackend, getDataSourceSrv } from '@grafana/runtime'; -import { sceneGraph, SceneObject, SceneObjectState, SceneObjectUrlValues, SceneQueryRunner } from '@grafana/scenes'; +import { sceneGraph, SceneObject, SceneObjectUrlValues, SceneQueryRunner } from '@grafana/scenes'; import { LOG_STREAM_SELECTOR_EXPR, VAR_DATASOURCE_EXPR, VAR_LABELS_EXPR } from './variables'; import { EXPLORATIONS_ROUTE } from './routing'; import { IndexScene } from 'Components/IndexScene/IndexScene'; From 29065c0a2d9a81e965352e8ca92feacd3e70af84 Mon Sep 17 00:00:00 2001 From: Galen Date: Thu, 5 Dec 2024 15:21:10 -0600 Subject: [PATCH 23/34] chore: remove css hack --- src/Components/Panels/PanelMenu.tsx | 34 ++++-------------- .../ServiceScene/Breakdowns/FieldSelector.tsx | 12 +++++-- .../Breakdowns/FieldValuesBreakdownScene.tsx | 16 +++++---- .../FieldsAggregatedBreakdownScene.tsx | 10 +++--- .../Breakdowns/FieldsBreakdownScene.tsx | 19 ++++++---- .../Breakdowns/LabelBreakdownScene.tsx | 20 +++++++---- .../Breakdowns/LabelValuesBreakdownScene.tsx | 10 +++--- .../LabelsAggregatedBreakdownScene.tsx | 10 +++--- .../Breakdowns/LayoutSwitcher.tsx | 36 ++++++++++++------- .../Breakdowns/Panels/ValueSummary.tsx | 22 +++++++++--- .../ServiceScene/Breakdowns/SortByScene.tsx | 32 ++++++++--------- .../ServiceScene/LogsVolumePanel.tsx | 12 +++---- src/services/fields.ts | 3 +- 13 files changed, 130 insertions(+), 106 deletions(-) diff --git a/src/Components/Panels/PanelMenu.tsx b/src/Components/Panels/PanelMenu.tsx index b4ed13ab..c894df3f 100644 --- a/src/Components/Panels/PanelMenu.tsx +++ b/src/Components/Panels/PanelMenu.tsx @@ -1,4 +1,4 @@ -import { DataFrame, GrafanaTheme2, PanelMenuItem } from '@grafana/data'; +import { DataFrame, PanelMenuItem } from '@grafana/data'; import { PanelBuilders, SceneComponentProps, @@ -13,7 +13,6 @@ import { VizPanelMenu, } from '@grafana/scenes'; import React from 'react'; -import { css } from '@emotion/css'; import { onExploreLinkClick } from '../ServiceScene/GoToExploreButton'; import { IndexScene } from '../IndexScene/IndexScene'; import { findObjectOfType, getQueryRunnerFromChildren } from '../../services/scenes'; @@ -105,9 +104,11 @@ export class PanelMenu extends SceneObjectBase implements VizPan }), }); - this.state.addToExplorations?.subscribeToState(() => { - subscribeToAddToExploration(this); - }); + this._subs.add( + this.state.addToExplorations?.subscribeToState(() => { + subscribeToAddToExploration(this); + }) + ); }); } @@ -305,26 +306,3 @@ function subscribeToAddToExploration(exploreLogsVizPanelMenu: PanelMenu) { } } } - -export const getPanelWrapperStyles = (theme: GrafanaTheme2) => { - return { - panelWrapper: css({ - width: '100%', - height: '100%', - label: 'panel-wrapper', - position: 'absolute', - display: 'flex', - - // @todo remove this wrapper and styles when core changes are introduced in ??? - // Need more specificity to override core style - 'button.show-on-hover': { - opacity: 1, - visibility: 'visible', - background: 'none', - '&:hover': { - background: theme.colors.secondary.shade, - }, - }, - }), - }; -}; diff --git a/src/Components/ServiceScene/Breakdowns/FieldSelector.tsx b/src/Components/ServiceScene/Breakdowns/FieldSelector.tsx index 20f43d13..f3c3aea0 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldSelector.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldSelector.tsx @@ -32,7 +32,7 @@ export function FieldSelector({ options, value, onChange, label }: Props) }; }); return ( - + ) { const { body } = model.useState(); if (body instanceof LayoutSwitcher) { - return <>{body && }; + return <>{body && }; } return <>; @@ -58,9 +58,8 @@ export class FieldValuesBreakdownScene extends SceneObjectBase) => { const { body } = model.useState(); - const styles = useStyles2(getPanelWrapperStyles); if (body) { - return {body && }; + return <>{body && }; } return ; @@ -191,7 +190,11 @@ export class FieldValuesBreakdownScene extends SceneObjectBase, }), new ValueSummaryPanelScene({ title: optionValue }), - new SceneReactObject({ reactNode: , }), diff --git a/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx index 81dbc0bf..fbe8e615 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene.tsx @@ -12,7 +12,7 @@ import { import { ALL_VARIABLE_VALUE, DetectedFieldType, ParserType } from '../../../services/variables'; import { buildDataQuery } from '../../../services/query'; import { getQueryRunner, setLevelColorOverrides } from '../../../services/panel'; -import { DrawStyle, LoadingPlaceholder, StackingMode, useStyles2 } from '@grafana/ui'; +import { DrawStyle, LoadingPlaceholder, StackingMode } from '@grafana/ui'; import { LayoutSwitcher } from './LayoutSwitcher'; import { FIELDS_BREAKDOWN_GRID_TEMPLATE_COLUMNS, FieldsBreakdownScene } from './FieldsBreakdownScene'; import { @@ -37,7 +37,7 @@ import { getFieldsVariable, getValueFromFieldsFilter, } from '../../../services/variableGetters'; -import { AvgFieldPanelType, getPanelWrapperStyles, PanelMenu } from '../../Panels/PanelMenu'; +import { AvgFieldPanelType, PanelMenu } from '../../Panels/PanelMenu'; import { logger } from '../../../services/logger'; import { getPanelOption } from '../../../services/store'; import { MAX_NUMBER_OF_TIME_SERIES } from './TimeSeriesLimit'; @@ -342,6 +342,7 @@ export class FieldsAggregatedBreakdownScene extends SceneObjectBase) { const { body } = model.useState(); - return <>{body && }; + return <>{body && }; } public static Component = ({ model }: SceneComponentProps) => { const { body } = model.useState(); - const styles = useStyles2(getPanelWrapperStyles); if (body) { - return {body && }; + return <>{body && }; } return ; diff --git a/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx index 6ab2cbbd..b2d637b0 100644 --- a/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/FieldsBreakdownScene.tsx @@ -275,14 +275,15 @@ export class FieldsBreakdownScene extends SceneObjectBase) => { - const { body, loading } = model.useState(); + const { body, loading, search } = model.useState(); const styles = useStyles2(getStyles); const variable = getFieldGroupByVariable(model); const { options, value } = variable.useState(); return ( -
+
{body instanceof FieldsAggregatedBreakdownScene && } {body instanceof FieldValuesBreakdownScene && } + {!loading && options.length > 1 && ( )} @@ -290,16 +291,15 @@ export class FieldsBreakdownScene extends SceneObjectBase) => { - const { loading, search, sort } = model.useState(); + const { loading, sort } = model.useState(); const styles = useStyles2(getStyles); const variable = getFieldGroupByVariable(model); const { value } = variable.useState(); return ( -
+
{!loading && value !== ALL_VARIABLE_VALUE && ( <> - )}
@@ -343,7 +343,7 @@ function getStyles(theme: GrafanaTheme2) { display: 'flex', paddingTop: theme.spacing(0), }), - controls: css({ + parentMenuWrapper: css({ flexGrow: 0, display: 'flex', alignItems: 'top', @@ -351,5 +351,12 @@ function getStyles(theme: GrafanaTheme2) { flexDirection: 'row-reverse', gap: theme.spacing(2), }), + valueMenuWrapper: css({ + flexGrow: 0, + display: 'flex', + alignItems: 'top', + gap: theme.spacing(2), + flexDirection: 'row', + }), }; } diff --git a/src/Components/ServiceScene/Breakdowns/LabelBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelBreakdownScene.tsx index ca70f6b3..b20adf63 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelBreakdownScene.tsx @@ -273,16 +273,16 @@ export class LabelBreakdownScene extends SceneObjectBase) => { - const { body, loading } = model.useState(); + const { body, loading, search } = model.useState(); const variable = getLabelGroupByVariable(model); const { options, value } = variable.useState(); const styles = useStyles2(getStyles); return ( -
+
{body instanceof LabelValuesBreakdownScene && } {body instanceof LabelsAggregatedBreakdownScene && } - + {!loading && options.length > 0 && ( )} @@ -291,17 +291,16 @@ export class LabelBreakdownScene extends SceneObjectBase) => { - const { loading, search, sort } = model.useState(); + const { loading, sort } = model.useState(); const variable = getLabelGroupByVariable(model); const { value } = variable.useState(); const styles = useStyles2(getStyles); return ( -
+
{!loading && value !== ALL_VARIABLE_VALUE && ( <> - )}
@@ -343,7 +342,7 @@ function getStyles(theme: GrafanaTheme2) { display: 'flex', paddingTop: theme.spacing(0), }), - controls: css({ + parentMenuWrapper: css({ flexGrow: 0, display: 'flex', alignItems: 'top', @@ -351,5 +350,12 @@ function getStyles(theme: GrafanaTheme2) { flexDirection: 'row-reverse', gap: theme.spacing(2), }), + valueMenuWrapper: css({ + flexGrow: 0, + display: 'flex', + alignItems: 'top', + gap: theme.spacing(2), + flexDirection: 'row', + }), }; } diff --git a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx index fbdd68f4..5da3c95f 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelValuesBreakdownScene.tsx @@ -14,7 +14,7 @@ import { } from '@grafana/scenes'; import { LayoutSwitcher } from './LayoutSwitcher'; import { getLabelValue } from './SortByScene'; -import { DrawStyle, LoadingPlaceholder, StackingMode, useStyles2 } from '@grafana/ui'; +import { DrawStyle, LoadingPlaceholder, StackingMode } from '@grafana/ui'; import { getQueryRunner, setLevelColorOverrides } from '../../../services/panel'; import { getSortByPreference } from '../../../services/store'; import { AppEvents, DataQueryError, LoadingState } from '@grafana/data'; @@ -31,7 +31,7 @@ import { DEFAULT_SORT_BY } from '../../../services/sorting'; import { buildLabelsQuery, LABEL_BREAKDOWN_GRID_TEMPLATE_COLUMNS } from '../../../services/labels'; import { getAppEvents } from '@grafana/runtime'; import { getLabelGroupByVariable } from '../../../services/variableGetters'; -import { getPanelWrapperStyles, PanelMenu } from '../../Panels/PanelMenu'; +import { PanelMenu } from '../../Panels/PanelMenu'; import { ClearFiltersLayoutScene } from './ClearFiltersLayoutScene'; import { EmptyLayoutScene } from './EmptyLayoutScene'; import { IndexScene } from '../../IndexScene/IndexScene'; @@ -205,6 +205,7 @@ export class LabelValuesBreakdownScene extends SceneObjectBase) { const { body } = model.useState(); - return <>{body && body instanceof LayoutSwitcher && }; + return <>{body && body instanceof LayoutSwitcher && }; } public static Component = ({ model }: SceneComponentProps) => { const { body } = model.useState(); - const styles = useStyles2(getPanelWrapperStyles); if (body) { - return {body && }; + return <>{body && }; } return ; diff --git a/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx b/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx index 1328c6e2..f7374408 100644 --- a/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx +++ b/src/Components/ServiceScene/Breakdowns/LabelsAggregatedBreakdownScene.tsx @@ -12,7 +12,7 @@ import { VizPanel, } from '@grafana/scenes'; import { LayoutSwitcher } from './LayoutSwitcher'; -import { DrawStyle, LoadingPlaceholder, StackingMode, useStyles2 } from '@grafana/ui'; +import { DrawStyle, LoadingPlaceholder, StackingMode } from '@grafana/ui'; import { getQueryRunner, setLevelColorOverrides } from '../../../services/panel'; import { ALL_VARIABLE_VALUE, LEVEL_VARIABLE_VALUE } from '../../../services/variables'; import React from 'react'; @@ -23,7 +23,7 @@ import { buildLabelsQuery, LABEL_BREAKDOWN_GRID_TEMPLATE_COLUMNS } from '../../. import { getFieldsVariable, getLabelGroupByVariable } from '../../../services/variableGetters'; import { ServiceScene } from '../ServiceScene'; import { DataFrame, LoadingState } from '@grafana/data'; -import { getPanelWrapperStyles, PanelMenu } from '../../Panels/PanelMenu'; +import { PanelMenu } from '../../Panels/PanelMenu'; import { MAX_NUMBER_OF_TIME_SERIES } from './TimeSeriesLimit'; export interface LabelsAggregatedBreakdownSceneState extends SceneObjectState { @@ -220,6 +220,7 @@ export class LabelsAggregatedBreakdownScene extends SceneObjectBase) { const { body } = model.useState(); - return <>{body && }; + return <>{body && }; } public static Component = ({ model }: SceneComponentProps) => { const { body } = model.useState(); - const styles = useStyles2(getPanelWrapperStyles); if (body) { - return {body && }; + return <>{body && }; } return ; diff --git a/src/Components/ServiceScene/Breakdowns/LayoutSwitcher.tsx b/src/Components/ServiceScene/Breakdowns/LayoutSwitcher.tsx index efe6b991..0f623131 100644 --- a/src/Components/ServiceScene/Breakdowns/LayoutSwitcher.tsx +++ b/src/Components/ServiceScene/Breakdowns/LayoutSwitcher.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { SelectableValue } from '@grafana/data'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { SceneComponentProps, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; -import { Field, RadioButtonGroup } from '@grafana/ui'; -import { USER_EVENTS_ACTIONS, USER_EVENTS_PAGES, reportAppInteraction } from 'services/analytics'; +import { Field, RadioButtonGroup, useStyles2 } from '@grafana/ui'; +import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from 'services/analytics'; import { getDrilldownSlug } from '../../../services/routing'; +import { css } from '@emotion/css'; export interface LayoutSwitcherState extends SceneObjectState { active: LayoutType; @@ -15,15 +16,7 @@ export interface LayoutSwitcherState extends SceneObjectState { export type LayoutType = 'single' | 'grid' | 'rows'; export class LayoutSwitcher extends SceneObjectBase { - public Selector({ model }: { model: LayoutSwitcher }) { - const { active, options } = model.useState(); - - return ( - - - - ); - } + public static Selector = LayoutSwitcherComponent; public onLayoutChange = (active: LayoutType) => { reportAppInteraction(USER_EVENTS_PAGES.service_details, USER_EVENTS_ACTIONS.service_details.layout_type_changed, { @@ -46,3 +39,22 @@ export class LayoutSwitcher extends SceneObjectBase { return ; }; } + +function LayoutSwitcherComponent({ model }: { model: LayoutSwitcher }) { + const { active, options } = model.useState(); + const styles = useStyles2(getStyles); + + return ( + + + + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + field: css({ + marginBottom: 0, + }), + }; +}; diff --git a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.tsx b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.tsx index 9456ff04..fcc28122 100644 --- a/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.tsx +++ b/src/Components/ServiceScene/Breakdowns/Panels/ValueSummary.tsx @@ -11,7 +11,7 @@ import { import { CollapsablePanelType, PanelMenu } from '../../../Panels/PanelMenu'; import { DrawStyle, StackingMode } from '@grafana/ui'; import { setLevelColorOverrides } from '../../../../services/panel'; -import { getPanelOption } from '../../../../services/store'; +import { getPanelOption, setPanelOption } from '../../../../services/store'; import React from 'react'; const SUMMARY_PANEL_SERIES_LIMIT = 100; @@ -30,7 +30,11 @@ export class ValueSummaryPanelScene extends SceneObjectBase) => { const { body } = model.useState(); if (body) { - return ; + return ( +
+ +
+ ); } return null; @@ -39,7 +43,7 @@ export class ValueSummaryPanelScene extends SceneObjectBase { ); return ( <> + + { ]} > - -