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:
co-authored by
Dominik Prokop
parent
5e949fc955
commit
336aa06966
+2
-2
@@ -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[];
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+1
@@ -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
|
||||
|
||||
|
+283
-270
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],
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user