Dashboards: Add non-applicable filter pills per panel (#112507)

* wip non-applicable pills

* refactor

* fix

* tests

* locales

* canary version

* mods

* fix query fetching

* fix

* fix

* Add VizPanelSubHeader component

* lint

* extra check

* refactor

* wip

* refactor wipp

* refactor

* refactor

* use canary scenes

* prettier

* canary scenes

* interpolate ds uid for ds var case

* refactor

* refactor

* memoize values + tests + rename component

* refactor

* refactor

* refactor

* refactor

* canary scenes + fixes

* Fix infinite loop casused by unstable useEffect dependency

* lint

* helper functions

* bump scenes

* reduce applicability calls to only when filters or groupBy keys change, not any var state change

* refactor

* fix

* fix

* fixes

---------

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Victor Marin
2025-11-21 18:03:27 +02:00
committed by GitHub
co-authored by Dominik Prokop
parent 5e949fc955
commit 336aa06966
19 changed files with 1486 additions and 387 deletions
+2 -2
View File
@@ -296,8 +296,8 @@
"@grafana/plugin-ui": "^0.11.1",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^6.46.0",
"@grafana/scenes-react": "^6.46.0",
"@grafana/scenes": "6.47.1",
"@grafana/scenes-react": "6.47.1",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -646,7 +646,7 @@ export interface MetricFindValue {
}
export interface DataSourceGetDrilldownsApplicabilityOptions<TQuery extends DataQuery = DataQuery> {
filters: AdHocVariableFilter[];
filters?: AdHocVariableFilter[];
groupByKeys?: string[];
timeRange?: TimeRange;
queries?: TQuery[];
+4
View File
@@ -365,6 +365,10 @@ export interface FeatureToggles {
*/
unlimitedLayoutsNesting?: boolean;
/**
* Enables viewing non-applicable drilldowns on a panel level
*/
perPanelNonApplicableDrilldowns?: boolean;
/**
* Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard
*/
panelFilterVariable?: boolean;
@@ -426,6 +426,9 @@ export const versionedComponents = {
loadingBar: {
'10.0.0': () => `Panel loading bar`,
},
PanelNonApplicableDrilldownsSubHeader: {
'12.4.0': 'Panel non-applicable drilldowns subheader',
},
HoverWidget: {
container: {
'10.1.0': 'data-testid hover-header-container',
@@ -421,6 +421,70 @@ Component used for rendering content wrapped in the same style as grafana panels
</PanelChrome>
</ExampleFrame>
### Sub-header content
The panel supports displaying additional content below the main header using the `subHeaderContent` prop. This can be used to show context-specific information.
```tsx
<PanelChrome
title="My awesome panel title"
subHeaderContent={
<div style={{ display: 'flex', gap: '8px', padding: '4px 8px' }}>
<span style={{ fontSize: '12px', color: '#999' }}>Additional info</span>
</div>
}
width={400}
height={200}
>
{(innerwidth, innerheight) => {
return (
<div
style={{
width: innerwidth,
height: innerheight,
background: 'rgba(230,0,0,0.05)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
Content
</div>
);
}}
</PanelChrome>
```
<ExampleFrame>
<PanelChrome
title="My awesome panel title"
subHeaderContent={
<div style={{ display: 'flex', gap: '8px', padding: '4px 8px' }}>
<span style={{ fontSize: '12px', color: '#999' }}>Additional info</span>
</div>
}
width={400}
height={200}
>
{(innerwidth, innerheight) => {
return (
<div
style={{
width: innerwidth,
height: innerheight,
background: 'rgba(230,0,0,0.05)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
Content
</div>
);
}}
</PanelChrome>
</ExampleFrame>
### Collapsible
The panel can be collapsed/expanded by clicking on the chevron or the title.
@@ -196,3 +196,30 @@ it('collapses the uncontrolled panel when user clicks on the chevron or the titl
expect(button).not.toHaveAttribute('aria-controlls');
expect(screen.queryByTestId(selectors.components.Panels.Panel.content)?.id).toBe(undefined);
});
it('renders panel with a header if prop subHeaderContent', () => {
setup({
subHeaderContent: <div key="sub-header-test">This should be a sub-header node</div>,
});
expect(screen.getByTestId(selectors.components.Panels.Panel.headerContainer)).toBeInTheDocument();
});
it('renders panel with sub-header content in place if prop subHeaderContent', () => {
setup({
subHeaderContent: <div key="sub-header-test">This should be a sub-header node</div>,
});
expect(screen.getByText('This should be a sub-header node')).toBeInTheDocument();
});
it('does not render sub-header content when panel is collapsed', () => {
setup({
title: 'Test Panel',
collapsible: true,
collapsed: true,
subHeaderContent: <div key="sub-header-test">This should be a sub-header node</div>,
});
expect(screen.queryByText('This should be a sub-header node')).not.toBeInTheDocument();
});
@@ -76,6 +76,11 @@ interface BaseProps {
* If true, the VizPanelMenu will always be visible in the panel header. Defaults to false.
*/
showMenuAlways?: boolean;
/**
* Content to display in the sub-header area below the main header.
* Can contain text, pills, links, buttons, or any other React elements.
*/
subHeaderContent?: ReactNode;
}
interface FixedDimensions extends BaseProps {
@@ -157,6 +162,7 @@ export function PanelChrome({
onMouseEnter,
onDragStart,
showMenuAlways = false,
subHeaderContent,
}: PanelChromeProps) {
const theme = useTheme2();
const styles = useStyles2(getStyles);
@@ -164,6 +170,7 @@ export function PanelChrome({
const panelTitleId = useId().replace(/:/g, '_');
const { isSelected, onSelect, isSelectable } = useElementSelection(selectionId);
const pointerDistance = usePointerDistance();
const [subHeaderRef, { height: measuredSubHeaderHeight }] = useMeasure<HTMLDivElement>();
const hasHeader = !hoverHeader;
@@ -184,11 +191,13 @@ export function PanelChrome({
const isPanelTransparent = displayMode === 'transparent';
const headerHeight = getHeaderHeight(theme, hasHeader);
const subHeaderHeight = Math.min(measuredSubHeaderHeight, headerHeight);
const { contentStyle, innerWidth, innerHeight } = getContentStyle(
padding,
theme,
headerHeight,
collapsed,
subHeaderHeight,
height,
width
);
@@ -395,37 +404,44 @@ export function PanelChrome({
)}
{hasHeader && (
<div
className={cx(styles.headerContainer, dragClass)}
style={headerStyles}
data-testid={selectors.components.Panels.Panel.headerContainer}
onPointerDown={onPointerDown}
onMouseEnter={isSelectable ? onHeaderEnter : undefined}
onMouseLeave={isSelectable ? onHeaderLeave : undefined}
onPointerUp={onPointerUp}
>
{statusMessage && (
<div className={dragClassCancel}>
<PanelStatus
message={statusMessage}
onClick={statusMessageOnClick}
ariaLabel={t('grafana-ui.panel-chrome.ariaLabel-panel-status', 'Panel status')}
<>
<div
className={cx(styles.headerContainer, dragClass)}
style={headerStyles}
data-testid={selectors.components.Panels.Panel.headerContainer}
onPointerDown={onPointerDown}
onMouseEnter={isSelectable ? onHeaderEnter : undefined}
onMouseLeave={isSelectable ? onHeaderLeave : undefined}
onPointerUp={onPointerUp}
>
{statusMessage && (
<div className={dragClassCancel}>
<PanelStatus
message={statusMessage}
onClick={statusMessageOnClick}
ariaLabel={t('grafana-ui.panel-chrome.ariaLabel-panel-status', 'Panel status')}
/>
</div>
)}
{headerContent}
{menu && (
<PanelMenu
menu={menu}
title={typeof title === 'string' ? title : undefined}
placement="bottom-end"
menuButtonClass={cx(styles.menuItem, dragClassCancel, showOnHoverClass)}
onOpenMenu={onOpenMenu}
/>
)}
</div>
{!collapsed && subHeaderContent && (
<div className={styles.subHeader} ref={subHeaderRef}>
{subHeaderContent}
</div>
)}
{headerContent}
{menu && (
<PanelMenu
menu={menu}
title={typeof title === 'string' ? title : undefined}
placement="bottom-end"
menuButtonClass={cx(styles.menuItem, dragClassCancel, showOnHoverClass)}
onOpenMenu={onOpenMenu}
/>
)}
</div>
</>
)}
{!collapsed && (
@@ -466,6 +482,7 @@ const getContentStyle = (
theme: GrafanaTheme2,
headerHeight: number,
collapsed: boolean,
subHeaderHeight: number,
height?: number,
width?: number
) => {
@@ -481,7 +498,7 @@ const getContentStyle = (
let innerHeight = 0;
if (height) {
innerHeight = height - headerHeight - panelPadding - panelBorder;
innerHeight = height - headerHeight - panelPadding - panelBorder - subHeaderHeight;
}
if (collapsed) {
@@ -579,6 +596,15 @@ const getStyles = (theme: GrafanaTheme2) => {
padding: newPanelPadding ? theme.spacing(0, 1, 0, 1.5) : theme.spacing(0, 0.5, 0, 1),
gap: theme.spacing(1),
}),
subHeader: css({
label: 'panel-sub-header',
display: 'flex',
alignItems: 'center',
maxHeight: theme.spacing.gridSize * theme.components.panel.headerHeight,
padding: newPanelPadding ? theme.spacing(0, 1, 0, 1.5) : theme.spacing(0, 0.5, 0, 1),
overflow: 'hidden',
gap: theme.spacing(1),
}),
pointer: css({
cursor: 'pointer',
}),
+7
View File
@@ -586,6 +586,13 @@ var (
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
{
Name: "perPanelNonApplicableDrilldowns",
Description: "Enables viewing non-applicable drilldowns on a panel level",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
{
Name: "panelFilterVariable",
Description: "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard",
+1
View File
@@ -81,6 +81,7 @@ dashboardScene,GA,@grafana/dashboards-squad,false,false,true
dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,false
dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true
unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true
perPanelNonApplicableDrilldowns,experimental,@grafana/dashboards-squad,false,false,true
panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true
pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false
canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
81 dashboardNewLayouts experimental @grafana/dashboards-squad false false false
82 dashboardUndoRedo experimental @grafana/dashboards-squad false false true
83 unlimitedLayoutsNesting experimental @grafana/dashboards-squad false false true
84 perPanelNonApplicableDrilldowns experimental @grafana/dashboards-squad false false true
85 panelFilterVariable experimental @grafana/dashboards-squad false false true
86 pdfTables preview @grafana/grafana-operator-experience-squad false false false
87 canvasPanelPanZoom preview @grafana/dataviz-squad false false true
+283 -270
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,189 @@
import { css, cx } from '@emotion/css';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { AdHocFiltersVariable, GroupByVariable, SceneQueryRunner } from '@grafana/scenes';
import { Tooltip, measureText, useStyles2, useTheme2 } from '@grafana/ui';
import { getDrilldownApplicability } from '../utils/drilldownUtils';
const GAP_SIZE = 8;
const FONT_SIZE = 12;
interface Props {
filtersVar?: AdHocFiltersVariable;
groupByVar?: GroupByVariable;
queryRunner: SceneQueryRunner;
}
export function PanelNonApplicableDrilldownsSubHeader({ filtersVar, groupByVar, queryRunner }: Props) {
const styles = useStyles2(getStyles);
const theme = useTheme2();
const [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
// Subscribe to state changes (this triggers re-renders when state changes)
const filtersState = filtersVar?.useState();
const groupByState = groupByVar?.useState();
const [nonApplicable, setNonApplicable] = useState<string[]>([]);
const [visibleCount, setVisibleCount] = useState<number>(0);
// Create stable string representations to detect actual changes
const filterKey = useMemo(() => {
const filters = filtersState?.filters ?? [];
const originFilters = filtersState?.originFilters ?? [];
const filterValues = [...filters, ...originFilters];
return JSON.stringify(filterValues.map((f) => `${f.key}${f.operator}${f.value}${f.origin ?? ''}`));
}, [filtersState?.filters, filtersState?.originFilters]);
const groupByKey = useMemo(() => {
const value = groupByState?.value ?? [];
const groupByValues = Array.isArray(value) ? value : value ? [value] : [];
return JSON.stringify(groupByValues);
}, [groupByState?.value]);
useEffect(() => {
const fetchApplicability = async () => {
const filters = filtersState?.filters ?? [];
const originFilters = filtersState?.originFilters ?? [];
const value = groupByState?.value ?? [];
const filterValues = [...filters, ...originFilters];
const groupByValues = Array.isArray(value) ? value : value ? [value] : [];
const labels: string[] = [];
const applicability = await getDrilldownApplicability(queryRunner, filtersVar, groupByVar);
if (filterValues.length) {
const nonApplicableFilters = filterValues.filter((filter) => {
const result = applicability?.find((entry) => entry.key === filter.key && entry.origin === filter.origin);
return result && !result.applicable;
});
labels.push(...nonApplicableFilters.map((filter) => `${filter.key} ${filter.operator} ${filter.value}`));
}
if (groupByValues.length) {
const nonApplicableKeys = groupByValues
.filter((groupByKey) => {
const result = applicability?.find((entry) => entry.key === groupByKey);
return result && !result.applicable;
})
.map((key) => String(key));
labels.push(...nonApplicableKeys);
}
setNonApplicable(labels);
};
fetchApplicability();
}, [
filterKey,
groupByKey,
filtersVar,
groupByVar,
queryRunner,
filtersState?.filters,
groupByState?.value,
filtersState?.originFilters,
]);
useLayoutEffect(() => {
if (!nonApplicable.length) {
setVisibleCount(0);
return;
}
setVisibleCount(calculateVisibleCount(nonApplicable, containerWidth, theme));
}, [containerWidth, nonApplicable, theme]);
if (nonApplicable.length === 0) {
return null;
}
const visibleFilters = nonApplicable.slice(0, visibleCount);
const remainingCount = nonApplicable.length - visibleCount;
const hasOverflow = remainingCount > 0;
const remainingFilters = nonApplicable.slice(visibleCount);
return (
<div
ref={sizeRef}
className={styles.container}
data-testid={selectors.components.Panels.Panel.PanelNonApplicableDrilldownsSubHeader}
>
{visibleFilters.map((filter, index) => (
<div key={`${filter}-${index}`} className={cx(styles.disabledPill, styles.strikethrough, styles.pill)}>
{filter}
</div>
))}
{hasOverflow && (
<Tooltip content={remainingFilters.join(', ')}>
<div className={cx(styles.disabledPill, styles.pill)}>+{remainingCount}</div>
</Tooltip>
)}
</div>
);
}
function getPillWidth(label: string) {
return Math.ceil(measureText(label, FONT_SIZE).width) + GAP_SIZE;
}
function calculateVisibleCount(labels: string[], containerWidth: number, theme: GrafanaTheme2) {
let usedWidth = 0;
let visible = 0;
for (let i = 0; i < labels.length; i++) {
const pillWidth = getPillWidth(labels[i]);
const gapBefore = visible > 0 ? GAP_SIZE : 0;
const nextWidth = usedWidth + gapBefore + pillWidth;
const remaining = labels.length - (i + 1);
if (remaining > 0) {
const overflowWidth = getPillWidth(`+${remaining}`);
if (nextWidth + GAP_SIZE + overflowWidth > containerWidth) {
break;
}
} else if (nextWidth > containerWidth) {
break;
}
usedWidth = nextWidth;
visible++;
}
return visible;
}
function getStyles(theme: GrafanaTheme2) {
return {
container: css({
display: 'flex',
flexWrap: 'nowrap',
gap: theme.spacing(1),
width: '100%',
overflow: 'hidden',
}),
pill: css({
padding: theme.spacing(0.2, 0.4),
borderRadius: theme.shape.radius.default,
whiteSpace: 'nowrap',
flexShrink: 0,
fontSize: theme.typography.bodySmall.fontSize,
}),
disabledPill: css({
background: theme.colors.action.selected,
color: theme.colors.text.disabled,
border: 0,
'&:hover': {
background: theme.colors.action.selected,
},
}),
strikethrough: css({
textDecoration: 'line-through',
}),
};
}
@@ -0,0 +1,184 @@
import { of } from 'rxjs';
import { DataQueryRequest, DataSourceApi, LoadingState } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
import { setPluginImportUtils } from '@grafana/runtime';
import {
AdHocFiltersVariable,
GroupByVariable,
SceneDataTransformer,
SceneQueryRunner,
SceneVariableSet,
VizPanel,
VizPanelState,
} from '@grafana/scenes';
import { activateFullSceneTree } from '../utils/test-utils';
import { DashboardScene } from './DashboardScene';
import { VizPanelSubHeader } from './VizPanelSubHeader';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
return of({
state: LoadingState.Loading,
series: [],
timeRange: request.range,
});
});
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
return runRequestMock(ds, request);
},
getDataSourceSrv: () => ({
get: jest.fn().mockResolvedValue({
getRef: () => ({ uid: 'ds-1', type: 'test' }),
}),
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds-1', type: 'test' }),
}),
getPluginImportUtils: () => ({
getPanelPluginFromCache: jest.fn(() => undefined),
}),
}));
setPluginImportUtils({
importPanelPlugin: () => Promise.resolve(getPanelPlugin({})),
getPanelPluginFromCache: () => undefined,
});
describe('VizPanelSubHeader', () => {
it('renders when the drilldown variables apply to the panel', async () => {
const { subHeader } = await buildScene();
expect(subHeader.state.supportsApplicability).toBe(true);
});
it('renders only one drilldown var is set', async () => {
const { subHeader } = await buildScene({ noGroupBy: true });
expect(subHeader.state.supportsApplicability).toBe(true);
});
it('does not render when applicability is disabled', async () => {
const { subHeader } = await buildScene({ applicabilityEnabled: false });
expect(subHeader.state.supportsApplicability).toBe(false);
});
it('does not render when the datasource uid does not match', async () => {
const { subHeader } = await buildScene({
variableDatasourceUid: 'other-ds',
});
expect(subHeader.state.supportsApplicability).toBe(false);
});
it('no longer renders if variable ds changes to a different type', async () => {
const { subHeader, adhocFiltersVariable } = await buildScene({
noGroupBy: true,
});
expect(subHeader.state.supportsApplicability).toBe(true);
adhocFiltersVariable.setState({ datasource: { uid: 'ds-2' } });
expect(subHeader.state.supportsApplicability).toBe(false);
});
it('no longers renders if variable applicability becomes disabled', async () => {
const { subHeader, adhocFiltersVariable } = await buildScene({
noGroupBy: true,
});
expect(subHeader.state.supportsApplicability).toBe(true);
adhocFiltersVariable.setState({ applicabilityEnabled: false });
expect(subHeader.state.supportsApplicability).toBe(false);
});
it('continues to render if one adhoc is disabled, but groupby remains active', async () => {
const { subHeader, adhocFiltersVariable } = await buildScene();
expect(subHeader.state.supportsApplicability).toBe(true);
adhocFiltersVariable.setState({ applicabilityEnabled: false });
expect(subHeader.state.supportsApplicability).toBe(true);
});
it('stops rendering if queryRunner changes datasource to different one than vars', async () => {
const { subHeader, queryRunner } = await buildScene();
expect(subHeader.state.supportsApplicability).toBe(true);
queryRunner.setState({ datasource: { uid: 'ds-2' } });
expect(subHeader.state.supportsApplicability).toBe(false);
});
});
interface BuildSceneOptions {
applicabilityEnabled?: boolean;
variableDatasourceUid?: string;
noGroupBy?: boolean;
}
async function buildScene(options?: BuildSceneOptions) {
const subHeader = new VizPanelSubHeader({});
const queryRunner = new SceneQueryRunner({
datasource: { uid: 'ds-1' },
queries: [{ refId: 'A', datasource: { uid: 'ds-1' } }],
});
const adhocFiltersVariable = new AdHocFiltersVariable({
name: 'adhoc',
label: 'adhoc',
filters: [{ key: 'filter-1', operator: '=', value: 'value-1' }],
datasource: { uid: options?.variableDatasourceUid ?? 'ds-1' },
applicabilityEnabled: options?.applicabilityEnabled ?? true,
});
const groupByVariable = new GroupByVariable({
name: 'group',
label: 'group',
value: ['groupBy'],
text: ['groupBy'],
options: [],
applicabilityEnabled: options?.applicabilityEnabled ?? true,
datasource: { uid: options?.variableDatasourceUid ?? 'ds-1' },
});
const dataProvider = new SceneDataTransformer({
$data: queryRunner,
transformations: [],
});
const panelState: VizPanelState = {
key: 'panel-1',
title: 'Panel A',
pluginId: 'timeseries',
subHeader,
$data: dataProvider,
options: {},
fieldConfig: { defaults: {}, overrides: [] },
};
const panel = new VizPanel(panelState);
const scene = new DashboardScene({
$variables: new SceneVariableSet({
variables: options?.noGroupBy ? [adhocFiltersVariable] : [groupByVariable, adhocFiltersVariable],
}),
body: DefaultGridLayoutManager.fromVizPanels([panel]),
});
activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
return { subHeader, groupByVariable, adhocFiltersVariable, queryRunner };
}
@@ -0,0 +1,193 @@
import { Unsubscribable } from 'rxjs';
import {
SceneComponentProps,
SceneObjectState,
SceneObjectBase,
sceneGraph,
AdHocFiltersVariable,
GroupByVariable,
SceneQueryRunner,
VizPanel,
} from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema';
import { verifyDrilldownApplicability } from '../utils/drilldownUtils';
import { PanelNonApplicableDrilldownsSubHeader } from './PanelNonApplicableDrilldownsSubHeader';
interface ApplicabilitySupportHelperState {
datasource: DataSourceRef | null;
applicabilityEnabled?: boolean;
}
export interface VizPanelSubHeaderState extends SceneObjectState {
hideNonApplicableDrilldowns?: boolean;
supportsApplicability?: boolean;
}
export class VizPanelSubHeader extends SceneObjectBase<VizPanelSubHeaderState> {
static Component = VizPanelSubHeaderRenderer;
private _adHocVar?: AdHocFiltersVariable;
private _groupByVar?: GroupByVariable;
private _adHocSub?: Unsubscribable;
private _groupBySub?: Unsubscribable;
private _queryRunnerDatasource?: DataSourceRef;
constructor(state: Partial<VizPanelSubHeaderState>) {
super({
hideNonApplicableDrilldowns: state.hideNonApplicableDrilldowns ?? false,
...state,
});
this.addActivationHandler(this._onActivate);
}
private _onActivate = () => {
if (!this.parent || !(this.parent instanceof VizPanel)) {
throw new Error('VizPanelSubHeader can be used only with VizPanel');
}
if (!this.state.hideNonApplicableDrilldowns) {
this.subscribeToDrilldownVariableChanges();
}
return () => {
this._groupBySub?.unsubscribe();
this._adHocSub?.unsubscribe();
};
};
private subscribeToDrilldownVariableChanges() {
const vars = sceneGraph.getVariables(this);
const queryRunner = this.getQueryRunner();
this._adHocVar = vars.state.variables.find((variable) => variable instanceof AdHocFiltersVariable);
this._groupByVar = vars.state.variables.find((variable) => variable instanceof GroupByVariable);
this._queryRunnerDatasource = queryRunner?.state.datasource;
this.setDrilldownApplicabilitySupportHelper();
// keep track of queryRunner datasource updates andupdate rendering
this._subs.add(
queryRunner?.subscribeToState((n, p) => {
if (n.datasource !== p.datasource) {
this._queryRunnerDatasource = n.datasource;
this.setDrilldownApplicabilitySupportHelper();
}
})
);
// check when var set updates and search for drilldown vars
this._subs.add(
vars.subscribeToState((n) => {
this._adHocVar = n.variables.find((variable) => variable instanceof AdHocFiltersVariable);
this._groupByVar = n.variables.find((variable) => variable instanceof GroupByVariable);
this.refreshDrilldownVarsSubscriptions();
})
);
// adhoc sub so if that changes, we potentially update rendering
this._adHocSub = this._adHocVar?.subscribeToState((n, p) => {
if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) {
this.setDrilldownApplicabilitySupportHelper({
datasource: n.datasource,
applicabilityEnabled: n.applicabilityEnabled,
});
}
});
// same for groupBy
this._groupBySub = this._groupByVar?.subscribeToState((n, p) => {
if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) {
this.setDrilldownApplicabilitySupportHelper(undefined, {
datasource: n.datasource,
applicabilityEnabled: n.applicabilityEnabled,
});
}
});
}
private refreshDrilldownVarsSubscriptions() {
if (this._groupByVar) {
this._groupBySub?.unsubscribe();
this._groupByVar?.subscribeToState((n, p) => {
if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) {
this.setDrilldownApplicabilitySupportHelper(undefined, {
datasource: n.datasource,
applicabilityEnabled: n.applicabilityEnabled,
});
}
});
}
if (this._adHocVar) {
this._adHocVar?.subscribeToState((n, p) => {
if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) {
this.setDrilldownApplicabilitySupportHelper({
datasource: n.datasource,
applicabilityEnabled: n.applicabilityEnabled,
});
}
});
}
}
private setDrilldownApplicabilitySupportHelper(
adHocData?: ApplicabilitySupportHelperState,
groupByData?: ApplicabilitySupportHelperState
) {
this.setState({
supportsApplicability:
verifyDrilldownApplicability(
this,
this._queryRunnerDatasource,
adHocData?.datasource ?? this._adHocVar?.state.datasource ?? null,
adHocData?.applicabilityEnabled ?? this._adHocVar?.state.applicabilityEnabled ?? false
) ||
verifyDrilldownApplicability(
this,
this._queryRunnerDatasource,
groupByData?.datasource ?? this._groupByVar?.state.datasource ?? null,
groupByData?.applicabilityEnabled ?? this._groupByVar?.state.applicabilityEnabled ?? false
),
});
}
public getQueryRunner() {
const panel = this.parent;
const dataObject = panel ? sceneGraph.getData(panel) : undefined;
const queryRunner = dataObject?.state.$data;
if (!queryRunner || !(queryRunner instanceof SceneQueryRunner)) {
return null;
}
return queryRunner;
}
}
export function VizPanelSubHeaderRenderer({ model }: SceneComponentProps<VizPanelSubHeader>) {
const { supportsApplicability, hideNonApplicableDrilldowns } = model.useState();
const variables = sceneGraph.getVariables(model);
const adhocFiltersVar = variables.state.variables.find((variable) => variable instanceof AdHocFiltersVariable);
const groupByVar = variables.state.variables.find((variable) => variable instanceof GroupByVariable);
const queryRunner = model.getQueryRunner();
if (!queryRunner || hideNonApplicableDrilldowns || !supportsApplicability) {
return null;
}
return (
<PanelNonApplicableDrilldownsSubHeader
filtersVar={adhocFiltersVar}
groupByVar={groupByVar}
queryRunner={queryRunner}
/>
);
}
@@ -31,6 +31,7 @@ import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks';
import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior';
import { PanelNotices } from '../../scene/PanelNotices';
import { VizPanelSubHeader } from '../../scene/VizPanelSubHeader';
import { AutoGridItem } from '../../scene/layout-auto-grid/AutoGridItem';
import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem';
import { PanelTimeRange } from '../../scene/panel-timerange/PanelTimeRange';
@@ -70,6 +71,9 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel {
seriesLimit: config.panelSeriesLimit,
$data: createPanelDataProvider(panel),
titleItems,
subHeader: new VizPanelSubHeader({
hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns,
}),
$behaviors: [],
extendPanelContext: setDashboardPanelContext,
};
@@ -106,6 +110,9 @@ export function buildLibraryPanel(panel: LibraryPanelKind, id?: number): VizPane
const vizPanelState: VizPanelState = {
key: getVizPanelKeyForPanelId(id ?? panel.spec.id),
titleItems,
subHeader: new VizPanelSubHeader({
hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns,
}),
seriesLimit: config.panelSeriesLimit,
$behaviors: [
new LibraryPanelBehavior({
@@ -45,6 +45,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { PanelNotices } from '../scene/PanelNotices';
import { VizPanelSubHeader } from '../scene/VizPanelSubHeader';
import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
@@ -440,6 +441,9 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
hoverHeaderOffset: 0,
$data: createPanelDataProvider(panel),
titleItems,
subHeader: new VizPanelSubHeader({
hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns,
}),
$behaviors: [],
extendPanelContext: setDashboardPanelContext,
_UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel),
@@ -0,0 +1,236 @@
import type { DrilldownsApplicability } from '@grafana/data';
import { DataSourceSrv, getDataSourceSrv } from '@grafana/runtime';
import {
AdHocFiltersVariable,
GroupByVariable,
SceneQueryRunner,
SceneDataTransformer,
SceneVariableSet,
VizPanel,
VizPanelState,
} from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema';
import { DashboardScene } from '../scene/DashboardScene';
import { VizPanelSubHeader } from '../scene/VizPanelSubHeader';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { getDrilldownApplicability, verifyDrilldownApplicability } from './drilldownUtils';
jest.mock('@grafana/runtime', () => {
const actual = jest.requireActual('@grafana/runtime');
return {
...actual,
getDataSourceSrv: jest.fn(),
};
});
const getDataSourceSrvMock = getDataSourceSrv as jest.MockedFunction<typeof getDataSourceSrv>;
// Helper to create partial DataSourceSrv mock
const createMockDataSourceSrv = (overrides: Partial<DataSourceSrv> = {}): DataSourceSrv => ({
get: jest.fn(),
getList: jest.fn(),
getInstanceSettings: jest.fn(),
reload: jest.fn(),
registerRuntimeDataSource: jest.fn(),
...overrides,
});
describe('verifyDrilldownApplicability', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('returns true when applicability is enabled and datasources match', () => {
const subHeader = new VizPanelSubHeader({});
const result = verifyDrilldownApplicability(
subHeader,
{ uid: 'ds-1' } as DataSourceRef,
{ uid: 'ds-1' } as DataSourceRef,
true
);
expect(result).toBe(true);
});
it('returns false when datasources differ or applicability disabled', () => {
const subHeader = new VizPanelSubHeader({});
const mismatch = verifyDrilldownApplicability(
subHeader,
{ uid: 'ds-1' } as DataSourceRef,
{ uid: 'ds-2' } as DataSourceRef,
true
);
expect(mismatch).toBe(false);
const disabled = verifyDrilldownApplicability(
subHeader,
{ uid: 'ds-1' } as DataSourceRef,
{ uid: 'ds-1' } as DataSourceRef,
false
);
expect(disabled).toBe(false);
});
});
describe('getDrilldownApplicability', () => {
beforeEach(() => {
const applicabilityFn = jest.fn().mockResolvedValue([]);
getDataSourceSrvMock.mockReturnValue(
createMockDataSourceSrv({
get: jest.fn().mockResolvedValue({ getDrilldownsApplicability: applicabilityFn }),
getInstanceSettings: jest.fn(),
})
);
});
afterEach(() => {
jest.clearAllMocks();
});
it('returns undefined when no drilldown variables are provided', async () => {
const queryRunner = new SceneQueryRunner({
datasource: { uid: 'ds-1' },
queries: [{ refId: 'A' }],
});
const result = await getDrilldownApplicability(queryRunner);
expect(result).toBeUndefined();
expect(getDataSourceSrvMock).not.toHaveBeenCalled();
});
it('returns undefined when datasource lacks getDrilldownsApplicability', async () => {
getDataSourceSrvMock.mockReturnValue(
createMockDataSourceSrv({
get: jest.fn().mockResolvedValue({}),
getInstanceSettings: jest.fn(),
})
);
const { queryRunner, groupByVariable } = buildScene();
const result = await getDrilldownApplicability(queryRunner, undefined, groupByVariable);
expect(result).toBeUndefined();
});
it('returns undefined when drilldown variables use a different datasource', async () => {
const applicabilityFn = jest.fn().mockResolvedValue([]);
getDataSourceSrvMock.mockReturnValue(
createMockDataSourceSrv({
get: jest.fn().mockResolvedValue({ getDrilldownsApplicability: applicabilityFn }),
getInstanceSettings: jest.fn(),
})
);
const { queryRunner, adhocFiltersVariable, groupByVariable } = buildScene({
datasourceUid: 'ds-query',
filtersDatasourceUid: 'ds-other',
groupByDatasourceUid: 'ds-other',
});
const result = await getDrilldownApplicability(queryRunner, adhocFiltersVariable, groupByVariable);
expect(result).toBeUndefined();
expect(applicabilityFn).not.toHaveBeenCalled();
});
it('returns applicability data when datasource and variables align', async () => {
const applicability: DrilldownsApplicability[] = [{ key: 'region', applicable: true }];
const getApplicability = jest.fn().mockResolvedValue(applicability);
getDataSourceSrvMock.mockReturnValue(
createMockDataSourceSrv({
get: jest.fn().mockResolvedValue({ getDrilldownsApplicability: getApplicability }),
getInstanceSettings: jest.fn(),
})
);
const { queryRunner, adhocFiltersVariable, groupByVariable } = buildScene({
datasourceUid: 'ds-apply',
filtersDatasourceUid: 'ds-apply',
groupByDatasourceUid: 'ds-apply',
groupValues: ['region', 'instance'],
});
const result = await getDrilldownApplicability(queryRunner, adhocFiltersVariable, groupByVariable);
expect(getApplicability).toHaveBeenCalledTimes(1);
const payload = getApplicability.mock.calls[0][0];
expect(payload.groupByKeys).toEqual(['region', 'instance']);
expect(payload.filters).toEqual([]);
expect(payload.queries).toBe(queryRunner.state.data?.request?.targets);
expect(payload.timeRange).toBeDefined();
expect(result).toBe(applicability);
});
});
interface BuildSceneOptions {
datasourceUid?: string;
groupByDatasourceUid?: string;
filtersDatasourceUid?: string;
groupValues?: Array<string | number>;
}
function buildScene({
datasourceUid = 'ds-1',
groupByDatasourceUid = datasourceUid,
filtersDatasourceUid = datasourceUid,
groupValues = ['groupBy'],
}: BuildSceneOptions = {}) {
const subHeader = new VizPanelSubHeader({});
const queryRunner = new SceneQueryRunner({
datasource: { uid: datasourceUid },
queries: [{ refId: 'A', datasource: { uid: datasourceUid } }],
});
const adhocFiltersVariable = new AdHocFiltersVariable({
name: 'adhoc',
label: 'adhoc',
filters: [],
datasource: { uid: filtersDatasourceUid },
applicabilityEnabled: true,
});
const groupByVariable = new GroupByVariable({
name: 'group',
label: 'group',
value: groupValues,
text: groupValues.map((val) => String(val)),
options: [],
applicabilityEnabled: true,
datasource: { uid: groupByDatasourceUid },
});
const dataProvider = new SceneDataTransformer({
$data: queryRunner,
transformations: [],
});
const panelState: VizPanelState = {
key: 'panel-1',
title: 'Panel A',
pluginId: 'timeseries',
subHeader,
$data: dataProvider,
options: {},
fieldConfig: { defaults: {}, overrides: [] },
};
const panel = new VizPanel(panelState);
new DashboardScene({
$variables: new SceneVariableSet({
variables: [groupByVariable, adhocFiltersVariable],
}),
body: DefaultGridLayoutManager.fromVizPanels([panel]),
});
return { queryRunner, adhocFiltersVariable, groupByVariable };
}
@@ -0,0 +1,75 @@
import { DrilldownsApplicability } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { AdHocFiltersVariable, GroupByVariable, sceneGraph, SceneObject, SceneQueryRunner } from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema';
export function verifyDrilldownApplicability(
sourceObject: SceneObject,
queriesDataSource: DataSourceRef | undefined,
drilldownDatasource: DataSourceRef | null,
isApplicabilityEnabled?: boolean
): boolean {
const datasourceUid = sceneGraph.interpolate(sourceObject, queriesDataSource?.uid);
return Boolean(
isApplicabilityEnabled && datasourceUid === sceneGraph.interpolate(sourceObject, drilldownDatasource?.uid)
);
}
export async function getDrilldownApplicability(
queryRunner: SceneQueryRunner,
filtersVar?: AdHocFiltersVariable,
groupByVar?: GroupByVariable
): Promise<DrilldownsApplicability[] | undefined> {
//if no drilldown vars return
if (!filtersVar && !groupByVar) {
return;
}
const datasource = queryRunner.state.datasource;
const queries = queryRunner.state.data?.request?.targets;
const ds = await getDataSourceSrv().get(datasource?.uid);
// return if method not implemented
if (!ds.getDrilldownsApplicability) {
return;
}
const dsUid = sceneGraph.interpolate(queryRunner, datasource?.uid);
const timeRange = sceneGraph.getTimeRange(queryRunner).state.value;
const groupByKeys = [];
const filters = [];
const hasGroupByApplicability =
groupByVar && dsUid === sceneGraph.interpolate(groupByVar, groupByVar?.state.datasource?.uid);
const hasFiltersApplicability =
filtersVar && dsUid === sceneGraph.interpolate(filtersVar, filtersVar.state?.datasource?.uid);
// if neither vars use the ds from the queries, return
if (!hasGroupByApplicability && !hasFiltersApplicability) {
return;
}
if (hasGroupByApplicability) {
groupByKeys.push(
...(Array.isArray(groupByVar.state.value)
? groupByVar.state.value.map((v) => String(v))
: groupByVar.state.value
? [String(groupByVar.state.value)]
: [])
);
}
if (hasFiltersApplicability) {
filters.push(...filtersVar.state.filters, ...(filtersVar.state.originFilters ?? []));
}
return await ds.getDrilldownsApplicability({
groupByKeys,
filters,
queries,
timeRange,
scopes: sceneGraph.getScopes(queryRunner),
});
}
@@ -24,6 +24,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel';
import { VizPanelSubHeader } from '../scene/VizPanelSubHeader';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
@@ -267,6 +268,9 @@ export function getDefaultVizPanel(): VizPanel {
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
hoverHeaderOffset: 0,
$behaviors: [],
subHeader: new VizPanelSubHeader({
hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns,
}),
extendPanelContext: setDashboardPanelContext,
menu: new VizPanelMenu({
$behaviors: [panelMenuBehavior],
+147 -85
View File
@@ -1473,7 +1473,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7":
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7":
version: 7.28.4
resolution: "@babel/runtime@npm:7.28.4"
checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163
@@ -2324,15 +2324,15 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:2.3.6":
version: 2.3.6
resolution: "@formatjs/ecma402-abstract@npm:2.3.6"
"@formatjs/ecma402-abstract@npm:2.3.4":
version: 2.3.4
resolution: "@formatjs/ecma402-abstract@npm:2.3.4"
dependencies:
"@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/intl-localematcher": "npm:0.6.1"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/30b1b5cd6b62ba46245f934429936592df5500bc1b089dc92dd49c826757b873dd92c305dcfe370701e4df6b057bf007782113abb9b65db550d73be4961718bc
checksum: 10/573971ffc291096a4b9fcc80b4708124e89bf2e3ac50e0f78b41eb797e9aa1b842f4dc3665e4467a853c738386821769d9e40408a1d25bc73323a1f057a16cf2
languageName: node
linkType: hard
@@ -2376,13 +2376,13 @@ __metadata:
linkType: hard
"@formatjs/intl-durationformat@npm:^0.7.0":
version: 0.7.6
resolution: "@formatjs/intl-durationformat@npm:0.7.6"
version: 0.7.4
resolution: "@formatjs/intl-durationformat@npm:0.7.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
tslib: "npm:^2.8.0"
checksum: 10/442236ba85bcd9cb7296c43a708271fa09f110b1ca9d5899066d00812fc2965eaeaec6b5240be421b80daba62860352131088449ba0fcd2061f671cec6240f0b
checksum: 10/d62273ecd635475ca91e9b501301f3f396403fa91b584c550734b19b2d194ba1316b27303fed985c1d42ae933d54eb220da6540edfdf376b0d9371ecfd0d4e15
languageName: node
linkType: hard
@@ -2395,12 +2395,12 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.6.2":
version: 0.6.2
resolution: "@formatjs/intl-localematcher@npm:0.6.2"
"@formatjs/intl-localematcher@npm:0.6.1":
version: 0.6.1
resolution: "@formatjs/intl-localematcher@npm:0.6.1"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/eb12a7f5367bbecdfafc20d7f005559ce840f420e970f425c5213d35e94e86dfe75bde03464971a26494bf8427d4961269db22ecad2834f2a19d888b5d9cc064
checksum: 10/c7b3bc8395d18670677f207b2fd107561fff5d6394a9b4273c29e0bea920300ec3a2eefead600ebb7761c04a770cada28f78ac059f84d00520bfb57a9db36998
languageName: node
linkType: hard
@@ -3596,11 +3596,11 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes-react@npm:^6.46.0":
version: 6.46.1
resolution: "@grafana/scenes-react@npm:6.46.1"
"@grafana/scenes-react@npm:6.47.1":
version: 6.47.1
resolution: "@grafana/scenes-react@npm:6.47.1"
dependencies:
"@grafana/scenes": "npm:6.46.1"
"@grafana/scenes": "npm:6.47.1"
lru-cache: "npm:^10.2.2"
react-use: "npm:^17.4.0"
peerDependencies:
@@ -3612,7 +3612,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/47007ae998dd52c774f1b53cc1cbd62cbff6c077886ba36381f371454918bd7fbe9ea9373e021a38ba0144e507c8c51a435a2be1aa8250a36c37d7bbfa72fb9d
checksum: 10/dc20f9ee80eaf648665f7449e3ccb3b640a931f8f4a1be89599dce17eb0f52e763e3a603a4d491d9886b3e6cdf2ad3634124252c223315917206200f7cd6da16
languageName: node
linkType: hard
@@ -3642,9 +3642,9 @@ __metadata:
languageName: node
linkType: hard
"@grafana/scenes@npm:6.46.1, @grafana/scenes@npm:^6.46.0":
version: 6.46.1
resolution: "@grafana/scenes@npm:6.46.1"
"@grafana/scenes@npm:6.47.1":
version: 6.47.1
resolution: "@grafana/scenes@npm:6.47.1"
dependencies:
"@floating-ui/react": "npm:^0.26.16"
"@leeoniya/ufuzzy": "npm:^1.0.16"
@@ -3664,7 +3664,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/e72a90833a94307c6a4633d9f637e407f681d2be9fa27938158b0bdbb9520e31a3ff804eaab3f5e146673af5a21822b6a8279b4b8fd2ead79b94e2593e366144
checksum: 10/bc0c76258955058e7493b04e7cdd5d59dcc4159adf06da0837e992716ea15700b54f8403614df04326350363dc3344fb2602a2e8f7807724571659b4bd95aded
languageName: node
linkType: hard
@@ -4118,13 +4118,20 @@ __metadata:
languageName: node
linkType: hard
"@inquirer/figures@npm:^1.0.14, @inquirer/figures@npm:^1.0.3":
"@inquirer/figures@npm:^1.0.14":
version: 1.0.14
resolution: "@inquirer/figures@npm:1.0.14"
checksum: 10/39df361eb607cea5a020d457e25f9c6aee3a1de8975c6295a4b3bfe86ba7e7f7bfbefa6a52b145b1790f2690e5c8f10eb822e5bc764aff7ba00a6cd24eec5a25
languageName: node
linkType: hard
"@inquirer/figures@npm:^1.0.3":
version: 1.0.11
resolution: "@inquirer/figures@npm:1.0.11"
checksum: 10/357ddd2e83718bc3c9189d518b93fd69099af9c860354df9a5ac0ec024cb5df1228ae4608d2de7625624d2adcd047db813f29426a610eaae7b9e449f8c753c6b
languageName: node
linkType: hard
"@inquirer/input@npm:^4.2.5":
version: 4.2.5
resolution: "@inquirer/input@npm:4.2.5"
@@ -6096,9 +6103,9 @@ __metadata:
linkType: hard
"@openfeature/core@npm:^1.9.0":
version: 1.9.1
resolution: "@openfeature/core@npm:1.9.1"
checksum: 10/6099e16b1b4cd6e3c45c05ab4acd44c9cb4ab501b676ab6f3e77f0be1b56abc7506c5629187381333a02975d315d831e0905582435b356d9676255e72a465899
version: 1.9.0
resolution: "@openfeature/core@npm:1.9.0"
checksum: 10/c6d20edc09053afd99752fe46d8328158680950bca4b86679f67f79249d7226eea127b31fffdc38e26ecb729f2bab5a4a5a7c1db708ae76b7fbbac68cd56f094
languageName: node
linkType: hard
@@ -6123,11 +6130,11 @@ __metadata:
linkType: hard
"@openfeature/web-sdk@npm:^1.6.1":
version: 1.7.1
resolution: "@openfeature/web-sdk@npm:1.7.1"
version: 1.6.1
resolution: "@openfeature/web-sdk@npm:1.6.1"
peerDependencies:
"@openfeature/core": ^1.9.0
checksum: 10/358cabfddda8bf67a9bd96ce28f0f84a3218416907f8df4f2a58750ee840d164d86a1c859e8c9aa92d132fa28fcf72a0f17ceece798ff8af297d3e1c1e30cdab
checksum: 10/8bd7d1ea386e21cdd7492cab2fd1d2b138b4e6a376a4c0a40244633e5955f6452039bc2633fc5230bd7b494506a4137ba7210d40850634f9618f77a0ee435f9d
languageName: node
linkType: hard
@@ -7107,7 +7114,7 @@ __metadata:
languageName: node
linkType: hard
"@reduxjs/toolkit@npm:2.10.1, @reduxjs/toolkit@npm:^2.9.0":
"@reduxjs/toolkit@npm:2.10.1":
version: 2.10.1
resolution: "@reduxjs/toolkit@npm:2.10.1"
dependencies:
@@ -7129,6 +7136,28 @@ __metadata:
languageName: node
linkType: hard
"@reduxjs/toolkit@npm:^2.9.0":
version: 2.9.2
resolution: "@reduxjs/toolkit@npm:2.9.2"
dependencies:
"@standard-schema/spec": "npm:^1.0.0"
"@standard-schema/utils": "npm:^0.3.0"
immer: "npm:^10.0.3"
redux: "npm:^5.0.1"
redux-thunk: "npm:^3.1.0"
reselect: "npm:^5.1.0"
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
peerDependenciesMeta:
react:
optional: true
react-redux:
optional: true
checksum: 10/7a8fa57086bc44dafe2f04428d6557edad46ee6eb46b2fe80468ebf934a441a7d4d750b50a6d41bc360d9ab61baadb1dddb5a8f556cfac68977f56e832f509a2
languageName: node
linkType: hard
"@remix-run/router@npm:1.19.1":
version: 1.19.1
resolution: "@remix-run/router@npm:1.19.1"
@@ -9007,7 +9036,7 @@ __metadata:
languageName: node
linkType: hard
"@swc/core@npm:1.13.19, @swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22":
"@swc/core@npm:1.13.19":
version: 1.13.19
resolution: "@swc/core@npm:1.13.19"
dependencies:
@@ -9053,7 +9082,7 @@ __metadata:
languageName: node
linkType: hard
"@swc/core@npm:1.13.3":
"@swc/core@npm:1.13.3, @swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22":
version: 1.13.3
resolution: "@swc/core@npm:1.13.3"
dependencies:
@@ -12287,7 +12316,18 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1, axios@npm:^1.10.0, axios@npm:^1.12.2, axios@npm:^1.6.1, axios@npm:^1.8.3":
"axios@npm:^1, axios@npm:^1.10.0, axios@npm:^1.6.1, axios@npm:^1.8.3":
version: 1.12.2
resolution: "axios@npm:1.12.2"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.4"
proxy-from-env: "npm:^1.1.0"
checksum: 10/886a79770594eaad76493fecf90344b567bd956240609b5dcd09bd0afe8d3e6f1ad6d3257a93a483b6192b409d4b673d9515a34619e3e3ed1b2c0ec2a83b20ba
languageName: node
linkType: hard
"axios@npm:^1.12.2":
version: 1.13.2
resolution: "axios@npm:1.13.2"
dependencies:
@@ -13211,7 +13251,7 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:5.6.2, chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1, chalk@npm:^5.6.2":
"chalk@npm:5.6.2, chalk@npm:^5.6.2":
version: 5.6.2
resolution: "chalk@npm:5.6.2"
checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0
@@ -13249,6 +13289,13 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1":
version: 5.4.1
resolution: "chalk@npm:5.4.1"
checksum: 10/29df3ffcdf25656fed6e95962e2ef86d14dfe03cd50e7074b06bad9ffbbf6089adbb40f75c00744d843685c8d008adaf3aed31476780312553caf07fa86e5bc7
languageName: node
linkType: hard
"chance@npm:^1.1.13":
version: 1.1.13
resolution: "chance@npm:1.1.13"
@@ -13852,7 +13899,7 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:14.0.1, commander@npm:~14.0.0":
"commander@npm:14.0.1":
version: 14.0.1
resolution: "commander@npm:14.0.1"
checksum: 10/783115e9403caeca29c0fcbd4e0358f70c67760e4e4933f3453fcdd5ddba2ec44173c8da5213d7ce5e404f51c7e71203a42c548164dbe27b668b32a8981577f1
@@ -13929,6 +13976,13 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:~14.0.0":
version: 14.0.0
resolution: "commander@npm:14.0.0"
checksum: 10/c05418bfc35a3e8b5c67bd9f75f5b773f386f9b85f83e70e7c926047f270929cb06cf13cd68f387dd6e7e23c6157de8171b28ba606abd3e6256028f1f789becf
languageName: node
linkType: hard
"comment-parser@npm:1.4.1":
version: 1.4.1
resolution: "comment-parser@npm:1.4.1"
@@ -16176,7 +16230,16 @@ __metadata:
languageName: node
linkType: hard
"enquirer@npm:^2.3.6, enquirer@npm:^2.4.1":
"enquirer@npm:^2.3.6, enquirer@npm:~2.3.6":
version: 2.3.6
resolution: "enquirer@npm:2.3.6"
dependencies:
ansi-colors: "npm:^4.1.1"
checksum: 10/751d14f037eb7683997e696fb8d5fe2675e0b0cde91182c128cf598acf3f5bd9005f35f7c2a9109e291140af496ebec237b6dac86067d59a9b44f3688107f426
languageName: node
linkType: hard
"enquirer@npm:^2.4.1":
version: 2.4.1
resolution: "enquirer@npm:2.4.1"
dependencies:
@@ -16186,15 +16249,6 @@ __metadata:
languageName: node
linkType: hard
"enquirer@npm:~2.3.6":
version: 2.3.6
resolution: "enquirer@npm:2.3.6"
dependencies:
ansi-colors: "npm:^4.1.1"
checksum: 10/751d14f037eb7683997e696fb8d5fe2675e0b0cde91182c128cf598acf3f5bd9005f35f7c2a9109e291140af496ebec237b6dac86067d59a9b44f3688107f426
languageName: node
linkType: hard
"ensure-posix-path@npm:^1.1.0":
version: 1.1.1
resolution: "ensure-posix-path@npm:1.1.1"
@@ -18604,19 +18658,19 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:*":
version: 12.0.0
resolution: "glob@npm:12.0.0"
"glob@npm:*, glob@npm:11.0.3":
version: 11.0.3
resolution: "glob@npm:11.0.3"
dependencies:
foreground-child: "npm:^3.3.1"
jackspeak: "npm:^4.1.1"
minimatch: "npm:^10.1.1"
minimatch: "npm:^10.0.3"
minipass: "npm:^7.1.2"
package-json-from-dist: "npm:^1.0.0"
path-scurry: "npm:^2.0.0"
bin:
glob: dist/esm/bin.mjs
checksum: 10/6e21b3f1f1fa635836d45e54bbe50704884cc3e310e0cc011cfb5429db65a030e12936d99b07e66236370efe45dc8c8b26fa5334dbf555d6f8709e0315c77c30
checksum: 10/2ae536c1360c0266b523b2bfa6aadc10144a8b7e08869b088e37ac3c27cd30774f82e4bfb291cde796776e878f9e13200c7ff44010eb7054e00f46f649397893
languageName: node
linkType: hard
@@ -18651,22 +18705,6 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:11.0.3":
version: 11.0.3
resolution: "glob@npm:11.0.3"
dependencies:
foreground-child: "npm:^3.3.1"
jackspeak: "npm:^4.1.1"
minimatch: "npm:^10.0.3"
minipass: "npm:^7.1.2"
package-json-from-dist: "npm:^1.0.0"
path-scurry: "npm:^2.0.0"
bin:
glob: dist/esm/bin.mjs
checksum: 10/2ae536c1360c0266b523b2bfa6aadc10144a8b7e08869b088e37ac3c27cd30774f82e4bfb291cde796776e878f9e13200c7ff44010eb7054e00f46f649397893
languageName: node
linkType: hard
"glob@npm:11.1.0, glob@npm:^11.0.0":
version: 11.1.0
resolution: "glob@npm:11.1.0"
@@ -18926,8 +18964,8 @@ __metadata:
"@grafana/plugin-ui": "npm:^0.11.1"
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/scenes": "npm:^6.46.0"
"@grafana/scenes-react": "npm:^6.46.0"
"@grafana/scenes": "npm:6.47.1"
"@grafana/scenes-react": "npm:6.47.1"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/test-utils": "workspace:*"
@@ -20051,22 +20089,22 @@ __metadata:
linkType: hard
"i18next@npm:^23.5.1 || ^24.2.0":
version: 24.2.3
resolution: "i18next@npm:24.2.3"
version: 24.2.2
resolution: "i18next@npm:24.2.2"
dependencies:
"@babel/runtime": "npm:^7.26.10"
"@babel/runtime": "npm:^7.23.2"
peerDependencies:
typescript: ^5
peerDependenciesMeta:
typescript:
optional: true
checksum: 10/6c73d964f2a98b1aa2c2717fe6da66fc265bcbbc5fcd52b2bfff51ff013d30d4f7d7449c4eb7f464d27af43e2e73f2e7f1d46a144d731bd3bdb1385d4c199e4c
checksum: 10/f66ed9e56d9412e59502f5df39163631daf9f1264774732fb21edbd66a528ca7a6b67dc2e2aec95683c6c7956e42c651587a54bd8ee082bd12008880ce6cd326
languageName: node
linkType: hard
"i18next@npm:^25.0.0, i18next@npm:^25.5.2":
version: 25.6.2
resolution: "i18next@npm:25.6.2"
version: 25.5.2
resolution: "i18next@npm:25.5.2"
dependencies:
"@babel/runtime": "npm:^7.27.6"
peerDependencies:
@@ -20074,7 +20112,7 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 10/bb7f3e200caac72bacaead766287dbad5ed5945430bd9f2be1c14fbccd49cf78ab751f78cf53cd0be3ec063bc4c40837d7d771f7b14d50fcb9022e1ed5d6e72f
checksum: 10/8d52e82386722a228f4465aa5cf39d82bd9861ea9cc8b31d7cc4d22d5e70b2740ee006068b09f486d5273cabd227a2ac14f37d68fab26344524200083f679bcd
languageName: node
linkType: hard
@@ -20169,7 +20207,7 @@ __metadata:
languageName: node
linkType: hard
"immer@npm:10.2.0, immer@npm:^10.2.0":
"immer@npm:10.2.0, immer@npm:^10.0.3, immer@npm:^10.2.0":
version: 10.2.0
resolution: "immer@npm:10.2.0"
checksum: 10/d73e218c8f8ffbb39f9290dfafa478b94af73403dcf26b5672eef35233bb30f09ffe231f8a78a6c9cb442968510edd89e851776ec90a5ddfa82cee6db6b35137
@@ -23175,9 +23213,9 @@ __metadata:
linkType: hard
"lossless-json@npm:^4.1.1":
version: 4.3.0
resolution: "lossless-json@npm:4.3.0"
checksum: 10/a984a882c79b6e62a917d0202518472c17587bdee002f1427d7ec61115f9fbdeb5f0baa9f0e9fff8d9dbacd17da6b6c910012b2dcab69dd33d151cb7c13d5a37
version: 4.2.0
resolution: "lossless-json@npm:4.2.0"
checksum: 10/b29cbf9a90b8f3108219ff410223a90a3a6ad14cf902eed4038571d9421c69ade7077fb59737e1ca20f3f404ddd68fad69be11365b0d79b4b61421b054759551
languageName: node
linkType: hard
@@ -23652,9 +23690,9 @@ __metadata:
linkType: hard
"micro-memoize@npm:^4.1.2":
version: 4.2.0
resolution: "micro-memoize@npm:4.2.0"
checksum: 10/260e27a5c15809f7dc435a89a424d93e49ccca62b743fa96ee72f254264df58c95edacbd847a2899a6d7f8ad0daef188548a8b12add5ccff83e88869a50a762c
version: 4.1.3
resolution: "micro-memoize@npm:4.1.3"
checksum: 10/4e9c7767911cc76ae9c9779584ec87844437af9446b295a01774640a732c2c7f91944794027f44625031f7330ab7f9147740d0a9fb612680d1d2d858dad43402
languageName: node
linkType: hard
@@ -26460,6 +26498,15 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.54.1":
version: 1.54.1
resolution: "playwright-core@npm:1.54.1"
bin:
playwright-core: cli.js
checksum: 10/c0acfb7ecb48e9fb6c22f71298b966244bd4f8659093a798cc7f9a509deee22109560e44f2d507124e7718779640e0b4cb05a05c068b034405418d8740ec31ff
languageName: node
linkType: hard
"playwright-core@npm:1.56.1, playwright-core@npm:>=1.2.0":
version: 1.56.1
resolution: "playwright-core@npm:1.56.1"
@@ -26469,7 +26516,7 @@ __metadata:
languageName: node
linkType: hard
"playwright@npm:1.56.1, playwright@npm:^1.14.0":
"playwright@npm:1.56.1":
version: 1.56.1
resolution: "playwright@npm:1.56.1"
dependencies:
@@ -26484,6 +26531,21 @@ __metadata:
languageName: node
linkType: hard
"playwright@npm:^1.14.0":
version: 1.54.1
resolution: "playwright@npm:1.54.1"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.54.1"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 10/ebad2924b589a4cfedf48288e67b4afc81047222e607321c9452a37610025e6e249bdfeff010c1c70cf9a9d35c0b9e5cb7934a58986f67928dd469d9fde1a035
languageName: node
linkType: hard
"plop@npm:^4.0.1":
version: 4.0.1
resolution: "plop@npm:4.0.1"