From 2e6ccf98ba655548d7cf8e70e703fdc1455e25d3 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Thu, 16 Oct 2025 11:44:11 +0300 Subject: [PATCH 1/7] wip non applicable pills --- .../src/components/VizPanel/VizPanel.tsx | 4 +++ .../components/VizPanel/VizPanelRenderer.tsx | 18 +++++++++++++ packages/scenes/src/index.ts | 1 + .../variables/adhoc/AdHocFiltersVariable.tsx | 26 ++++++++++++------- .../src/variables/groupby/GroupByVariable.tsx | 19 ++++++++++---- 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/packages/scenes/src/components/VizPanel/VizPanel.tsx b/packages/scenes/src/components/VizPanel/VizPanel.tsx index 9fda04e30..6efd271e6 100644 --- a/packages/scenes/src/components/VizPanel/VizPanel.tsx +++ b/packages/scenes/src/components/VizPanel/VizPanel.tsx @@ -62,6 +62,10 @@ export interface VizPanelState extends SceneOb * Offset hoverHeader position on the y axis */ hoverHeaderOffset?: number; + /** + * Allows adding elements to the subheader of the panel. + */ + subHeaderContent?: React.ReactNode | SceneObject | SceneObject[]; /** * Only shows vizPanelMenu on hover if false, otherwise the menu is always visible in the header */ diff --git a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx index 70a5dc453..14beab3d6 100644 --- a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx +++ b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx @@ -29,6 +29,7 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { hoverHeaderOffset, menu, headerActions, + subHeaderContent, titleItems, seriesLimit, seriesLimitShowAll, @@ -103,6 +104,22 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { dataObject.setContainerWidth(Math.round(width)); } + let subHeaderElement: React.ReactNode[] = []; + + if (subHeaderContent) { + if (Array.isArray(subHeaderContent)) { + subHeaderElement = subHeaderElement.concat( + subHeaderContent.map((subHeaderItem) => { + return ; + }) + ); + } else if (isSceneObject(subHeaderContent)) { + subHeaderElement.push(); + } else { + subHeaderElement.push(subHeaderContent); + } + } + let titleItemsElement: React.ReactNode[] = []; if (titleItems) { @@ -212,6 +229,7 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { onFocus={setPanelAttention} onMouseEnter={setPanelAttention} onMouseMove={debouncedMouseMove} + subHeaderContent={subHeaderElement.length ? subHeaderElement : undefined} onDragStart={(e: React.PointerEvent) => { dragHooks.onDragStart?.(e, model); }} diff --git a/packages/scenes/src/index.ts b/packages/scenes/src/index.ts index 0bba41208..44b8173a2 100644 --- a/packages/scenes/src/index.ts +++ b/packages/scenes/src/index.ts @@ -83,6 +83,7 @@ export type { AdHocFilterWithLabels } from './variables/adhoc/AdHocFiltersVariab export { GroupByVariable } from './variables/groupby/GroupByVariable'; export { type MacroVariableConstructor } from './variables/macros/types'; export { escapeUrlPipeDelimiters } from './variables/utils'; +export { getNonApplicablePillStyles } from './variables/utils'; export { type UrlSyncManagerLike, UrlSyncManager, NewSceneObjectAddedEvent } from './services/UrlSyncManager'; export { useUrlSync } from './services/useUrlSync'; diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx index 8c662f556..85e1d8ba0 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx @@ -11,7 +11,7 @@ import { } from '@grafana/data'; import { SceneObjectBase } from '../../core/SceneObjectBase'; import { SceneVariable, SceneVariableState, SceneVariableValueChangedEvent, VariableValue } from '../types'; -import { ControlsLayout, SceneComponentProps } from '../../core/types'; +import { ControlsLayout, SceneComponentProps, SceneDataQuery } from '../../core/types'; import { DataSourceRef } from '@grafana/schema'; import { dataFromResponse, getQueriesForVariables, renderPrometheusLabelFilters, responseHasError } from '../utils'; import { patchGetAdhocFilters } from './patchGetAdhocFilters'; @@ -572,8 +572,10 @@ export class AdHocFiltersVariable } } - public async _verifyApplicability() { - const filters = [...this.state.filters, ...(this.state.originFilters ?? [])]; + public async getFiltersApplicabilityForQueries(filters: AdHocFilterWithLabels[], queries: SceneDataQuery[]) { + if (!filters.length || !queries.length) { + return; + } const ds = await this._dataSourceSrv.get(this.state.datasource, this._scopedVars); // @ts-expect-error (temporary till we update grafana/data) @@ -581,21 +583,27 @@ export class AdHocFiltersVariable return; } - if (!filters) { - return; - } - const timeRange = sceneGraph.getTimeRange(this).state.value; - const queries = this.state.useQueriesAsFilterForOptions ? getQueriesForVariables(this) : undefined; // @ts-expect-error (temporary till we update grafana/data) - const response: DrilldownsApplicability[] = await ds.getDrilldownsApplicability({ + return await ds.getDrilldownsApplicability({ filters, queries, timeRange, scopes: sceneGraph.getScopes(this), ...getEnrichedFiltersRequest(this), }); + } + + public async _verifyApplicability() { + const filters = [...this.state.filters, ...(this.state.originFilters ?? [])]; + const queries = this.state.useQueriesAsFilterForOptions ? getQueriesForVariables(this) : undefined; + + const response = await this.getFiltersApplicabilityForQueries(filters, queries ?? []); + + if (!response) { + return; + } const responseMap = new Map(); response.forEach((filter) => { diff --git a/packages/scenes/src/variables/groupby/GroupByVariable.tsx b/packages/scenes/src/variables/groupby/GroupByVariable.tsx index 16ae0b53f..c7c30c955 100644 --- a/packages/scenes/src/variables/groupby/GroupByVariable.tsx +++ b/packages/scenes/src/variables/groupby/GroupByVariable.tsx @@ -11,7 +11,7 @@ import { } from '@grafana/data'; import { allActiveGroupByVariables } from './findActiveGroupByVariablesByUid'; import { DataSourceRef, VariableType } from '@grafana/schema'; -import { SceneComponentProps, ControlsLayout, SceneObjectUrlSyncHandler } from '../../core/types'; +import { SceneComponentProps, ControlsLayout, SceneObjectUrlSyncHandler, SceneDataQuery } from '../../core/types'; import { sceneGraph } from '../../core/sceneGraph'; import { SceneVariableValueChangedEvent, @@ -219,7 +219,7 @@ export class GroupByVariable extends MultiValueVariable { return applicableValues; } - public async _verifyApplicability() { + public async getGroupByApplicabilityForQueries(value: VariableValue, queries: SceneDataQuery[]) { const ds = await getDataSource(this.state.datasource, { __sceneObject: wrapInSafeSerializableSceneObject(this), }); @@ -229,18 +229,27 @@ export class GroupByVariable extends MultiValueVariable { return; } - const queries = getQueriesForVariables(this); const timeRange = sceneGraph.getTimeRange(this).state.value; - const value = this.state.value; // @ts-expect-error (temporary till we update grafana/data) - const response = await ds.getDrilldownsApplicability({ + return await ds.getDrilldownsApplicability({ groupByKeys: Array.isArray(value) ? value.map((v) => String(v)) : value ? [String(value)] : [], queries, timeRange, scopes: sceneGraph.getScopes(this), ...getEnrichedFiltersRequest(this), }); + } + + public async _verifyApplicability() { + const queries = getQueriesForVariables(this); + const value = this.state.value; + + const response = await this.getGroupByApplicabilityForQueries(value, queries); + + if (!response) { + return; + } if (!isEqual(response, this.state.keysApplicability)) { this.setState({ keysApplicability: response ?? undefined }); From 7469b356b1ea8101547b1c76dc26de236cf6ecb1 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Thu, 16 Oct 2025 13:12:12 +0300 Subject: [PATCH 2/7] fix --- packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx index 85e1d8ba0..6f2f67675 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx @@ -573,10 +573,6 @@ export class AdHocFiltersVariable } public async getFiltersApplicabilityForQueries(filters: AdHocFilterWithLabels[], queries: SceneDataQuery[]) { - if (!filters.length || !queries.length) { - return; - } - const ds = await this._dataSourceSrv.get(this.state.datasource, this._scopedVars); // @ts-expect-error (temporary till we update grafana/data) if (!ds || !ds.getDrilldownsApplicability) { From 0d5d207063ad63b263747d818d8d924b3b6ddb3b Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Thu, 16 Oct 2025 13:32:55 +0300 Subject: [PATCH 3/7] typecheck --- packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx | 2 +- packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx index 14beab3d6..32bd9f3f1 100644 --- a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx +++ b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx @@ -208,7 +208,6 @@ export function VizPanelRenderer({ model }: SceneComponentProps) {
} className={absoluteWrapper} data-viz-panel-key={model.state.key}> {width > 0 && height > 0 && ( - // @ts-expect-error showMenuAlways remove when updating to @grafana/ui@12 fixed in https://github.com/grafana/grafana/pull/103553 ) { onFocus={setPanelAttention} onMouseEnter={setPanelAttention} onMouseMove={debouncedMouseMove} + // @ts-expect-error remove this on next grafana/ui update subHeaderContent={subHeaderElement.length ? subHeaderElement : undefined} onDragStart={(e: React.PointerEvent) => { dragHooks.onDragStart?.(e, model); diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx index 6f2f67675..b143b77bf 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx @@ -602,7 +602,7 @@ export class AdHocFiltersVariable } const responseMap = new Map(); - response.forEach((filter) => { + response.forEach((filter: DrilldownsApplicability) => { responseMap.set(`${filter.key}${filter.origin ? `-${filter.origin}` : ''}`, filter); }); From a5ed58fe78ab72ca2f0ab9f03aa8860c29598013 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Thu, 16 Oct 2025 15:17:11 +0300 Subject: [PATCH 4/7] add return types --- packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx | 5 ++++- packages/scenes/src/variables/groupby/GroupByVariable.tsx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx index b143b77bf..04cf97074 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx @@ -572,7 +572,10 @@ export class AdHocFiltersVariable } } - public async getFiltersApplicabilityForQueries(filters: AdHocFilterWithLabels[], queries: SceneDataQuery[]) { + public async getFiltersApplicabilityForQueries( + filters: AdHocFilterWithLabels[], + queries: SceneDataQuery[] + ): Promise { const ds = await this._dataSourceSrv.get(this.state.datasource, this._scopedVars); // @ts-expect-error (temporary till we update grafana/data) if (!ds || !ds.getDrilldownsApplicability) { diff --git a/packages/scenes/src/variables/groupby/GroupByVariable.tsx b/packages/scenes/src/variables/groupby/GroupByVariable.tsx index c7c30c955..a2f2abfc5 100644 --- a/packages/scenes/src/variables/groupby/GroupByVariable.tsx +++ b/packages/scenes/src/variables/groupby/GroupByVariable.tsx @@ -219,7 +219,10 @@ export class GroupByVariable extends MultiValueVariable { return applicableValues; } - public async getGroupByApplicabilityForQueries(value: VariableValue, queries: SceneDataQuery[]) { + public async getGroupByApplicabilityForQueries( + value: VariableValue, + queries: SceneDataQuery[] + ): Promise { const ds = await getDataSource(this.state.datasource, { __sceneObject: wrapInSafeSerializableSceneObject(this), }); From 5bbd878d429306b101210d4a448ff73a5935aee9 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Fri, 31 Oct 2025 14:35:18 +0200 Subject: [PATCH 5/7] refactor --- .../scenes/src/components/VizPanel/VizPanel.tsx | 2 +- .../src/components/VizPanel/VizPanelRenderer.tsx | 14 +++++++------- packages/scenes/src/index.ts | 1 - 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/scenes/src/components/VizPanel/VizPanel.tsx b/packages/scenes/src/components/VizPanel/VizPanel.tsx index f5ee30b4a..601aae04c 100644 --- a/packages/scenes/src/components/VizPanel/VizPanel.tsx +++ b/packages/scenes/src/components/VizPanel/VizPanel.tsx @@ -66,7 +66,7 @@ export interface VizPanelState extends SceneOb /** * Allows adding elements to the subheader of the panel. */ - subHeaderContent?: React.ReactNode | SceneObject | SceneObject[]; + subHeader?: React.ReactNode | SceneObject | SceneObject[]; /** * Only shows vizPanelMenu on hover if false, otherwise the menu is always visible in the header */ diff --git a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx index 3987cc3a4..a79813c12 100644 --- a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx +++ b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx @@ -29,7 +29,7 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { hoverHeaderOffset, menu, headerActions, - subHeaderContent, + subHeader, titleItems, seriesLimit, seriesLimitShowAll, @@ -132,17 +132,17 @@ export function VizPanelRenderer({ model }: SceneComponentProps) { let subHeaderElement: React.ReactNode[] = []; - if (subHeaderContent) { - if (Array.isArray(subHeaderContent)) { + if (subHeader) { + if (Array.isArray(subHeader)) { subHeaderElement = subHeaderElement.concat( - subHeaderContent.map((subHeaderItem) => { + subHeader.map((subHeaderItem) => { return ; }) ); - } else if (isSceneObject(subHeaderContent)) { - subHeaderElement.push(); + } else if (isSceneObject(subHeader)) { + subHeaderElement.push(); } else { - subHeaderElement.push(subHeaderContent); + subHeaderElement.push(subHeader); } } diff --git a/packages/scenes/src/index.ts b/packages/scenes/src/index.ts index be686c6c6..267a98924 100644 --- a/packages/scenes/src/index.ts +++ b/packages/scenes/src/index.ts @@ -89,7 +89,6 @@ export { AdHocFiltersComboboxRenderer } from './variables/adhoc/AdHocFiltersComb export { GroupByVariable } from './variables/groupby/GroupByVariable'; export { type MacroVariableConstructor } from './variables/macros/types'; export { escapeUrlPipeDelimiters } from './variables/utils'; -export { getNonApplicablePillStyles } from './variables/utils'; export { type UrlSyncManagerLike, UrlSyncManager, NewSceneObjectAddedEvent } from './services/UrlSyncManager'; export { useUrlSync } from './services/useUrlSync'; From f4009d2bcffadbcf48cfb3978c1e9257c1471d25 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Thu, 6 Nov 2025 12:01:17 +0200 Subject: [PATCH 6/7] Expose focus so we can add keybindings to adhoc and groupby inputs --- .../AdHocFiltersComboboxRenderer.tsx | 12 ++++++-- .../variables/adhoc/AdHocFiltersVariable.tsx | 24 +++++++++++++++ .../controller/AdHocFiltersController.ts | 12 ++++++++ .../AdHocFiltersVariableController.ts | 9 ++++++ .../src/variables/groupby/GroupByVariable.tsx | 30 ++++++++++++++++++- 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx index 8368126b2..97ad54078 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersCombobox/AdHocFiltersComboboxRenderer.tsx @@ -1,7 +1,7 @@ import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Icon, useStyles2 } from '@grafana/ui'; -import React, { memo, useRef } from 'react'; +import React, { memo, useEffect, useRef } from 'react'; import { AdHocFiltersController } from '../controller/AdHocFiltersController'; import { AdHocFilterPill } from './AdHocFilterPill'; import { AdHocFiltersAlwaysWipCombobox } from './AdHocFiltersAlwaysWipCombobox'; @@ -11,13 +11,21 @@ interface Props { } export const AdHocFiltersComboboxRenderer = memo(function AdHocFiltersComboboxRenderer({ controller }: Props) { - const { originFilters, filters, readOnly } = controller.useState(); + const { originFilters, filters, readOnly, _shouldFocus } = controller.useState(); const styles = useStyles2(getStyles); // ref that focuses on the always wip filter input // defined in the combobox component via useImperativeHandle const focusOnWipInputRef = useRef<() => void>(); + // Handle focus when _shouldFocus flag is set + useEffect(() => { + if (_shouldFocus) { + focusOnWipInputRef.current?.(); + controller.resetFocusFlag?.(); + } + }, [_shouldFocus, controller]); + return (
{ public getDefaultMultiState(options: VariableValueOption[]): { value: VariableValueSingle[]; text: string[] } { return { value: [], text: [] }; } + + /** + * Focus the group by input to start selecting dimensions. + */ + public focusInput() { + if (this.state.readOnly) { + return; + } + + this.setState({ _shouldFocus: true }); + } } export function GroupByVariableRenderer({ model }: SceneComponentProps) { @@ -357,6 +373,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps>>(() => { @@ -380,6 +397,15 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps(null); + + useEffect(() => { + if (_shouldFocus) { + selectRef.current?.focus?.(); + model.setState({ _shouldFocus: false }); + } + }, [_shouldFocus, model]); + // Detect value changes outside useEffect(() => { setUncommittedValue(values); @@ -409,6 +435,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps + selectRef={selectRef} aria-label={t( 'grafana-scenes.variables.group-by-variable-renderer.aria-label-group-by-selector', 'Group by selector' @@ -487,6 +514,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps ) : (