Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/scenes/src/components/VizPanel/VizPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export interface VizPanelState<TOptions = {}, TFieldConfig = {}> extends SceneOb
* Offset hoverHeader position on the y axis
*/
hoverHeaderOffset?: number;
/**
* Allows adding elements to the subheader of the panel.
*/
subHeader?: React.ReactNode | SceneObject | SceneObject[];
/**
* Only shows vizPanelMenu on hover if false, otherwise the menu is always visible in the header
*/
Expand Down
20 changes: 19 additions & 1 deletion packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function VizPanelRenderer({ model }: SceneComponentProps<VizPanel>) {
hoverHeaderOffset,
menu,
headerActions,
subHeader,
titleItems,
seriesLimit,
seriesLimitShowAll,
Expand Down Expand Up @@ -129,6 +130,22 @@ export function VizPanelRenderer({ model }: SceneComponentProps<VizPanel>) {
dataObject.setContainerWidth(Math.round(width));
}

let subHeaderElement: React.ReactNode[] = [];

if (subHeader) {
if (Array.isArray(subHeader)) {
subHeaderElement = subHeaderElement.concat(
subHeader.map((subHeaderItem) => {
return <subHeaderItem.Component model={subHeaderItem} key={`${subHeaderItem.state.key}`} />;
})
);
} else if (isSceneObject(subHeader)) {
subHeaderElement.push(<subHeader.Component model={subHeader} />);
} else {
subHeaderElement.push(subHeader);
}
}

let titleItemsElement: React.ReactNode[] = [];

if (titleItems) {
Expand Down Expand Up @@ -217,7 +234,6 @@ export function VizPanelRenderer({ model }: SceneComponentProps<VizPanel>) {
<div className={relativeWrapper}>
<div ref={ref as RefCallback<HTMLDivElement>} 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
<PanelChrome
title={titleInterpolated}
description={description?.trim() ? model.getDescription : undefined}
Expand All @@ -238,6 +254,8 @@ export function VizPanelRenderer({ model }: SceneComponentProps<VizPanel>) {
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);
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<div
className={cx(styles.comboboxWrapper, { [styles.comboboxFocusOutline]: !readOnly })}
Expand Down
57 changes: 46 additions & 11 deletions packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -143,6 +143,10 @@ export interface AdHocFiltersVariableState extends SceneVariableState {
* Allows custom formatting of a value before saving to filter state
*/
onAddCustomValue?: OnAddCustomValueFn;
/**
* @internal allows to focus the adhoc input through focusInput()
*/
_shouldFocus?: boolean;
}

export type AdHocVariableExpressionBuilderFn = (filters: AdHocFilterWithLabels[]) => string;
Expand Down Expand Up @@ -573,33 +577,40 @@ export class AdHocFiltersVariable
}
}

public async _verifyApplicability() {
const filters = [...this.state.filters, ...(this.state.originFilters ?? [])];

public async getFiltersApplicabilityForQueries(
filters: AdHocFilterWithLabels[],
queries: SceneDataQuery[]
): Promise<DrilldownsApplicability[] | undefined> {
const ds = await this._dataSourceSrv.get(this.state.datasource, this._scopedVars);
// @ts-expect-error (temporary till we update grafana/data)
if (!ds || !ds.getDrilldownsApplicability) {
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<string, DrilldownsApplicability>();
response.forEach((filter) => {
response.forEach((filter: DrilldownsApplicability) => {
responseMap.set(`${filter.key}${filter.origin ? `-${filter.origin}` : ''}`, filter);
});

Expand Down Expand Up @@ -754,6 +765,30 @@ export class AdHocFiltersVariable
});
}

/**
* Focus the filter input to start adding a new filter.
* Works with both standard and combobox layouts.
*/
public focusInput() {
if (this.state.readOnly) {
return;
}

if (this.state.layout === 'combobox') {
this.setState({ _shouldFocus: true });
}
}

/**
* Reset the focus flag after focusing has completed
* @internal
*/
public _resetFocusFlag() {
if (this.state._shouldFocus) {
this.setState({ _shouldFocus: false });
}
}

public _getOperators() {
const { supportsMultiValueOperators, allowCustomValue = true } = this.state;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface AdHocFiltersControllerState {
onAddCustomValue?: OnAddCustomValueFn;
wip?: AdHocFilterWithLabels;
inputPlaceholder?: string;
_shouldFocus?: boolean;
}

/**
Expand Down Expand Up @@ -84,6 +85,17 @@ export interface AdHocFiltersController {
*/
restoreOriginalFilter(filter: AdHocFilterWithLabels): void;

/**
* Optional: Focus the filter input (for combobox layout).
* This allows external code to programmatically focus the filter input.
*/
focusInput?(): void;

/**
* Reset the focus flag.
*/
resetFocusFlag?(): void;

/**
* Optional: Start profiling an interaction (for performance tracking).
* @param name - The interaction name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class AdHocFiltersVariableController implements AdHocFiltersController {
supportsMultiValueOperators: state.supportsMultiValueOperators,
onAddCustomValue: state.onAddCustomValue,
wip: state._wip,
_shouldFocus: state._shouldFocus,
};
}

Expand Down Expand Up @@ -64,6 +65,14 @@ export class AdHocFiltersVariableController implements AdHocFiltersController {
this.model.restoreOriginalFilter(filter);
}

public focusInput(): void {
this.model.focusInput();
}

public resetFocusFlag(): void {
this.model._resetFocusFlag();
}

public startProfile(name: string): void {
const queryController = getQueryController(this.model);
queryController?.startProfile(name);
Expand Down
53 changes: 47 additions & 6 deletions packages/scenes/src/variables/groupby/GroupByVariable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { t } from '@grafana/i18n';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
AdHocVariableFilter,
DataSourceApi,
Expand All @@ -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,
Expand Down Expand Up @@ -77,6 +77,11 @@ export interface GroupByVariableState extends MultiValueVariableState {
* Holds the applicability for each of the selected keys
*/
keysApplicability?: DrilldownsApplicability[];
/**
* @internal
* Flag to trigger focus on the input
*/
_shouldFocus?: boolean;
}

export type getTagKeysProvider = (
Expand Down Expand Up @@ -219,7 +224,10 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
return applicableValues;
}

public async _verifyApplicability() {
public async getGroupByApplicabilityForQueries(
value: VariableValue,
queries: SceneDataQuery[]
): Promise<DrilldownsApplicability[] | undefined> {
const ds = await getDataSource(this.state.datasource, {
__sceneObject: wrapInSafeSerializableSceneObject(this),
});
Expand All @@ -229,18 +237,27 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
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 });
Expand Down Expand Up @@ -330,6 +347,17 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
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<GroupByVariable>) {
Expand All @@ -345,6 +373,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa
allowCustomValue = true,
defaultValue,
keysApplicability,
_shouldFocus,
} = model.useState();

const values = useMemo<Array<SelectableValue<VariableValueSingle>>>(() => {
Expand All @@ -368,6 +397,15 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa

const hasDefaultValue = defaultValue !== undefined;

const selectRef = useRef<any>(null);

useEffect(() => {
if (_shouldFocus) {
selectRef.current?.focus?.();
model.setState({ _shouldFocus: false });
}
}, [_shouldFocus, model]);

// Detect value changes outside
useEffect(() => {
setUncommittedValue(values);
Expand Down Expand Up @@ -397,6 +435,8 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa

return isMulti ? (
<MultiSelect<VariableValueSingle>
// @ts-expect-error
selectRef={selectRef}
aria-label={t(
'grafana-scenes.variables.group-by-variable-renderer.aria-label-group-by-selector',
'Group by selector'
Expand Down Expand Up @@ -475,6 +515,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa
/>
) : (
<Select
selectRef={selectRef}
aria-label={t(
'grafana-scenes.variables.group-by-variable-renderer.aria-label-group-by-selector',
'Group by selector'
Expand Down