ConditionalRendering: Fix for repeated items (#114160)

This commit is contained in:
Marc M.
2025-11-27 12:05:15 +01:00
committed by GitHub
parent 1c8f4a745f
commit 95174454e3
18 changed files with 468 additions and 53 deletions
@@ -4,6 +4,8 @@ import { test, expect, E2ESelectorGroups, DashboardPage, DashboardPageArgs } fro
import testDashboard from '../dashboards/DashboardWithAllConditionalRendering.json';
import { checkRepeatedPanelTitles } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
@@ -93,7 +95,7 @@ test.describe('Dashboard - Conditional Rendering - Load and Change', { tag: ['@d
test.afterAll(async ({ request }) => {
if (uid) {
await request.delete(`/apis/dashboard.grafana.app/v1beta1/namespaces/default/dashboards/${uid}`);
await request.delete(`/apis/dashboard.grafana.app/v1beta1/namespaces/stacks-12345/dashboards/${uid}`);
}
});
@@ -407,4 +409,52 @@ test.describe('Dashboard - Conditional Rendering - Load and Change', { tag: ['@d
await expect(getTabShowNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideNotMatches(dashboardPage, selectors)).not.toBeVisible();
});
test.describe('Variable repeat', () => {
const repeatOptions = ['a', 'b', 'c'];
async function failTestDataRequestForOption(page: Page, option: string) {
await page.route(/\/api\/ds\/query\?.*\bds_type=grafana-testdata-datasource/, async (route) => {
const rawPostData = route.request().postData();
if (!rawPostData) {
return;
}
// the first panel query has a label set to the current variable value
if (JSON.parse(rawPostData).queries[0].labels === `key=${option}`) {
await route.fulfill({ status: 500, body: '{}' });
} else {
await route.continue();
}
});
}
test('Hide when equals, hide when no data', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await loadDashboard(page, gotoDashboardPage);
await getTab(dashboardPage, selectors, 'repeated items').click();
const optionForHiddenPanels = repeatOptions[0];
await failTestDataRequestForOption(page, optionForHiddenPanels);
await checkRepeatedPanelTitles(
dashboardPage,
selectors,
'Hide panel - ',
[
`custom variable equals ${optionForHiddenPanels} (current = ${optionForHiddenPanels})`,
`no data (current = ${optionForHiddenPanels})`,
],
true
);
const optionsForVisiblePanels = repeatOptions.slice(1);
await checkRepeatedPanelTitles(dashboardPage, selectors, 'Hide panel - ', [
...optionsForVisiblePanels.map((o) => `custom variable equals ${optionForHiddenPanels} (current = ${o})`),
...optionsForVisiblePanels.map((o) => `no data (current = ${o})`),
]);
});
});
});
+10 -4
View File
@@ -97,12 +97,18 @@ export async function checkRepeatedPanelTitles(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
title: string,
options: Array<string | number>
options: Array<string | number>,
expectHidden = false
) {
for (const option of options) {
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(`${title}${option}`))
).toBeVisible();
const titleLocator = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Panel.title(`${title}${option}`)
);
if (expectHidden) {
await expect(titleLocator).toBeHidden();
} else {
await expect(titleLocator).toBeVisible();
}
}
}
@@ -3308,6 +3308,170 @@
}
}
},
"panel-37": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"hidden": false,
"query": {
"group": "",
"kind": "DataQuery",
"spec": {},
"version": "v0"
},
"refId": "A"
}
}
],
"queryOptions": {},
"transformations": []
}
},
"description": "",
"id": 37,
"links": [],
"title": "Hide panel - custom variable equals a (current = ${myCustomVariable})",
"vizConfig": {
"group": "text",
"kind": "VizConfig",
"spec": {
"fieldConfig": {
"defaults": {},
"overrides": []
},
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "",
"mode": "markdown"
}
},
"version": "12.2.0-pre"
}
}
},
"panel-38": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"hidden": false,
"query": {
"datasource": {
"name": "PD8C576611E62080A"
},
"group": "grafana-testdata-datasource",
"kind": "DataQuery",
"spec": {
"labels": "key=$myCustomVariable",
"scenarioId": "random_walk",
"seriesCount": 1
},
"version": "v0"
},
"refId": "A"
}
}
],
"queryOptions": {},
"transformations": []
}
},
"description": "",
"id": 38,
"links": [],
"title": "Hide panel - no data (current = ${myCustomVariable})",
"vizConfig": {
"group": "timeseries",
"kind": "VizConfig",
"spec": {
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
}
},
"version": "12.2.0-pre"
}
}
},
"panel-4": {
"kind": "Panel",
"spec": {
@@ -5091,6 +5255,80 @@
},
"title": "Tab - hide - time range <7d"
}
},
{
"kind": "TabsLayoutTab",
"spec": {
"layout": {
"kind": "AutoGridLayout",
"spec": {
"columnWidthMode": "standard",
"items": [
{
"kind": "AutoGridLayoutItem",
"spec": {
"conditionalRendering": {
"kind": "ConditionalRenderingGroup",
"spec": {
"condition": "and",
"items": [
{
"kind": "ConditionalRenderingVariable",
"spec": {
"operator": "equals",
"value": "a",
"variable": "myCustomVariable"
}
}
],
"visibility": "hide"
}
},
"element": {
"kind": "ElementReference",
"name": "panel-37"
},
"repeat": {
"mode": "variable",
"value": "myCustomVariable"
}
}
},
{
"kind": "AutoGridLayoutItem",
"spec": {
"conditionalRendering": {
"kind": "ConditionalRenderingGroup",
"spec": {
"condition": "and",
"items": [
{
"kind": "ConditionalRenderingData",
"spec": {
"value": false
}
}
],
"visibility": "hide"
}
},
"element": {
"kind": "ElementReference",
"name": "panel-38"
},
"repeat": {
"mode": "variable",
"value": "myCustomVariable"
}
}
}
],
"maxColumnCount": 3,
"rowHeightMode": "standard"
}
},
"title": "Tab - repeated items"
}
}
]
}
@@ -5122,6 +5360,39 @@
"query": "",
"skipUrlSync": false
}
},
{
"kind": "CustomVariable",
"spec": {
"allowCustomValue": false,
"current": {
"text": "All",
"value": "$__all"
},
"hide": "dontHide",
"includeAll": true,
"multi": false,
"name": "myCustomVariable",
"options": [
{
"selected": false,
"text": "a",
"value": "a"
},
{
"selected": false,
"text": "b",
"value": "b"
},
{
"selected": false,
"text": "c",
"value": "c"
}
],
"query": "a, b, c",
"skipUrlSync": false
}
}
]
},
@@ -68,22 +68,29 @@ export class ConditionalRenderingData extends SceneObjectBase<ConditionalRenderi
};
}
private _getObjectDataProvider(): SceneDataProvider | undefined {
private _getPanelFromObject(): VizPanel | undefined {
const object = getObject(this);
if (!object) {
return undefined;
}
let panel: VizPanel | undefined;
if (object instanceof VizPanel) {
return object;
}
for (const val of Object.values(object.state)) {
if (val instanceof VizPanel) {
panel = val;
break;
return val;
}
}
return undefined;
}
private _getObjectDataProvider(): SceneDataProvider | undefined {
const panel = this._getPanelFromObject();
if (!panel) {
return undefined;
}
@@ -133,6 +140,10 @@ export class ConditionalRenderingData extends SceneObjectBase<ConditionalRenderi
}
}
public forceCheck() {
this._check();
}
public renderCmp(): ReactElement {
return <this.Component model={this} key={this.state.key} />;
}
@@ -80,6 +80,10 @@ export class ConditionalRenderingTimeRangeSize extends SceneObjectBase<Condition
}
}
public forceCheck() {
this._check();
}
public renderCmp(): ReactElement {
return <this.Component model={this} key={this.state.key} />;
}
@@ -20,7 +20,7 @@ import { getLowerTranslatedObjectType } from '../object';
import { ConditionalRenderingConditionWrapper } from './ConditionalRenderingConditionWrapper';
import { ConditionalRenderingConditionsSerializerRegistryItem } from './serializers';
import { checkGroup, getObjectType } from './utils';
import { checkGroup, getObject, getObjectType } from './utils';
type VariableConditionValueOperator = '=' | '!=' | '=~' | '!~';
@@ -40,14 +40,6 @@ export class ConditionalRenderingVariable extends SceneObjectBase<ConditionalRen
deserialize: this.deserialize,
};
protected _variableDependency = new VariableDependencyConfig(this, {
onAnyVariableChanged: (v) => {
if (v.state.name === this.state.variable) {
this._check();
}
},
});
public constructor(state: ConditionalRenderingVariableState) {
super(state);
@@ -55,6 +47,20 @@ export class ConditionalRenderingVariable extends SceneObjectBase<ConditionalRen
}
private _activationHandler() {
const object = getObject(this);
if (!object) {
return;
}
this._variableDependency = new VariableDependencyConfig(object, {
onAnyVariableChanged: (v) => {
if (v.state.name === this.state.variable) {
this._check();
}
},
});
this.forEachChild((child) => {
if (!child.isActive) {
this._subs.add(child.activate());
@@ -78,7 +84,13 @@ export class ConditionalRenderingVariable extends SceneObjectBase<ConditionalRen
return undefined;
}
const variable = sceneGraph.getVariables(this).getByName(this.state.variable);
const object = getObject(this);
if (!object) {
return undefined;
}
const variable = sceneGraph.getVariables(object).getByName(this.state.variable);
if (!variable) {
return undefined;
@@ -127,6 +139,10 @@ export class ConditionalRenderingVariable extends SceneObjectBase<ConditionalRen
}
}
public forceCheck() {
this._check();
}
public renderCmp(): ReactElement {
return <this.Component model={this} key={this.state.key} />;
}
@@ -14,6 +14,13 @@ export function getGroup(condition: ConditionalRenderingConditions): Conditional
}
export function getObject(condition: ConditionalRenderingConditions): SceneObject | undefined {
const group = getGroup(condition);
const groupTarget = group.getTarget();
if (groupTarget) {
return groupTarget;
}
return getGroup(condition).parent;
}
@@ -2,7 +2,14 @@ import { lowerCase } from 'lodash';
import { useMemo } from 'react';
import { t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import {
SceneComponentProps,
sceneGraph,
SceneObject,
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
} from '@grafana/scenes';
import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Stack } from '@grafana/ui';
@@ -33,6 +40,7 @@ export class ConditionalRenderingGroup extends SceneObjectBase<ConditionalRender
private _shouldShow: boolean;
private _shouldMatchAll: boolean;
private _target?: SceneObjectRef<SceneObject>;
public constructor(state: ConditionalRenderingGroupState) {
super(state);
@@ -52,6 +60,19 @@ export class ConditionalRenderingGroup extends SceneObjectBase<ConditionalRender
this.check();
}
public setTarget(target: SceneObject | undefined) {
this._target = target ? target.getRef() : undefined;
this.forceCheck();
}
public getTarget(): SceneObject | undefined {
return this._target?.resolve();
}
public forceCheck() {
this.state.conditions.forEach((condition) => condition.forceCheck());
}
public check() {
// Filter out undefined results
// Because we negate the result if shouldShow is false, we can use `condition.state.result ?? true` directly below
@@ -1,12 +1,13 @@
import { ReactNode } from 'react';
import { SceneObject, useSceneObjectState } from '@grafana/scenes';
import { useSceneObjectState } from '@grafana/scenes';
import { ConditionalRenderingGroup } from '../group/ConditionalRenderingGroup';
import { ConditionalRenderingOverlay } from './ConditionalRenderingOverlay';
let placeholderConditionalRendering: ConditionalRenderingGroup | undefined;
function getPlaceholderConditionalRendering(): ConditionalRenderingGroup {
if (!placeholderConditionalRendering) {
placeholderConditionalRendering = ConditionalRenderingGroup.createEmpty();
@@ -14,13 +15,10 @@ function getPlaceholderConditionalRendering(): ConditionalRenderingGroup {
return placeholderConditionalRendering;
}
export function useIsConditionallyHidden(scene: SceneObject): [boolean, string | undefined, ReactNode | null, boolean] {
const conditionalRenderingToRender =
'conditionalRendering' in scene.state && scene.state.conditionalRendering instanceof ConditionalRenderingGroup
? scene.state.conditionalRendering
: getPlaceholderConditionalRendering();
const { result, renderHidden } = useSceneObjectState(conditionalRenderingToRender, {
export function useIsConditionallyHidden(
conditionalRendering: ConditionalRenderingGroup = getPlaceholderConditionalRendering()
): [boolean, string | undefined, ReactNode | null, boolean] {
const { result, renderHidden } = useSceneObjectState(conditionalRendering, {
shouldActivateOrKeepAlive: true,
});
@@ -1,7 +1,7 @@
import { capitalize, lowerCase } from 'lodash';
import { t } from '@grafana/i18n';
import { SceneObject } from '@grafana/scenes';
import { SceneObject, VizPanel } from '@grafana/scenes';
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
import { RowItem } from '../scene/layout-rows/RowItem';
@@ -50,7 +50,7 @@ export function getLowerTranslatedObjectType(type: ObjectsWithConditionalRenderi
export function extractObjectType(object: SceneObject | undefined): ObjectsWithConditionalRendering {
if (!object) {
return 'element';
} else if (object instanceof AutoGridItem) {
} else if (object instanceof AutoGridItem || object instanceof VizPanel) {
return 'panel';
} else if (object instanceof RowItem) {
return 'row';
@@ -31,6 +31,7 @@ export interface AutoGridItemState extends SceneObjectState {
variableName?: string;
isHidden?: boolean;
conditionalRendering?: ConditionalRenderingGroup;
repeatedConditionalRendering?: ConditionalRenderingGroup[];
}
export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements DashboardLayoutItem {
@@ -130,7 +131,21 @@ export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements
}
}
this.setState({ repeatedPanels });
let repeatedConditionalRendering: ConditionalRenderingGroup[] | undefined;
if (this.state.conditionalRendering) {
repeatedConditionalRendering = repeatedPanels.reduce<ConditionalRenderingGroup[]>((acc, panel) => {
const conditionalRendering = this.state.conditionalRendering!.clone();
conditionalRendering.setTarget(panel);
acc.push(conditionalRendering);
return acc;
}, []);
this.state.conditionalRendering.setTarget(panelToRepeat);
}
this.setState({ repeatedPanels, repeatedConditionalRendering });
this._prevRepeatValues = values;
}
@@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data/';
import { LazyLoader, SceneComponentProps, VizPanel } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import { ConditionalRenderingGroup } from '../../conditional-rendering/group/ConditionalRenderingGroup';
import { useIsConditionallyHidden } from '../../conditional-rendering/hooks/useIsConditionallyHidden';
import { useDashboardState } from '../../utils/utils';
import { renderMatchingSoloPanels, useSoloPanelContext } from '../SoloPanelContext';
@@ -17,8 +18,6 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
const { body, repeatedPanels = [], key } = model.useState();
const { draggingKey } = model.getParentGrid().useState();
const { isEditing, preload } = useDashboardState(model);
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay, renderHidden] =
useIsConditionallyHidden(model);
const styles = useStyles2(getStyles);
const soloPanelContext = useSoloPanelContext();
const isLazy = useMemo(() => getIsLazy(preload), [preload]);
@@ -29,18 +28,23 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
memo(
({
item,
conditionalRendering,
addDndContainer,
isDragged,
isDragging,
isRepeat = false,
}: {
item: VizPanel;
conditionalRendering?: ConditionalRenderingGroup;
addDndContainer: boolean;
isDragged: boolean;
isDragging: boolean;
isRepeat?: boolean;
}) =>
isConditionallyHidden && !isEditing && !renderHidden ? null : (
}) => {
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay, renderHidden] =
useIsConditionallyHidden(conditionalRendering);
return isConditionallyHidden && !isEditing && !renderHidden ? null : (
<div
{...(addDndContainer
? { ref: model.containerRef, ['data-auto-grid-item-drop-target']: isDragging ? key : undefined }
@@ -78,19 +82,10 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
)
}
</div>
)
);
}
),
[
conditionalRenderingClass,
conditionalRenderingOverlay,
isLazy,
key,
model.containerRef,
styles,
isConditionallyHidden,
isEditing,
renderHidden,
]
[model, isLazy, key, styles, isEditing]
);
if (soloPanelContext) {
@@ -102,10 +97,18 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
return (
<>
<Wrapper item={body} addDndContainer={true} key={body.state.key!} isDragged={isDragged} isDragging={isDragging} />
{repeatedPanels.map((item) => (
<Wrapper
item={body}
conditionalRendering={model.state.conditionalRendering}
addDndContainer={true}
key={body.state.key!}
isDragged={isDragged}
isDragging={isDragging}
/>
{repeatedPanels.map((item, idx) => (
<Wrapper
item={item}
conditionalRendering={model.state.repeatedConditionalRendering?.[idx]}
addDndContainer={false}
key={item.state.key!}
isDragged={isDragged}
@@ -20,8 +20,9 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
const { layout, collapse: isCollapsed, fillScreen, hideHeader: isHeaderHidden, isDropTarget, key } = model.useState();
const isClone = isRepeatCloneOrChildOf(model);
const { isEditing } = useDashboardState(model);
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] =
useIsConditionallyHidden(model);
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden(
model.state.conditionalRendering
);
const { isSelected, onSelect, isSelectable } = useElementSelection(key);
const title = useInterpolatedTitle(model);
const { rows } = model.getParentLayout().useState();
@@ -112,7 +112,10 @@ export function performRowRepeats(variable: MultiValueVariable, row: RowItem, co
});
if (!isSourceRow) {
rowClone.state.conditionalRendering?.setTarget(rowClone);
clonedRows.push(rowClone);
} else {
row.state.conditionalRendering?.setTarget(row);
}
}
@@ -292,6 +292,8 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
const conditionalRendering = tab.state.conditionalRendering;
conditionalRendering?.clearParent();
// We need to clear the target since we don't want to point the original tab anymore (if it was set)
conditionalRendering?.setTarget(undefined);
rows.push(
new RowItem({
@@ -29,7 +29,7 @@ export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
const href = textUtil.sanitize(locationUtil.getUrlForPartial(location, { [urlKey]: mySlug }));
const styles = useStyles2(getStyles);
const pointerDistance = usePointerDistance();
const [isConditionallyHidden] = useIsConditionallyHidden(model);
const [isConditionallyHidden] = useIsConditionallyHidden(model.state.conditionalRendering);
const isClone = isRepeatCloneOrChildOf(model);
const soloPanelContext = useSoloPanelContext();
@@ -116,7 +116,9 @@ interface TabItemLayoutRendererProps {
export function TabItemLayoutRenderer({ tab, isEditing }: TabItemLayoutRendererProps) {
const { layout, key } = tab.useState();
const styles = useStyles2(getStyles);
const [_, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden(tab);
const [_, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden(
tab.state.conditionalRendering
);
return (
<TabContent
@@ -167,7 +167,10 @@ export function createTabRepeats({
});
if (!isSourceTab) {
tabClone.state.conditionalRendering?.setTarget(tabClone);
repeats.push(tabClone);
} else {
tab.state.conditionalRendering?.setTarget(tab);
}
}
return repeats;
@@ -417,6 +417,8 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
const conditionalRendering = row.state.conditionalRendering;
conditionalRendering?.clearParent();
// We need to clear the target since we don't want to point the original row anymore (if it was set)
conditionalRendering?.setTarget(undefined);
tabs.push(
new TabItem({