Dashboards: Refactor conditional rendering (#108596)

This commit is contained in:
Bogdan Matei
2025-09-04 16:22:26 +00:00
committed by GitHub
parent 456bd0e81a
commit eed8d189ac
42 changed files with 7237 additions and 1167 deletions
+2
View File
@@ -414,6 +414,8 @@
/e2e-playwright/dashboard-cujs/ @grafana/dashboards-squad
/e2e-playwright/dashboards-search-suite/ @grafana/dashboards-squad
/e2e-playwright/dashboards/cujs/ @grafana/dashboards-squad
/e2e-playwright/dashboards/DashboardForConditionalRendering.json @grafana/dashboards-squad
/e2e-playwright/dashboards/DashboardWithAllConditionalRendering.json @grafana/dashboards-squad
/e2e-playwright/dashboards/AdHocFilterTest.json @grafana/datapro
/e2e-playwright/dashboards/DashboardLiveTest.json @grafana/dashboards-squad
/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json @grafana/dashboards-squad
@@ -0,0 +1,409 @@
import { Page } from '@playwright/test';
import { test, expect, E2ESelectorGroups, DashboardPage, DashboardPageArgs } from '@grafana/plugin-e2e';
import testDashboard from '../dashboards/DashboardWithAllConditionalRendering.json';
test.use({
featureToggles: {
kubernetesDashboards: true,
dashboardNewLayouts: true,
groupByVariable: true,
},
viewport: { width: 1920, height: 1080 },
});
test.describe('Dashboard - Conditional Rendering - Load and Change', { tag: ['@dashboards'] }, () => {
let uid: string;
const loadDashboard = async (
page: Page,
gotoDashboardPage: (args: DashboardPageArgs) => Promise<DashboardPage>,
options?: { from?: string; to?: string; myVariable?: string }
) => {
const params: DashboardPageArgs = { uid };
if (options?.from && options?.to) {
params.timeRange = {
from: options.from,
to: options.to,
};
}
if (options?.myVariable) {
params.queryParams = new URLSearchParams();
params.queryParams.set('var-myVariable', options.myVariable);
}
const dashboardPage = await gotoDashboardPage(params);
await expect(page.getByText(testDashboard.spec.title)).toBeVisible();
await page.waitForLoadState('networkidle');
return dashboardPage;
};
const fillVariable = async (page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups, text: string) => {
const variable = dashboardPage
.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(testDashboard.spec.variables[0].spec.name)
)
.locator('..')
.locator('input');
await variable.click();
await variable.clear();
await variable.fill(text);
await variable.press('Enter');
await page.waitForLoadState('networkidle');
};
const setTimeRange = async (
page: Page,
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
from: string,
to: string
) => {
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click();
const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField);
await fromField.click();
await fromField.fill(from);
const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField);
await toField.click();
await toField.fill(to);
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.applyTimeRange).click();
await page.waitForLoadState('networkidle');
};
test.beforeAll(async ({ request }) => {
const response = await request.post('/apis/dashboard.grafana.app/v2beta1/namespaces/stacks-12345/dashboards', {
data: {
metadata: {
annotations: {
'grafana.app/folder': '',
'grafana.app/grant-permissions': 'default',
},
generateName: 'ad',
},
spec: testDashboard.spec,
},
});
const responseBody = await response.json();
uid = responseBody.metadata.name;
});
test.afterAll(async ({ request }) => {
if (uid) {
await request.delete(`/apis/dashboard.grafana.app/v1beta1/namespaces/default/dashboards/${uid}`);
}
});
const getPanel = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, title: string) =>
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(`Panel - ${title}`));
const getPanelShowData = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'show - data');
const getPanelHideData = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'hide - data');
const getPanelShowNoData = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'show - no data');
const getPanelHideNoData = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'hide - no data');
const getPanelShowTimeRange = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'show - time range <7d');
const getPanelHideTimeRange = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'hide - time range <7d');
const getPanelShowEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'show - variable - equals 1,2,3');
const getPanelHideEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'hide - variable - equals 1,2,3');
const getPanelShowNotEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'show - variable - not equals 1,2,3');
const getPanelHideNotEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'hide - variable - not equals 1,2,3');
const getPanelShowMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'show - variable - matches .*2.*');
const getPanelHideMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'hide - variable - matches .*2.*');
const getPanelShowNotMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'show - variable - not matches .*2.*');
const getPanelHideNotMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getPanel(dashboardPage, selectors, 'hide - variable - not matches .*2.*');
const getRow = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, title: string) =>
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`Row - ${title}`));
const getRowShowTimeRange = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getRow(dashboardPage, selectors, 'show - time range <7d');
const getRowHideTimeRange = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getRow(dashboardPage, selectors, 'hide - time range <7d');
const getRowShowEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getRow(dashboardPage, selectors, 'show - variable - equals 1,2,3');
const getRowHideEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getRow(dashboardPage, selectors, 'hide - variable - equals 1,2,3');
const getRowShowNotEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getRow(dashboardPage, selectors, 'show - variable - not equals 1,2,3');
const getRowHideNotEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getRow(dashboardPage, selectors, 'hide - variable - not equals 1,2,3');
const getRowShowMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getRow(dashboardPage, selectors, 'show - variable - matches .*2.*');
const getRowHideMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getRow(dashboardPage, selectors, 'hide - variable - matches .*2.*');
const getRowShowNotMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getRow(dashboardPage, selectors, 'show - variable - not matches .*2.*');
const getRowHideNotMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getRow(dashboardPage, selectors, 'hide - variable - not matches .*2.*');
const getTab = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, title: string) =>
dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`Tab - ${title}`));
const getTabShowTimeRange = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getTab(dashboardPage, selectors, 'show - time range <7d');
const getTabHideTimeRange = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getTab(dashboardPage, selectors, 'hide - time range <7d');
const getTabShowEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getTab(dashboardPage, selectors, 'show - variable - equals 1,2,3');
const getTabHideEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getTab(dashboardPage, selectors, 'hide - variable - equals 1,2,3');
const getTabShowNotEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getTab(dashboardPage, selectors, 'show - variable - not equals 1,2,3');
const getTabHideNotEquals = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getTab(dashboardPage, selectors, 'hide - variable - not equals 1,2,3');
const getTabShowMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getTab(dashboardPage, selectors, 'show - variable - matches .*2.*');
const getTabHideMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getTab(dashboardPage, selectors, 'hide - variable - matches .*2.*');
const getTabShowNotMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getTab(dashboardPage, selectors, 'show - variable - not matches .*2.*');
const getTabHideNotMatches = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) =>
getTab(dashboardPage, selectors, 'hide - variable - not matches .*2.*');
test('Load without data', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await loadDashboard(page, gotoDashboardPage);
await expect(getPanelShowData(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideData(dashboardPage, selectors)).toBeVisible();
await expect(getPanelShowNoData(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideNoData(dashboardPage, selectors)).not.toBeVisible();
await fillVariable(page, dashboardPage, selectors, '1,2,3,4');
await expect(getPanelShowData(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideData(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelShowNoData(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideNoData(dashboardPage, selectors)).toBeVisible();
});
test('Load with data', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await loadDashboard(page, gotoDashboardPage, { myVariable: '1,2,3,4' });
await expect(getPanelShowData(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideData(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelShowNoData(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideNoData(dashboardPage, selectors)).toBeVisible();
await fillVariable(page, dashboardPage, selectors, '');
await expect(getPanelShowData(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideData(dashboardPage, selectors)).toBeVisible();
await expect(getPanelShowNoData(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideNoData(dashboardPage, selectors)).not.toBeVisible();
});
test('Load without time range', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await loadDashboard(page, gotoDashboardPage);
await expect(getPanelShowTimeRange(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideTimeRange(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowShowTimeRange(dashboardPage, selectors)).toBeVisible();
await expect(getRowHideTimeRange(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabShowTimeRange(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideTimeRange(dashboardPage, selectors)).not.toBeVisible();
await setTimeRange(page, dashboardPage, selectors, 'now-8d', 'now');
await expect(getPanelShowTimeRange(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideTimeRange(dashboardPage, selectors)).toBeVisible();
await expect(getRowShowTimeRange(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowHideTimeRange(dashboardPage, selectors)).toBeVisible();
await expect(getTabShowTimeRange(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabHideTimeRange(dashboardPage, selectors)).toBeVisible();
});
test('Load with time range', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await loadDashboard(page, gotoDashboardPage, { from: 'now-8d', to: 'now' });
await expect(getPanelShowTimeRange(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideTimeRange(dashboardPage, selectors)).toBeVisible();
await expect(getRowShowTimeRange(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowHideTimeRange(dashboardPage, selectors)).toBeVisible();
await expect(getTabShowTimeRange(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabHideTimeRange(dashboardPage, selectors)).toBeVisible();
await setTimeRange(page, dashboardPage, selectors, 'now-6h', 'now');
await expect(getPanelShowTimeRange(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideTimeRange(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowShowTimeRange(dashboardPage, selectors)).toBeVisible();
await expect(getRowHideTimeRange(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabShowTimeRange(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideTimeRange(dashboardPage, selectors)).not.toBeVisible();
});
test('Load without variable equals/not equals', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await loadDashboard(page, gotoDashboardPage);
await expect(getPanelShowEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideEquals(dashboardPage, selectors)).toBeVisible();
await expect(getPanelShowNotEquals(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideNotEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowShowEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowHideEquals(dashboardPage, selectors)).toBeVisible();
await expect(getRowShowNotEquals(dashboardPage, selectors)).toBeVisible();
await expect(getRowHideNotEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabShowEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabHideEquals(dashboardPage, selectors)).toBeVisible();
await expect(getTabShowNotEquals(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideNotEquals(dashboardPage, selectors)).not.toBeVisible();
await fillVariable(page, dashboardPage, selectors, '1,2,3');
await expect(getPanelShowEquals(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelShowNotEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideNotEquals(dashboardPage, selectors)).toBeVisible();
await expect(getRowShowEquals(dashboardPage, selectors)).toBeVisible();
await expect(getRowHideEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowShowNotEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowHideNotEquals(dashboardPage, selectors)).toBeVisible();
await expect(getTabShowEquals(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabShowNotEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabHideNotEquals(dashboardPage, selectors)).toBeVisible();
});
test('Load with variable equals/not equals', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await loadDashboard(page, gotoDashboardPage, { myVariable: '1,2,3' });
await expect(getPanelShowEquals(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelShowNotEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideNotEquals(dashboardPage, selectors)).toBeVisible();
await expect(getRowShowEquals(dashboardPage, selectors)).toBeVisible();
await expect(getRowHideEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowShowNotEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowHideNotEquals(dashboardPage, selectors)).toBeVisible();
await expect(getTabShowEquals(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabShowNotEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabHideNotEquals(dashboardPage, selectors)).toBeVisible();
await fillVariable(page, dashboardPage, selectors, '');
await expect(getPanelShowEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideEquals(dashboardPage, selectors)).toBeVisible();
await expect(getPanelShowNotEquals(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideNotEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowShowEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowHideEquals(dashboardPage, selectors)).toBeVisible();
await expect(getRowShowNotEquals(dashboardPage, selectors)).toBeVisible();
await expect(getRowHideNotEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabShowEquals(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabHideEquals(dashboardPage, selectors)).toBeVisible();
await expect(getTabShowNotEquals(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideNotEquals(dashboardPage, selectors)).not.toBeVisible();
});
test('Load without variable matches/not matches', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await loadDashboard(page, gotoDashboardPage);
await expect(getPanelShowMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideMatches(dashboardPage, selectors)).toBeVisible();
await expect(getPanelShowNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideNotMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowShowMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowHideMatches(dashboardPage, selectors)).toBeVisible();
await expect(getRowShowNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getRowHideNotMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabShowMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabHideMatches(dashboardPage, selectors)).toBeVisible();
await expect(getTabShowNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideNotMatches(dashboardPage, selectors)).not.toBeVisible();
await fillVariable(page, dashboardPage, selectors, '1,2,3');
await expect(getPanelShowMatches(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelShowNotMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getRowShowMatches(dashboardPage, selectors)).toBeVisible();
await expect(getRowHideMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowShowNotMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowHideNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getTabShowMatches(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabShowNotMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabHideNotMatches(dashboardPage, selectors)).toBeVisible();
});
test('Load with variable matches/not matches', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await loadDashboard(page, gotoDashboardPage, { myVariable: '1,2,3' });
await expect(getPanelShowMatches(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelShowNotMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getRowShowMatches(dashboardPage, selectors)).toBeVisible();
await expect(getRowHideMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowShowNotMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowHideNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getTabShowMatches(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabShowNotMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabHideNotMatches(dashboardPage, selectors)).toBeVisible();
await fillVariable(page, dashboardPage, selectors, '');
await expect(getPanelShowMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getPanelHideMatches(dashboardPage, selectors)).toBeVisible();
await expect(getPanelShowNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getPanelHideNotMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowShowMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getRowHideMatches(dashboardPage, selectors)).toBeVisible();
await expect(getRowShowNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getRowHideNotMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabShowMatches(dashboardPage, selectors)).not.toBeVisible();
await expect(getTabHideMatches(dashboardPage, selectors)).toBeVisible();
await expect(getTabShowNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideNotMatches(dashboardPage, selectors)).not.toBeVisible();
});
});
@@ -0,0 +1,491 @@
{
"apiVersion": "dashboard.grafana.app/v2beta1",
"kind": "Dashboard",
"metadata": {
"name": "adtbh3z",
"namespace": "default",
"uid": "l3FuRuXP1JYJT4L98cvFa41mh7nr4qjyQiO3wXAY5N8X",
"resourceVersion": "11",
"generation": 11,
"creationTimestamp": "2025-08-25T11:33:54Z",
"labels": {
"grafana.app/deprecatedInternalID": "148"
},
"annotations": {
"grafana.app/createdBy": "user:u000000001",
"grafana.app/updatedBy": "user:u000000001",
"grafana.app/updatedTimestamp": "2025-08-25T13:23:31Z",
"grafana.app/folder": ""
}
},
"spec": {
"annotations": [],
"cursorSync": "Off",
"description": "",
"editable": true,
"elements": {
"panel-1": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"hidden": false,
"query": {
"datasource": {
"name": "gdev-testdata"
},
"group": "grafana-testdata-datasource",
"kind": "DataQuery",
"spec": {
"errorType": "server_panic",
"scenarioId": "server_error_500",
"stringInput": "$myVariable"
},
"version": "v0"
},
"refId": "A"
}
}
],
"queryOptions": {},
"transformations": []
}
},
"description": "",
"id": 1,
"links": [],
"title": "Panel 1",
"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",
"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-2": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"hidden": false,
"query": {
"datasource": {
"name": "gdev-testdata"
},
"group": "grafana-testdata-datasource",
"kind": "DataQuery",
"spec": {
"errorType": "server_panic",
"scenarioId": "server_error_500",
"stringInput": "$myVariable"
},
"version": "v0"
},
"refId": "A"
}
}
],
"queryOptions": {},
"transformations": []
}
},
"description": "",
"id": 2,
"links": [],
"title": "Panel 2",
"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",
"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-3": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"hidden": false,
"query": {
"datasource": {
"name": "gdev-testdata"
},
"group": "grafana-testdata-datasource",
"kind": "DataQuery",
"spec": {
"errorType": "server_panic",
"scenarioId": "server_error_500",
"stringInput": "$myVariable"
},
"version": "v0"
},
"refId": "A"
}
}
],
"queryOptions": {},
"transformations": []
}
},
"description": "",
"id": 3,
"links": [],
"title": "Panel 3",
"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",
"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"
}
}
}
},
"layout": {
"kind": "TabsLayout",
"spec": {
"tabs": [
{
"kind": "TabsLayoutTab",
"spec": {
"layout": {
"kind": "RowsLayout",
"spec": {
"rows": [
{
"kind": "RowsLayoutRow",
"spec": {
"collapse": false,
"layout": {
"kind": "AutoGridLayout",
"spec": {
"columnWidthMode": "standard",
"items": [
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-1"
}
}
}
],
"maxColumnCount": 3,
"rowHeightMode": "standard"
}
},
"title": "Row 1"
}
},
{
"kind": "RowsLayoutRow",
"spec": {
"layout": {
"kind": "AutoGridLayout",
"spec": {
"columnWidthMode": "standard",
"items": [
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-2"
}
}
}
],
"maxColumnCount": 3,
"rowHeightMode": "standard"
}
},
"title": "Row 2"
}
}
]
}
},
"title": "Tab 1"
}
},
{
"kind": "TabsLayoutTab",
"spec": {
"layout": {
"kind": "AutoGridLayout",
"spec": {
"columnWidthMode": "standard",
"items": [
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-3"
}
}
}
],
"maxColumnCount": 3,
"rowHeightMode": "standard"
}
},
"title": "Tab 2"
}
}
]
}
},
"links": [],
"liveNow": false,
"preload": true,
"tags": [],
"timeSettings": {
"autoRefresh": "",
"autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"fiscalYearStartMonth": 0,
"from": "now-6h",
"hideTimepicker": false,
"timezone": "browser",
"to": "now"
},
"title": "New dashboard for conditional rendering",
"variables": [
{
"kind": "TextVariable",
"spec": {
"current": {
"text": "",
"value": ""
},
"hide": "dontHide",
"name": "myVariable",
"query": "",
"skipUrlSync": false
}
}
]
},
"status": {}
}
File diff suppressed because it is too large Load Diff
@@ -1,77 +0,0 @@
import { t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { ConditionalRenderingChangedEvent } from '../edit-pane/shared';
import { ConditionalRenderingBase } from './ConditionalRenderingBase';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
import { ItemsWithConditionalRendering } from './types';
import { getItemType, translatedItemType } from './utils';
export interface ConditionalRenderingState extends SceneObjectState {
rootGroup: ConditionalRenderingGroup;
}
export class ConditionalRendering extends SceneObjectBase<ConditionalRenderingState> {
public static Component = ConditionalRenderingRenderer;
public get info(): string {
return t(
'dashboard.conditional-rendering.root.info',
'Set rules to control {{type}} visibility by matching any or all rules.',
{ type: translatedItemType(this.getItemType()) }
);
}
public constructor(state: ConditionalRenderingState) {
super(state);
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
// This ensures that all children are activated when conditional rendering is activated
// We need this to allow children to subscribe to variable changes etc.
this.forEachChild((child) => {
if (!child.isActive) {
this._subs.add(child.activate());
}
});
}
public evaluate(): boolean {
return this.state.rootGroup.evaluate();
}
public notifyChange() {
this.parent?.forceRender();
this.parent?.publishEvent(new ConditionalRenderingChangedEvent(this), true);
}
public deleteItem<T extends ConditionalRenderingBase>(item: T) {
sceneGraph.getAncestor(item, ConditionalRenderingGroup).removeItem(item.state.key!);
}
public serialize(): ConditionalRenderingGroupKind {
return this.state.rootGroup.serialize();
}
public getItem(): SceneObject {
return this.parent!;
}
public getItemType(): ItemsWithConditionalRendering {
return getItemType(this.getItem());
}
public static createEmpty(): ConditionalRendering {
return new ConditionalRendering({ rootGroup: ConditionalRenderingGroup.createEmpty() });
}
}
function ConditionalRenderingRenderer({ model }: SceneComponentProps<ConditionalRendering>) {
const { rootGroup } = model.useState();
return rootGroup.render(false);
}
@@ -1,160 +0,0 @@
import { ReactElement } from 'react';
import { Trans, t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { Alert, Icon, IconButton, Stack, Text, Tooltip } from '@grafana/ui';
import { dashboardEditActions } from '../edit-pane/shared';
import { ConditionalRendering } from './ConditionalRendering';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
import {
ConditionalRenderingConditions,
ConditionalRenderingKindTypes,
ConditionValues,
ItemsWithConditionalRendering,
} from './types';
export interface ConditionalRenderingBaseState<V = ConditionValues> extends SceneObjectState {
value: V;
}
export abstract class ConditionalRenderingBase<
S extends ConditionalRenderingBaseState = ConditionalRenderingBaseState,
> extends SceneObjectBase<S> {
public constructor(state: S) {
super(state);
this.addActivationHandler(() => this._baseActivationHandler());
}
private _baseActivationHandler() {
// Similarly to the ConditionalRendering activation handler,
// this ensures that all children are activated when conditional rendering is activated
// We need this to allow children to subscribe to variable changes, etc.
this.forEachChild((child) => {
if (!child.isActive) {
this._subs.add(child.activate());
}
});
}
public readonly supportedItemTypes: ItemsWithConditionalRendering[] | undefined = undefined;
public abstract readonly title: string;
public abstract get info(): string | undefined;
public abstract serialize(): ConditionalRenderingKindTypes;
public abstract evaluate(): boolean;
public onDelete() {
this._getConditionalLogicRoot().deleteItem(this);
}
public render(withWrapper = true): ReactElement {
return <ConditionalRenderingBaseRenderer model={this} withWrapper={withWrapper} key={this.state.key!} />;
}
public getItem(): SceneObject {
return this._getConditionalLogicRoot().getItem();
}
public getItemType(): ItemsWithConditionalRendering {
return this._getConditionalLogicRoot().getItemType();
}
public isItemSupported(): boolean {
if (!this.supportedItemTypes) {
return true;
}
return this.supportedItemTypes.includes(this.getItemType());
}
public notifyChange() {
this._getConditionalLogicRoot().notifyChange();
}
public setStateAndNotify(state: Partial<S>) {
this.setState(state);
this.notifyChange();
}
public findRule() {
return this.getRenderingGroup().getRule(this.state.key!);
}
public undoDeleted(index: number, rule: ConditionalRenderingConditions) {
const group = this.getRenderingGroup();
const restoredState = [...group.state.value];
restoredState.splice(index, 0, rule);
group.setStateAndNotify({ value: restoredState });
}
private getRenderingGroup(): ConditionalRenderingGroup {
// TODO: Adjust once nested rules are introduced to get relevant ConditionalRenderingGroup
return this._getConditionalLogicRoot().state.rootGroup;
}
private _getConditionalLogicRoot(): ConditionalRendering {
return sceneGraph.getAncestor(this, ConditionalRendering);
}
}
function ConditionalRenderingBaseRenderer<T extends ConditionalRenderingBase>({
model,
withWrapper,
}: SceneComponentProps<T> & { withWrapper: boolean }) {
const comp = <model.Component model={model} key={model.state.key} />;
if (!withWrapper) {
return comp;
}
return (
<Stack direction="column" key={model.state.key}>
<Stack direction="row" gap={1}>
<Text variant="bodySmall">{model.title}</Text>
{model.info && (
<Tooltip content={model.info}>
<Icon name="info-circle" />
</Tooltip>
)}
</Stack>
<Stack direction="row" gap={1} justifyContent="stretch" alignItems="center">
<Stack flex={1} direction="column" gap={1}>
{comp}
{!model.isItemSupported() && (
<Alert severity="error" title="">
<Trans i18nKey="dashboard.conditional-rendering.conditions.shared.unsupported-item-type">
This condition is not supported by the element, hence it will be ignored.
</Trans>
</Alert>
)}
</Stack>
<IconButton
aria-label={t('dashboard.conditional-rendering.conditions.shared.delete-condition', 'Delete Condition')}
name="trash-alt"
onClick={() => {
const { rule, ruleIndex } = model.findRule();
dashboardEditActions.edit({
description: t('dashboard.conditional-rendering.conditions.shared.delete-condition', 'Delete Condition'),
source: model,
perform: () => model.onDelete(),
undo: () => {
if (rule) {
model.undoDeleted(ruleIndex, rule);
}
},
});
}}
/>
</Stack>
</Stack>
);
}
@@ -1,141 +0,0 @@
import { useMemo } from 'react';
import { PanelData } from '@grafana/data';
import { t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { ConditionalRenderingDataKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Combobox, ComboboxOption } from '@grafana/ui';
import { dashboardEditActions } from '../edit-pane/shared';
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase';
import { ConditionalRenderingSerializerRegistryItem, DataConditionValue, ItemsWithConditionalRendering } from './types';
import { translatedItemType } from './utils';
type ConditionalRenderingDataState = ConditionalRenderingBaseState<DataConditionValue>;
export class ConditionalRenderingData extends ConditionalRenderingBase<ConditionalRenderingDataState> {
public static Component = ConditionalRenderingDataRenderer;
public static serializer: ConditionalRenderingSerializerRegistryItem = {
id: 'ConditionalRenderingData',
name: 'Data',
deserialize: this.deserialize,
};
public readonly supportedItemTypes: ItemsWithConditionalRendering[] = ['panel'];
public get title(): string {
return t('dashboard.conditional-rendering.conditions.data.label', 'Query result');
}
public get info(): string {
return t(
'dashboard.conditional-rendering.conditions.data.info',
'Show or hide the {{type}} based on query results.',
{ type: translatedItemType(this.getItemType()) }
);
}
public constructor(state: ConditionalRenderingDataState) {
super(state);
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
if (!this.isItemSupported()) {
return;
}
const item = this.getItem();
if (item instanceof AutoGridItem) {
const dataProvider = sceneGraph.getData(item.state.body);
if (!dataProvider) {
return;
}
this._subs.add(dataProvider.subscribeToState(() => this.notifyChange()));
}
}
public evaluate(): boolean {
if (!this.isItemSupported()) {
return true;
}
const hasData = this._hasData();
return (this.state.value && hasData) || (!this.state.value && !hasData);
}
public serialize(): ConditionalRenderingDataKind {
return { kind: 'ConditionalRenderingData', spec: { value: this.state.value } };
}
private _hasData(): boolean {
const item = this.getItem();
let data: PanelData | undefined;
if (item instanceof AutoGridItem) {
data = sceneGraph.getData(item.state.body).state.data;
}
if (!data) {
return false;
}
const series = data?.series ?? [];
for (let seriesIdx = 0; seriesIdx < series.length; seriesIdx++) {
if (series[seriesIdx].length > 0) {
return true;
}
}
return false;
}
public static deserialize(model: ConditionalRenderingDataKind): ConditionalRenderingData {
return new ConditionalRenderingData({ value: model.spec.value });
}
public static createEmpty(): ConditionalRenderingData {
return new ConditionalRenderingData({ value: true });
}
}
function ConditionalRenderingDataRenderer({ model }: SceneComponentProps<ConditionalRenderingData>) {
const { value } = model.useState();
const enableConditionOptions: Array<ComboboxOption<1 | 0>> = useMemo(
() => [
{ label: t('dashboard.conditional-rendering.conditions.data.enable', 'Has data'), value: 1 },
{ label: t('dashboard.conditional-rendering.conditions.data.disable', 'No data'), value: 0 },
],
[]
);
const enableConditionOption = useMemo(
() => enableConditionOptions.find((option) => Boolean(option.value) === value) ?? enableConditionOptions[0],
[enableConditionOptions, value]
);
return (
<Combobox
options={enableConditionOptions}
value={enableConditionOption}
onChange={({ value: val }) => {
dashboardEditActions.edit({
description: t('dashboard.edit-actions.edit-query-result-rule', 'Change query result rule'),
source: model,
perform: () => model.setStateAndNotify({ value: Boolean(val) }),
undo: () => model.setStateAndNotify({ value }),
});
}}
/>
);
}
@@ -1,204 +0,0 @@
import { capitalize, lowerCase } from 'lodash';
import { useMemo } from 'react';
import { t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Stack } from '@grafana/ui';
import { dashboardEditActions } from '../edit-pane/shared';
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase';
import { ConditionalRenderingData } from './ConditionalRenderingData';
import { ConditionalRenderingGroupAdd } from './ConditionalRenderingGroupAdd';
import { ConditionalRenderingGroupCondition } from './ConditionalRenderingGroupCondition';
import { ConditionalRenderingGroupVisibility } from './ConditionalRenderingGroupVisibility';
import { ConditionalRenderingTimeRangeSize } from './ConditionalRenderingTimeRangeSize';
import { ConditionalRenderingVariable } from './ConditionalRenderingVariable';
import { conditionalRenderingSerializerRegistry } from './serializers';
import {
ConditionalRenderingKindTypes,
ConditionalRenderingSerializerRegistryItem,
GroupConditionCondition,
GroupConditionItemType,
GroupConditionVisibility,
GroupConditionValue,
ConditionalRenderingConditions,
} from './types';
import { translatedItemType } from './utils';
export interface ConditionalRenderingGroupState extends ConditionalRenderingBaseState<GroupConditionValue> {
visibility: GroupConditionVisibility;
condition: GroupConditionCondition;
}
export class ConditionalRenderingGroup extends ConditionalRenderingBase<ConditionalRenderingGroupState> {
public static Component = ConditionalRenderingGroupRenderer;
public static serializer: ConditionalRenderingSerializerRegistryItem = {
id: 'ConditionalRenderingGroup',
name: 'Group',
deserialize: this.deserialize,
};
public get title(): string {
return t('dashboard.conditional-rendering.conditions.group.label', 'Group');
}
public get info(): undefined {
return undefined;
}
public evaluate(): boolean {
if (this.state.value.length === 0) {
return true;
}
const value =
this.state.condition === 'and'
? this.state.value.every((entry) => entry.evaluate())
: this.state.value.some((entry) => entry.evaluate());
return this.state.visibility === 'show' ? value : !value;
}
public changeVisibility(visibility: GroupConditionVisibility) {
this.setStateAndNotify({ visibility });
}
public changeCondition(condition: GroupConditionCondition) {
this.setStateAndNotify({ condition });
}
public createItem(itemType: GroupConditionItemType): ConditionalRenderingConditions {
switch (itemType) {
case 'data':
return ConditionalRenderingData.createEmpty();
case 'timeRangeSize':
return ConditionalRenderingTimeRangeSize.createEmpty();
case 'variable':
return ConditionalRenderingVariable.createEmpty(sceneGraph.getVariables(this).state.variables[0].state.name);
}
}
public addItem(item: ConditionalRenderingConditions) {
// We don't use `setStateAndNotify` here because
// We need to set a parent and activate the new condition before notifying the root
this.setState({ value: [...this.state.value, item] });
if (this.isActive && !item.isActive) {
item.activate();
}
this.notifyChange();
}
public removeItem(key: string) {
this.setStateAndNotify({ value: this.state.value.filter((condition) => condition.state.key !== key) });
}
public removeLastItem() {
const newValues = [...this.state.value];
newValues.pop();
this.setStateAndNotify({ value: newValues });
}
public getRule(key: string) {
const ruleIndex = this.state.value.findIndex((rule) => rule.state.key === key);
const rule = this.state.value[ruleIndex];
return { rule, ruleIndex };
}
public serialize(): ConditionalRenderingGroupKind {
if (this.state.value.some((item) => item instanceof ConditionalRenderingGroup)) {
throw new Error('ConditionalRenderingGroup cannot contain nested ConditionalRenderingGroups');
}
return {
kind: 'ConditionalRenderingGroup',
spec: {
visibility: this.state.visibility,
condition: this.state.condition,
items: this.state.value
.map((condition) => condition.serialize())
.filter((item) => item.kind !== 'ConditionalRenderingGroup'),
},
};
}
public static deserialize(model: ConditionalRenderingGroupKind): ConditionalRenderingGroup {
return new ConditionalRenderingGroup({
condition: model.spec.condition,
visibility: model.spec.visibility,
value: model.spec.items.map((item: ConditionalRenderingKindTypes) => {
const serializerRegistryItem = conditionalRenderingSerializerRegistry.getIfExists(item.kind);
if (!serializerRegistryItem) {
throw new Error(`No serializer found for conditional rendering kind: ${item.kind}`);
}
return serializerRegistryItem.deserialize(item);
}),
});
}
public static createEmpty(): ConditionalRenderingGroup {
return new ConditionalRenderingGroup({ condition: 'and', visibility: 'show', value: [] });
}
}
function ConditionalRenderingGroupRenderer({ model }: SceneComponentProps<ConditionalRenderingGroup>) {
const { condition, visibility, value } = model.useState();
const { variables } = sceneGraph.getVariables(model).useState();
const itemType = useMemo(() => model.getItemType(), [model]);
return (
<Stack direction="column" gap={2}>
<ConditionalRenderingGroupVisibility
itemType={itemType}
value={visibility}
onChange={(value) => {
dashboardEditActions.edit({
description: t('dashboard.conditional-rendering.conditions.group.visibility.label', '{{type}} visibility', {
type: capitalize(translatedItemType(itemType)),
}),
source: model,
perform: () => model.changeVisibility(value),
undo: () => model.changeVisibility(visibility),
});
}}
/>
{value.length > 1 && (
<ConditionalRenderingGroupCondition
value={condition}
onChange={(value) => {
dashboardEditActions.edit({
description: t('dashboard.conditional-rendering.conditions.group.condition.label', 'Match rules'),
source: model,
perform: () => model.changeCondition(value),
undo: () => model.changeCondition(condition),
});
}}
/>
)}
{value.map((entry) => entry.render())}
<ConditionalRenderingGroupAdd
itemType={itemType}
hasVariables={variables.length > 0}
onAdd={({ value, label }) => {
const item = model.createItem(value!);
dashboardEditActions.edit({
description: t('dashboard.edit-actions.add-conditional-rule', 'Add {{ruleDescription}} rule', {
ruleDescription: lowerCase(label),
}),
source: model,
perform: () => model.addItem(item),
undo: () => model.removeLastItem(),
});
}}
/>
</Stack>
);
}
@@ -1,256 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph, VariableDependencyConfig } from '@grafana/scenes';
import {
ConditionalRenderingVariableKind,
ConditionalRenderingVariableSpec,
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Box, Combobox, ComboboxOption, Field, Input, Stack } from '@grafana/ui';
import { dashboardEditActions } from '../edit-pane/shared';
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase';
import {
ConditionalRenderingSerializerRegistryItem,
VariableConditionValue,
VariableConditionValueOperator,
} from './types';
import { translatedItemType } from './utils';
type ConditionalRenderingVariableState = ConditionalRenderingBaseState<VariableConditionValue>;
export class ConditionalRenderingVariable extends ConditionalRenderingBase<ConditionalRenderingVariableState> {
public static Component = ConditionalRenderingVariableRenderer;
public static serializer: ConditionalRenderingSerializerRegistryItem = {
id: 'ConditionalRenderingVariable',
name: 'Variable',
deserialize: this.deserialize,
};
public get title(): string {
return t('dashboard.conditional-rendering.conditions.variable.label', 'Template variable');
}
public get info(): string {
return t(
'dashboard.conditional-rendering.conditions.variable.info',
'Show or hide the {{type}} dynamically based on the variable value.',
{ type: translatedItemType(this.getItemType()) }
);
}
protected _variableDependency = new VariableDependencyConfig(this, {
onAnyVariableChanged: (v) => {
if (v.state.name === this.state.value.name) {
this.notifyChange();
}
},
});
public evaluate(): boolean {
if (!this.state.value.name) {
return true;
}
const variable = sceneGraph.getVariables(this).getByName(this.state.value.name);
if (!variable) {
return true;
}
const variableValue = variable.getValue() ?? '';
let hit: boolean;
if (this.state.value.operator === '=' || this.state.value.operator === '!=') {
hit = Array.isArray(variableValue)
? variableValue.includes(this.state.value.value.toString())
: variableValue === this.state.value.value.toString();
} else {
try {
const regex = new RegExp(this.state.value.value);
hit = Array.isArray(variableValue)
? variableValue.some((currentVariableValue) => regex.test(currentVariableValue.toString()))
: regex.test(variableValue.toString());
} catch (err) {
return true;
}
}
return this.state.value.operator === '!=' || this.state.value.operator === '!~' ? !hit : hit;
}
public serialize(): ConditionalRenderingVariableKind {
return {
kind: 'ConditionalRenderingVariable',
spec: {
variable: this.state.value.name,
operator: this._getLongOperator(this.state.value.operator),
value: this.state.value.value,
},
};
}
public static deserialize(model: ConditionalRenderingVariableKind): ConditionalRenderingVariable {
return new ConditionalRenderingVariable({
value: {
name: model.spec.variable,
operator: ConditionalRenderingVariable._getShortOperator(model.spec.operator),
value: model.spec.value,
},
});
}
public static createEmpty(name: string): ConditionalRenderingVariable {
return new ConditionalRenderingVariable({ value: { name, operator: '=', value: '' } });
}
private _getLongOperator(operator: VariableConditionValueOperator): ConditionalRenderingVariableSpec['operator'] {
switch (operator) {
case '=':
return 'equals';
case '!=':
return 'notEquals';
case '=~':
return 'matches';
case '!~':
return 'notMatches';
}
}
private static _getShortOperator(
operator: ConditionalRenderingVariableSpec['operator']
): VariableConditionValueOperator {
switch (operator) {
case 'equals':
return '=';
case 'notEquals':
return '!=';
case 'matches':
return '=~';
case 'notMatches':
return '!~';
}
}
}
function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<ConditionalRenderingVariable>) {
const { value } = model.useState();
const [actualValue, setActualValue] = useState(value.value);
useEffect(() => {
setActualValue(value.value);
}, [value.value]);
const variables = useMemo(() => sceneGraph.getVariables(model), [model]);
const variableNames: ComboboxOption[] = useMemo(
() => variables.state.variables.map((v) => ({ value: v.state.name, label: v.state.label ?? v.state.name })),
[variables.state.variables]
);
const operatorOptions: Array<ComboboxOption<VariableConditionValueOperator>> = useMemo(
() => [
{ value: '=', description: t('dashboard.conditional-rendering.conditions.variable.operator.equals', 'Equals') },
{
value: '!=',
description: t('dashboard.conditional-rendering.conditions.variable.operator.not-equals', 'Not equals'),
},
{
value: '=~',
description: t('dashboard.conditional-rendering.conditions.variable.operator.matches', 'Matches'),
},
{
value: '!~',
description: t('dashboard.conditional-rendering.conditions.variable.operator.not-matches', 'Not matches'),
},
],
[]
);
const valueError = useMemo(() => {
if (value.operator === '=~' || value.operator === '!~') {
try {
new RegExp(actualValue);
return '';
} catch (err) {
return t('dashboard.conditional-rendering.conditions.variable.error.invalid-regex', 'Invalid regex');
}
}
return '';
}, [actualValue, value.operator]);
const undoText = t('dashboard.edit-actions.edit-template-variable-rule', 'Change template variable rule');
return (
<Stack direction="column" gap={0.5}>
<Stack direction="row" gap={0.5} grow={1}>
<Box flex={1}>
<Combobox
placeholder={t('dashboard.conditional-rendering.conditions.variable.name', 'Name')}
options={variableNames}
value={value.name}
onChange={(option) => {
if (option.value !== value.name) {
dashboardEditActions.edit({
description: undoText,
source: model,
perform: () => model.setStateAndNotify({ value: { ...value, name: option.value } }),
undo: () => model.setStateAndNotify({ value: { ...value, name: value.name } }),
});
}
}}
/>
</Box>
<Combobox
width="auto"
minWidth={10}
options={operatorOptions}
value={value.operator}
onChange={(option) => {
if (option.value !== value.operator) {
dashboardEditActions.edit({
description: undoText,
source: model,
perform: () => model.setStateAndNotify({ value: { ...value, operator: option.value } }),
undo: () => model.setStateAndNotify({ value: { ...value, operator: value.operator } }),
});
}
}}
/>
</Stack>
<Field error={valueError} invalid={!!valueError} noMargin>
<Input
placeholder={t('dashboard.conditional-rendering.conditions.variable.value', 'Value')}
value={actualValue}
onChange={(evt) => {
if (evt.currentTarget.value !== value.value) {
setActualValue(evt.currentTarget.value);
}
}}
onBlur={() => {
if (actualValue !== value.value) {
dashboardEditActions.edit({
description: undoText,
source: model,
perform: () => model.setStateAndNotify({ value: { ...value, value: actualValue } }),
undo: () => model.setStateAndNotify({ value: { ...value, value: value.value } }),
});
}
}}
/>
</Field>
</Stack>
);
}
@@ -0,0 +1,64 @@
import { ReactNode } from 'react';
import { t, Trans } from '@grafana/i18n';
import { Alert, Icon, IconButton, Stack, Text, Tooltip } from '@grafana/ui';
import { dashboardEditActions } from '../../edit-pane/shared';
import { ConditionalRenderingConditions } from './types';
import { getConditionIndex, removeCondition, undoRemoveCondition } from './utils';
interface Props {
children: ReactNode;
info: string;
isObjectSupported: boolean;
model: ConditionalRenderingConditions;
title: string;
}
export function ConditionalRenderingConditionWrapper({ children, info, isObjectSupported, model, title }: Props) {
return (
<Stack direction="column" key={model.state.key!}>
<Stack direction="row" gap={1}>
<Text variant="bodySmall">{title}</Text>
{info && (
<Tooltip content={info}>
<Icon name="info-circle" />
</Tooltip>
)}
</Stack>
<Stack direction="row" gap={1} justifyContent="stretch" alignItems="center">
<Stack flex={1} direction="column" gap={1}>
{children}
{!isObjectSupported && (
<Alert severity="error" title="">
<Trans i18nKey="dashboard.conditional-rendering.conditions.wrapper.unsupported-object-type">
This condition is not supported by the element, hence it will be ignored.
</Trans>
</Alert>
)}
</Stack>
<IconButton
aria-label={t('dashboard.conditional-rendering.conditions.wrapper.delete-condition', 'Delete Condition')}
name="trash-alt"
onClick={() => {
const index = getConditionIndex(model);
dashboardEditActions.edit({
description: t('dashboard.conditional-rendering.conditions.wrapper.delete-condition', 'Delete Condition'),
source: model,
perform: () => removeCondition(model),
undo: () => {
if (index > -1) {
undoRemoveCondition(model, index);
}
},
});
}}
/>
</Stack>
</Stack>
);
}
@@ -0,0 +1,196 @@
import { ReactElement, useMemo } from 'react';
import { LoadingState } from '@grafana/data';
import { t } from '@grafana/i18n';
import {
CancelActivationHandler,
SceneComponentProps,
SceneDataProvider,
sceneGraph,
SceneObjectBase,
SceneObjectState,
VizPanel,
} from '@grafana/scenes';
import { ConditionalRenderingDataKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Combobox, ComboboxOption } from '@grafana/ui';
import { dashboardEditActions } from '../../edit-pane/shared';
import { getLowerTranslatedObjectType } from '../object';
import { ConditionalRenderingConditionWrapper } from './ConditionalRenderingConditionWrapper';
import { ConditionalRenderingConditionsSerializerRegistryItem } from './serializers';
import { checkGroup, getObject, getObjectType } from './utils';
interface ConditionalRenderingDataState extends SceneObjectState {
value: boolean;
result: boolean | undefined;
}
export class ConditionalRenderingData extends SceneObjectBase<ConditionalRenderingDataState> {
public static Component = ConditionalRenderingDataRenderer;
public static serializer: ConditionalRenderingConditionsSerializerRegistryItem = {
id: 'ConditionalRenderingData',
name: 'Data',
deserialize: this.deserialize,
};
private _dataProvider: SceneDataProvider | undefined = undefined;
public constructor(state: ConditionalRenderingDataState) {
super(state);
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
this.forEachChild((child) => {
if (!child.isActive) {
this._subs.add(child.activate());
}
});
this._dataProvider = this._getObjectDataProvider();
if (!this._dataProvider) {
return;
}
// Be sure to activate the data provider so it doesn't get deactivated on item unmount
const dpDeactivate: CancelActivationHandler = this._dataProvider.activate();
this._check();
this._subs.add(this._dataProvider.subscribeToState(() => this._check()));
return () => {
dpDeactivate?.();
};
}
private _getObjectDataProvider(): SceneDataProvider | undefined {
const object = getObject(this);
if (!object) {
return undefined;
}
let panel: VizPanel | undefined;
for (const val of Object.values(object.state)) {
if (val instanceof VizPanel) {
panel = val;
break;
}
}
if (!panel) {
return undefined;
}
return sceneGraph.getData(panel) ?? undefined;
}
private _check() {
const result = this._evaluate();
if (result !== this.state.result) {
this.setState({ ...this.state, result });
checkGroup(this);
}
}
private _evaluate(): boolean | undefined {
if (
!this._dataProvider ||
!this._dataProvider.state.data ||
this._dataProvider.state.data.state === LoadingState.Loading ||
this._dataProvider.state.data.state === LoadingState.NotStarted
) {
return undefined;
}
const series = this._dataProvider?.state.data?.series ?? [];
let hasData = false;
for (let seriesIdx = 0; seriesIdx < series.length; seriesIdx++) {
if (series[seriesIdx].length > 0) {
hasData = true;
break;
}
}
// The logic here is pretty simple:
// If value is true (meaning "has data"), then we check if the object has data
// If value is false (meaning "no data"), then we check if the object doesn't have data
return this.state.value === hasData;
}
public changeValue(value: boolean) {
if (this.state.value !== value) {
this.setState({ value });
this._check();
}
}
public render(): ReactElement {
return <this.Component model={this} key={this.state.key} />;
}
public serialize(): ConditionalRenderingDataKind {
return { kind: 'ConditionalRenderingData', spec: { value: this.state.value } };
}
public static deserialize(model: ConditionalRenderingDataKind): ConditionalRenderingData {
return new ConditionalRenderingData({ value: model.spec.value, result: undefined });
}
public static createEmpty(): ConditionalRenderingData {
return new ConditionalRenderingData({ value: true, result: undefined });
}
}
function ConditionalRenderingDataRenderer({ model }: SceneComponentProps<ConditionalRenderingData>) {
const { value } = model.useState();
const enableConditionOptions: Array<ComboboxOption<1 | 0>> = useMemo(
() => [
{ label: t('dashboard.conditional-rendering.conditions.data.enable', 'Has data'), value: 1 },
{ label: t('dashboard.conditional-rendering.conditions.data.disable', 'No data'), value: 0 },
],
[]
);
const enableConditionOption = useMemo(
() => enableConditionOptions.find((option) => Boolean(option.value) === value) ?? enableConditionOptions[0],
[enableConditionOptions, value]
);
const objectType = getObjectType(model);
return (
<ConditionalRenderingConditionWrapper
info={t(
'dashboard.conditional-rendering.conditions.data.info',
'Show or hide the {{type}} based on query results.',
{ type: getLowerTranslatedObjectType(objectType) }
)}
isObjectSupported={objectType === 'panel'}
model={model}
title={t('dashboard.conditional-rendering.conditions.data.label', 'Query result')}
>
<Combobox
options={enableConditionOptions}
value={enableConditionOption}
onChange={({ value: newValue }) => {
dashboardEditActions.edit({
description: t('dashboard.edit-actions.edit-query-result-rule', 'Change query result rule'),
source: model,
perform: () => model.changeValue(Boolean(newValue)),
undo: () => model.changeValue(value),
});
}}
/>
</ConditionalRenderingConditionWrapper>
);
}
@@ -1,40 +1,33 @@
import { css } from '@emotion/css';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
import { rangeUtil, SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { ConditionalRenderingTimeRangeSizeKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Field, Select, useStyles2 } from '@grafana/ui';
import { Field, Select } from '@grafana/ui';
import { dashboardEditActions } from '../edit-pane/shared';
import { dashboardEditActions } from '../../edit-pane/shared';
import { getLowerTranslatedObjectType } from '../object';
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase';
import { ConditionalRenderingSerializerRegistryItem, TimeRangeSizeConditionValue } from './types';
import { translatedItemType } from './utils';
import { ConditionalRenderingConditionWrapper } from './ConditionalRenderingConditionWrapper';
import { ConditionalRenderingConditionsSerializerRegistryItem } from './serializers';
import { checkGroup, getObjectType } from './utils';
type ConditionalRenderingTimeRangeSizeState = ConditionalRenderingBaseState<TimeRangeSizeConditionValue>;
interface ConditionalRenderingTimeRangeSizeState extends SceneObjectState {
value: string;
result: boolean | undefined;
}
export class ConditionalRenderingTimeRangeSize extends ConditionalRenderingBase<ConditionalRenderingTimeRangeSizeState> {
export class ConditionalRenderingTimeRangeSize extends SceneObjectBase<ConditionalRenderingTimeRangeSizeState> {
public static Component = ConditionalRenderingTimeRangeSizeRenderer;
public static serializer: ConditionalRenderingSerializerRegistryItem = {
public static serializer: ConditionalRenderingConditionsSerializerRegistryItem = {
id: 'ConditionalRenderingTimeRangeSize',
name: 'Time Range Size',
deserialize: this.deserialize,
};
public get title(): string {
return t('dashboard.conditional-rendering.conditions.time-range-size.label', 'Time range less than');
}
public get info(): string {
return t(
'dashboard.conditional-rendering.conditions.time-range-size.info',
'Show or hide the {{type}} if the dashboard time range is shorter than the selected time frame.',
{ type: translatedItemType(this.getItemType()) }
);
}
public readonly validateIntervalRegex = /^(\d+(?:\.\d+)?)[Mwdhmsy]$/;
public constructor(state: ConditionalRenderingTimeRangeSizeState) {
super(state);
@@ -43,26 +36,52 @@ export class ConditionalRenderingTimeRangeSize extends ConditionalRenderingBase<
}
private _activationHandler() {
this._subs.add(sceneGraph.getTimeRange(this).subscribeToState(() => this.notifyChange()));
this.forEachChild((child) => {
if (!child.isActive) {
this._subs.add(child.activate());
}
});
this._check();
this._subs.add(sceneGraph.getTimeRange(this).subscribeToState(() => this._check()));
}
public evaluate(): boolean {
private _check() {
const result = this._evaluate();
if (result !== this.state.result) {
this.setState({ ...this.state, result });
checkGroup(this);
}
return result;
}
private _evaluate(): boolean | undefined {
try {
if (!validateIntervalRegex.test(this.state.value)) {
return true;
if (!this.validateIntervalRegex.test(this.state.value)) {
return undefined;
}
const interval = rangeUtil.intervalToSeconds(this.state.value);
const timeRange = sceneGraph.getTimeRange(this);
if (timeRange.state.value.to.unix() - timeRange.state.value.from.unix() <= interval) {
return true;
}
return timeRange.state.value.to.unix() - timeRange.state.value.from.unix() <= interval;
} catch {
return false;
return undefined;
}
}
return false;
public changeValue(value: string) {
if (this.state.value !== value) {
this.setState({ value });
this._check();
}
}
public render(): ReactElement {
return <this.Component model={this} key={this.state.key} />;
}
public serialize(): ConditionalRenderingTimeRangeSizeKind {
@@ -70,20 +89,19 @@ export class ConditionalRenderingTimeRangeSize extends ConditionalRenderingBase<
}
public static deserialize(model: ConditionalRenderingTimeRangeSizeKind): ConditionalRenderingTimeRangeSize {
return new ConditionalRenderingTimeRangeSize({ value: model.spec.value });
return new ConditionalRenderingTimeRangeSize({ value: model.spec.value, result: undefined });
}
public static createEmpty(): ConditionalRenderingTimeRangeSize {
return new ConditionalRenderingTimeRangeSize({ value: '7d' });
return new ConditionalRenderingTimeRangeSize({ value: '7d', result: undefined });
}
}
function ConditionalRenderingTimeRangeSizeRenderer({ model }: SceneComponentProps<ConditionalRenderingTimeRangeSize>) {
const { value } = model.useState();
const [isValid, setIsValid] = useState(validateIntervalRegex.test(value));
const styles = useStyles2(getStyles);
const [isValid, setIsValid] = useState(model.validateIntervalRegex.test(value));
useEffect(() => setIsValid(validateIntervalRegex.test(value)), [value]);
useEffect(() => setIsValid(model.validateIntervalRegex.test(value)), [model, value]);
const staticOptions = useMemo<Array<SelectableValue<string>>>(
() => [
@@ -200,35 +218,38 @@ function ConditionalRenderingTimeRangeSizeRenderer({ model }: SceneComponentProp
dashboardEditActions.edit({
description: t('dashboard.edit-actions.edit-time-range-rule', 'Change time range rule'),
source: model,
perform: () => model.setStateAndNotify({ value: newValue }),
undo: () => model.setStateAndNotify({ value }),
perform: () => model.changeValue(newValue ?? ''),
undo: () => model.changeValue(value),
});
},
[model, value]
);
return (
<Field
invalid={!isValid}
error={t('dashboard.conditional-rendering.conditions.time-range-size.invalid-message', 'Invalid interval')}
className={styles.container}
<ConditionalRenderingConditionWrapper
info={t(
'dashboard.conditional-rendering.conditions.time-range-size.info',
'Show or hide the {{type}} if the dashboard time range is shorter than the selected time frame.',
{ type: getLowerTranslatedObjectType(getObjectType(model)) }
)}
isObjectSupported={true}
model={model}
title={t('dashboard.conditional-rendering.conditions.time-range-size.label', 'Time range less than')}
>
<Select
isClearable={false}
allowCustomValue
onCreateOption={(value) => handleChange(value)}
value={value}
options={options}
onChange={({ value }) => handleChange(value)}
/>
</Field>
<Field
invalid={!isValid}
error={t('dashboard.conditional-rendering.conditions.time-range-size.invalid-message', 'Invalid interval')}
noMargin
>
<Select
isClearable={false}
allowCustomValue
onCreateOption={(value) => handleChange(value)}
value={value}
options={options}
onChange={({ value }) => handleChange(value)}
/>
</Field>
</ConditionalRenderingConditionWrapper>
);
}
const getStyles = () => ({
container: css({
margin: 0,
}),
});
const validateIntervalRegex = /^(\d+(?:\.\d+)?)[Mwdhmsy]$/;
@@ -0,0 +1,318 @@
import { ReactElement, useEffect, useMemo, useState } from 'react';
import { t } from '@grafana/i18n';
import {
SceneComponentProps,
sceneGraph,
SceneObjectBase,
SceneObjectState,
VariableDependencyConfig,
} from '@grafana/scenes';
import {
ConditionalRenderingVariableKind,
ConditionalRenderingVariableSpec,
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Box, Combobox, ComboboxOption, Field, Input, Stack } from '@grafana/ui';
import { dashboardEditActions } from '../../edit-pane/shared';
import { getDashboardSceneFor } from '../../utils/utils';
import { getLowerTranslatedObjectType } from '../object';
import { ConditionalRenderingConditionWrapper } from './ConditionalRenderingConditionWrapper';
import { ConditionalRenderingConditionsSerializerRegistryItem } from './serializers';
import { checkGroup, getObjectType } from './utils';
type VariableConditionValueOperator = '=' | '!=' | '=~' | '!~';
interface ConditionalRenderingVariableState extends SceneObjectState {
variable: string;
operator: VariableConditionValueOperator;
value: string;
result: boolean | undefined;
}
export class ConditionalRenderingVariable extends SceneObjectBase<ConditionalRenderingVariableState> {
public static Component = ConditionalRenderingVariableRenderer;
public static serializer: ConditionalRenderingConditionsSerializerRegistryItem = {
id: 'ConditionalRenderingVariable',
name: 'Variable',
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);
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
this.forEachChild((child) => {
if (!child.isActive) {
this._subs.add(child.activate());
}
});
this._check();
}
public _check() {
const result = this._evaluate();
if (result !== this.state.result) {
this.setState({ result });
checkGroup(this);
}
}
private _evaluate(): boolean | undefined {
if (!this.state.variable) {
return undefined;
}
const variable = sceneGraph.getVariables(this).getByName(this.state.variable);
if (!variable) {
return undefined;
}
const variableValue = variable.getValue() ?? '';
let hit: boolean;
if (this.state.operator === '=' || this.state.operator === '!=') {
hit = Array.isArray(variableValue)
? variableValue.includes(this.state.value.toString())
: variableValue === this.state.value.toString();
} else {
try {
const regex = new RegExp(this.state.value);
hit = Array.isArray(variableValue)
? variableValue.some((currentVariableValue) => regex.test(currentVariableValue.toString()))
: regex.test(variableValue.toString());
} catch (err) {
return true;
}
}
return this.state.operator === '!=' || this.state.operator === '!~' ? !hit : hit;
}
public changeVariable(variable: string) {
if (this.state.variable !== variable) {
this.setState({ variable });
this._check();
}
}
public changeOperator(operator: VariableConditionValueOperator) {
if (this.state.operator !== operator) {
this.setState({ operator });
this._check();
}
}
public changeValue(value: string) {
if (this.state.value !== value) {
this.setState({ value });
this._check();
}
}
public render(): ReactElement {
return <this.Component model={this} key={this.state.key} />;
}
public serialize(): ConditionalRenderingVariableKind {
return {
kind: 'ConditionalRenderingVariable',
spec: {
variable: this.state.variable,
operator: this._getLongOperator(this.state.operator),
value: this.state.value,
},
};
}
public static deserialize(model: ConditionalRenderingVariableKind): ConditionalRenderingVariable {
return new ConditionalRenderingVariable({
variable: model.spec.variable,
operator: ConditionalRenderingVariable._getShortOperator(model.spec.operator),
value: model.spec.value,
result: undefined,
});
}
public static createEmpty(variable: string): ConditionalRenderingVariable {
return new ConditionalRenderingVariable({ variable, operator: '=', value: '', result: undefined });
}
private _getLongOperator(operator: VariableConditionValueOperator): ConditionalRenderingVariableSpec['operator'] {
switch (operator) {
case '=':
return 'equals';
case '!=':
return 'notEquals';
case '=~':
return 'matches';
case '!~':
return 'notMatches';
}
}
private static _getShortOperator(
operator: ConditionalRenderingVariableSpec['operator']
): VariableConditionValueOperator {
switch (operator) {
case 'equals':
return '=';
case 'notEquals':
return '!=';
case 'matches':
return '=~';
case 'notMatches':
return '!~';
}
}
}
function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<ConditionalRenderingVariable>) {
const { variable, operator, value } = model.useState();
const [newValue, setNewValue] = useState(value);
useEffect(() => setNewValue(value), [value]);
const variables = sceneGraph.getVariables(getDashboardSceneFor(model));
const variableNames: ComboboxOption[] = useMemo(
() => variables.state.variables.map((v) => ({ value: v.state.name, label: v.state.label ?? v.state.name })),
[variables.state.variables]
);
const operatorOptions: Array<ComboboxOption<VariableConditionValueOperator>> = useMemo(
() => [
{ value: '=', description: t('dashboard.conditional-rendering.conditions.variable.operator.equals', 'Equals') },
{
value: '!=',
description: t('dashboard.conditional-rendering.conditions.variable.operator.not-equals', 'Not equals'),
},
{
value: '=~',
description: t('dashboard.conditional-rendering.conditions.variable.operator.matches', 'Matches'),
},
{
value: '!~',
description: t('dashboard.conditional-rendering.conditions.variable.operator.not-matches', 'Not matches'),
},
],
[]
);
const valueError = useMemo(() => {
if (operator === '=~' || operator === '!~') {
try {
new RegExp(newValue);
return '';
} catch (err) {
return t('dashboard.conditional-rendering.conditions.variable.error.invalid-regex', 'Invalid regex');
}
}
return '';
}, [newValue, operator]);
const undoText = t('dashboard.edit-actions.edit-template-variable-rule', 'Change template variable rule');
return (
<ConditionalRenderingConditionWrapper
info={t(
'dashboard.conditional-rendering.conditions.variable.info',
'Show or hide the {{type}} dynamically based on the variable value.',
{ type: getLowerTranslatedObjectType(getObjectType(model)) }
)}
isObjectSupported={true}
model={model}
title={t('dashboard.conditional-rendering.conditions.variable.label', 'Template variable')}
>
<Stack direction="column" gap={0.5}>
<Stack direction="row" gap={0.5} grow={1}>
<Box flex={1}>
<Combobox
placeholder={t('dashboard.conditional-rendering.conditions.variable.name', 'Name')}
options={variableNames}
value={variable}
onChange={(option) => {
const newVariable = option.value;
if (newVariable !== variable) {
dashboardEditActions.edit({
description: undoText,
source: model,
perform: () => model.changeVariable(newVariable),
undo: () => model.changeVariable(variable),
});
}
}}
/>
</Box>
<Combobox
width="auto"
minWidth={10}
options={operatorOptions}
value={operator}
onChange={(option) => {
const newOperator = option.value;
if (newOperator !== operator) {
dashboardEditActions.edit({
description: undoText,
source: model,
perform: () => model.changeOperator(newOperator),
undo: () => model.changeOperator(operator),
});
}
}}
/>
</Stack>
<Field error={valueError} invalid={!!valueError} noMargin>
<Input
placeholder={t('dashboard.conditional-rendering.conditions.variable.value', 'Value')}
value={newValue}
onChange={(evt) => {
if (evt.currentTarget.value !== value) {
setNewValue(evt.currentTarget.value);
}
}}
onBlur={() => {
if (newValue !== value) {
dashboardEditActions.edit({
description: undoText,
source: model,
perform: () => model.changeValue(newValue),
undo: () => model.changeValue(value),
});
}
}}
/>
</Field>
</Stack>
</ConditionalRenderingConditionWrapper>
);
}
@@ -0,0 +1,29 @@
import { Registry, RegistryItem } from '@grafana/data';
import {
ConditionalRenderingDataKind,
ConditionalRenderingTimeRangeSizeKind,
ConditionalRenderingVariableKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { ConditionalRenderingData } from './ConditionalRenderingData';
import { ConditionalRenderingTimeRangeSize } from './ConditionalRenderingTimeRangeSize';
import { ConditionalRenderingVariable } from './ConditionalRenderingVariable';
import { ConditionalRenderingConditions } from './types';
export type ConditionalRenderingConditionsKindTypes =
| ConditionalRenderingVariableKind
| ConditionalRenderingDataKind
| ConditionalRenderingTimeRangeSizeKind;
export interface ConditionalRenderingConditionsSerializerRegistryItem extends RegistryItem {
deserialize(model: ConditionalRenderingConditionsKindTypes): ConditionalRenderingConditions;
}
export const conditionalRenderingSerializerRegistry =
new Registry<ConditionalRenderingConditionsSerializerRegistryItem>(() => {
return [
ConditionalRenderingVariable.serializer,
ConditionalRenderingData.serializer,
ConditionalRenderingTimeRangeSize.serializer,
];
});
@@ -0,0 +1,8 @@
import { type ConditionalRenderingData } from './ConditionalRenderingData';
import { type ConditionalRenderingTimeRangeSize } from './ConditionalRenderingTimeRangeSize';
import { type ConditionalRenderingVariable } from './ConditionalRenderingVariable';
export type ConditionalRenderingConditions =
| ConditionalRenderingVariable
| ConditionalRenderingData
| ConditionalRenderingTimeRangeSize;
@@ -0,0 +1,38 @@
import { sceneGraph, SceneObject } from '@grafana/scenes';
import { ConditionalRenderingGroup } from '../group/ConditionalRenderingGroup';
import { extractObjectType, ObjectsWithConditionalRendering } from '../object';
import { ConditionalRenderingConditions } from './types';
export function getGroup(condition: ConditionalRenderingConditions): ConditionalRenderingGroup {
if (condition.parent instanceof ConditionalRenderingGroup) {
return condition.parent;
}
return sceneGraph.getAncestor(condition, ConditionalRenderingGroup);
}
export function getObject(condition: ConditionalRenderingConditions): SceneObject | undefined {
return getGroup(condition).parent;
}
export function getObjectType(condition: ConditionalRenderingConditions): ObjectsWithConditionalRendering {
return extractObjectType(getObject(condition));
}
export function removeCondition(condition: ConditionalRenderingConditions) {
getGroup(condition).removeCondition(condition);
}
export function undoRemoveCondition(condition: ConditionalRenderingConditions, index: number) {
getGroup(condition).undoRemoveCondition(condition, index);
}
export function getConditionIndex(condition: ConditionalRenderingConditions): number {
return getGroup(condition).getConditionIndex(condition);
}
export function checkGroup(condition: ConditionalRenderingConditions) {
getGroup(condition).check();
}
@@ -0,0 +1,240 @@
import { lowerCase } from 'lodash';
import { useMemo } from 'react';
import { t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Stack } from '@grafana/ui';
import { ConditionalRenderingChangedEvent, dashboardEditActions } from '../../edit-pane/shared';
import { getDashboardSceneFor } from '../../utils/utils';
import { ConditionalRenderingData } from '../conditions/ConditionalRenderingData';
import { ConditionalRenderingTimeRangeSize } from '../conditions/ConditionalRenderingTimeRangeSize';
import { ConditionalRenderingVariable } from '../conditions/ConditionalRenderingVariable';
import { conditionalRenderingSerializerRegistry } from '../conditions/serializers';
import { ConditionalRenderingConditions } from '../conditions/types';
import { extractObjectType, getTranslatedObjectType } from '../object';
import { ConditionalRenderingGroupAdd } from './ConditionalRenderingGroupAdd';
import { ConditionalRenderingGroupCondition } from './ConditionalRenderingGroupCondition';
import { ConditionalRenderingGroupVisibility } from './ConditionalRenderingGroupVisibility';
import { GroupConditionCondition, GroupConditionConditionType, GroupConditionVisibility } from './types';
export interface ConditionalRenderingGroupState extends SceneObjectState {
conditions: ConditionalRenderingConditions[];
visibility: GroupConditionVisibility;
condition: GroupConditionCondition;
renderHidden: boolean;
result: boolean;
}
export class ConditionalRenderingGroup extends SceneObjectBase<ConditionalRenderingGroupState> {
public static Component = ConditionalRenderingGroupRenderer;
private _shouldShow: boolean;
private _shouldMatchAll: boolean;
public constructor(state: ConditionalRenderingGroupState) {
super(state);
this._shouldShow = state.visibility === 'show';
this._shouldMatchAll = state.condition === 'and';
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
this.forEachChild((child) => {
if (!child.isActive) {
this._subs.add(child.activate());
}
});
this.check();
}
public check() {
// Filter out undefined results
// Because we negate the result if shouldShow is false, we can use `condition.state.result ?? true` directly below
const validConditions = this.state.conditions.filter((condition) => condition.state.result !== undefined);
let result = true;
if (validConditions.length > 0) {
result = this._shouldMatchAll
? validConditions.every((condition) => condition.state.result)
: validConditions.some((condition) => condition.state.result);
if (!this._shouldShow) {
result = !result;
}
}
if (result !== this.state.result) {
this.setState({ ...this.state, result });
this.publishEvent(new ConditionalRenderingChangedEvent(this), true);
}
}
public changeVisibility(visibility: GroupConditionVisibility) {
if (visibility !== this.state.visibility) {
this._shouldShow = visibility === 'show';
this.setState({ visibility });
this.check();
}
}
public changeCondition(condition: GroupConditionCondition) {
if (condition !== this.state.condition) {
this._shouldMatchAll = condition === 'and';
this.setState({ condition });
this.check();
}
}
public createCondition(conditionType: GroupConditionConditionType): ConditionalRenderingConditions {
switch (conditionType) {
case 'data':
return ConditionalRenderingData.createEmpty();
case 'timeRangeSize':
return ConditionalRenderingTimeRangeSize.createEmpty();
case 'variable':
return ConditionalRenderingVariable.createEmpty(
sceneGraph.getVariables(getDashboardSceneFor(this)).state.variables[0].state.name
);
}
}
public addCondition(condition: ConditionalRenderingConditions) {
const conditions = [...this.state.conditions, condition];
this.setState({
conditions,
renderHidden: conditions.some((condition) => condition instanceof ConditionalRenderingData),
});
if (this.isActive && !condition.isActive) {
condition.activate();
}
this.check();
}
public removeCondition(condition: ConditionalRenderingConditions) {
const conditions = this.state.conditions.filter((currentCondition) => currentCondition !== condition);
this.setState({
conditions,
renderHidden: conditions.some((condition) => condition instanceof ConditionalRenderingData),
});
this.check();
}
public undoRemoveCondition(condition: ConditionalRenderingConditions, index: number) {
const conditions = [...this.state.conditions];
conditions.splice(index, 0, condition);
this.setState({ conditions });
this.check();
}
public removeLastCondition() {
const conditions = [...this.state.conditions];
conditions.pop();
this.setState({ conditions });
this.check();
}
public getConditionIndex(condition: ConditionalRenderingConditions): number {
return this.state.conditions.findIndex((currentCondition) => currentCondition === condition);
}
public serialize(): ConditionalRenderingGroupKind {
return {
kind: 'ConditionalRenderingGroup',
spec: {
visibility: this.state.visibility,
condition: this.state.condition,
items: this.state.conditions.map((condition) => condition.serialize()),
},
};
}
public static createEmpty(): ConditionalRenderingGroup {
return new ConditionalRenderingGroup({
condition: 'and',
visibility: 'show',
conditions: [],
result: true,
renderHidden: false,
});
}
public static deserialize(model: ConditionalRenderingGroupKind): ConditionalRenderingGroup {
const conditions = model.spec.items.map((item) =>
conditionalRenderingSerializerRegistry.get(item.kind).deserialize(item)
);
return new ConditionalRenderingGroup({
condition: model.spec.condition,
visibility: model.spec.visibility,
conditions,
result: true,
renderHidden: conditions.some((condition) => condition instanceof ConditionalRenderingData),
});
}
}
function ConditionalRenderingGroupRenderer({ model }: SceneComponentProps<ConditionalRenderingGroup>) {
const { condition, visibility, conditions } = model.useState();
const { variables } = sceneGraph.getVariables(model).useState();
const objectType = useMemo(() => extractObjectType(model.parent), [model]);
return (
<Stack direction="column" gap={2}>
<ConditionalRenderingGroupVisibility
objectType={objectType}
value={visibility}
onChange={(value) => {
dashboardEditActions.edit({
description: t('dashboard.conditional-rendering.conditions.group.visibility.label', '{{type}} visibility', {
type: getTranslatedObjectType(objectType),
}),
source: model,
perform: () => model.changeVisibility(value),
undo: () => model.changeVisibility(visibility),
});
}}
/>
{conditions.length > 1 && (
<ConditionalRenderingGroupCondition
value={condition}
onChange={(value) => {
dashboardEditActions.edit({
description: t('dashboard.conditional-rendering.conditions.group.condition.label', 'Match rules'),
source: model,
perform: () => model.changeCondition(value),
undo: () => model.changeCondition(condition),
});
}}
/>
)}
{conditions.map((currentCondition) => currentCondition.render())}
<ConditionalRenderingGroupAdd
objectType={objectType}
hasVariables={variables.length > 0}
onAdd={({ value, label }) => {
const condition = model.createCondition(value!);
dashboardEditActions.edit({
description: t('dashboard.edit-actions.add-conditional-rule', 'Add {{ruleDescription}} rule', {
ruleDescription: lowerCase(label),
}),
source: model,
perform: () => model.addCondition(condition),
undo: () => model.removeLastCondition(),
});
}}
/>
</Stack>
);
}
@@ -4,17 +4,19 @@ import { SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n';
import { ValuePicker } from '@grafana/ui';
import { GroupConditionItemType, ItemsWithConditionalRendering } from './types';
import { ObjectsWithConditionalRendering } from '../object';
import { GroupConditionConditionType } from './types';
interface Props {
itemType: ItemsWithConditionalRendering;
objectType: ObjectsWithConditionalRendering;
hasVariables: boolean;
onAdd: (option: SelectableValue<GroupConditionItemType>) => void;
onAdd: (option: SelectableValue<GroupConditionConditionType>) => void;
}
export const ConditionalRenderingGroupAdd = ({ itemType, hasVariables, onAdd }: Props) => {
const options = useMemo<Array<SelectableValue<GroupConditionItemType>>>(() => {
const allOptions: Array<SelectableValue<GroupConditionItemType>> = [
export const ConditionalRenderingGroupAdd = ({ objectType, hasVariables, onAdd }: Props) => {
const options = useMemo<Array<SelectableValue<GroupConditionConditionType>>>(() => {
const allOptions: Array<SelectableValue<GroupConditionConditionType>> = [
{ label: t('dashboard.conditional-rendering.conditions.group.add.data', 'Query result'), value: 'data' },
{
label: t('dashboard.conditional-rendering.conditions.group.add.variable', 'Template variable'),
@@ -27,12 +29,12 @@ export const ConditionalRenderingGroupAdd = ({ itemType, hasVariables, onAdd }:
},
];
if (itemType !== 'panel') {
if (objectType !== 'panel') {
allOptions.shift();
}
return allOptions;
}, [itemType, hasVariables]);
}, [objectType, hasVariables]);
return (
<ValuePicker
@@ -1,9 +1,8 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n';
import { Field, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { Field, RadioButtonGroup } from '@grafana/ui';
import { GroupConditionCondition } from './types';
@@ -13,8 +12,6 @@ interface Props {
}
export const ConditionalRenderingGroupCondition = ({ value, onChange }: Props) => {
const styles = useStyles2(getStyles);
const options: Array<SelectableValue<GroupConditionCondition>> = useMemo(
() => [
{ label: t('dashboard.conditional-rendering.conditions.group.condition.all', 'Match all'), value: 'and' },
@@ -24,17 +21,8 @@ export const ConditionalRenderingGroupCondition = ({ value, onChange }: Props) =
);
return (
<Field
label={t('dashboard.conditional-rendering.conditions.group.condition.label', 'Match rules')}
className={styles.container}
>
<Field label={t('dashboard.conditional-rendering.conditions.group.condition.label', 'Match rules')} noMargin>
<RadioButtonGroup fullWidth options={options} value={value} onChange={onChange} />
</Field>
);
};
const getStyles = () => ({
container: css({
margin: 0,
}),
});
@@ -1,23 +1,20 @@
import { css } from '@emotion/css';
import { capitalize } from 'lodash';
import { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n';
import { Field, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { Field, RadioButtonGroup } from '@grafana/ui';
import { GroupConditionVisibility, ItemsWithConditionalRendering } from './types';
import { translatedItemType } from './utils';
import { getTranslatedObjectType, ObjectsWithConditionalRendering } from '../object';
import { GroupConditionVisibility } from './types';
interface Props {
itemType: ItemsWithConditionalRendering;
objectType: ObjectsWithConditionalRendering;
value: GroupConditionVisibility;
onChange: (value: GroupConditionVisibility) => void;
}
export const ConditionalRenderingGroupVisibility = ({ itemType, value, onChange }: Props) => {
const styles = useStyles2(getStyles);
export const ConditionalRenderingGroupVisibility = ({ objectType, value, onChange }: Props) => {
const options: Array<SelectableValue<GroupConditionVisibility>> = useMemo(
() => [
{ label: t('dashboard.conditional-rendering.conditions.group.visibility.show', 'Show'), value: 'show' },
@@ -29,17 +26,11 @@ export const ConditionalRenderingGroupVisibility = ({ itemType, value, onChange
return (
<Field
label={t('dashboard.conditional-rendering.conditions.group.visibility.label', '{{type}} visibility', {
type: capitalize(translatedItemType(itemType)),
type: getTranslatedObjectType(objectType),
})}
className={styles.container}
noMargin
>
<RadioButtonGroup fullWidth options={options} value={value} onChange={onChange} />
</Field>
);
};
const getStyles = () => ({
container: css({
margin: 0,
}),
});
@@ -0,0 +1,3 @@
export type GroupConditionVisibility = 'show' | 'hide';
export type GroupConditionCondition = 'and' | 'or';
export type GroupConditionConditionType = 'data' | 'timeRangeSize' | 'variable';
@@ -3,25 +3,20 @@ import { Icon, Stack, Tooltip } from '@grafana/ui';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { ConditionalRendering } from './ConditionalRendering';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
import { ConditionalRenderingGroup } from '../group/ConditionalRenderingGroup';
import { extractObjectType, getLowerTranslatedObjectType } from '../object';
let placeholderConditionalRendering: ConditionalRendering | undefined;
function getPlaceholderConditionalRendering(): ConditionalRendering {
let placeholderConditionalRendering: ConditionalRenderingGroup | undefined;
function getPlaceholderConditionalRendering(): ConditionalRenderingGroup {
if (!placeholderConditionalRendering) {
placeholderConditionalRendering = new ConditionalRendering({
rootGroup: new ConditionalRenderingGroup({
visibility: 'show',
condition: 'and',
value: [],
}),
});
placeholderConditionalRendering = ConditionalRenderingGroup.createEmpty();
}
return placeholderConditionalRendering;
}
export function useConditionalRenderingEditor(
conditionalRendering?: ConditionalRendering,
conditionalRendering?: ConditionalRenderingGroup,
disabledText?: string
): OptionsPaneCategoryDescriptor {
const title = t('dashboard.conditional-rendering.root.title', 'Show / hide rules');
@@ -34,22 +29,32 @@ export function useConditionalRenderingEditor(
? undefined
: (disabledText ??
t(
'dashboard.conditional-rendering.editor.unsupported-item-type',
'dashboard.conditional-rendering.editor.unsupported-object-type',
'Conditional rendering not supported for this item type'
)),
id: 'conditional-rendering-options',
renderTitle: () => (
<Stack direction="row" gap={1} alignItems="center">
<div>{title}</div>
{conditionalRendering ? (
<Tooltip content={conditionalRenderingToRender.info}>
<Icon name={!conditionalRenderingToRender.evaluate() ? 'eye-slash' : 'eye'} />
</Tooltip>
) : (
<Icon name="eye-slash" />
)}
</Stack>
),
renderTitle: () => {
const { result } = conditionalRenderingToRender.useState();
return (
<Stack direction="row" gap={1} alignItems="center">
<div>{title}</div>
{conditionalRendering ? (
<Tooltip
content={t(
'dashboard.conditional-rendering.editor.info',
'Set rules to control {{type}} visibility by matching any or all rules.',
{ type: getLowerTranslatedObjectType(extractObjectType(conditionalRenderingToRender.parent)) }
)}
>
<Icon name={!result ? 'eye-slash' : 'eye'} />
</Tooltip>
) : (
<Icon name="eye-slash" />
)}
</Stack>
);
},
isOpenDefault: true,
}).addItem(
new OptionsPaneItemDescriptor({
@@ -0,0 +1,33 @@
import { ReactNode } from 'react';
import { SceneObject, 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();
}
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, {
shouldActivateOrKeepAlive: true,
});
return [
!result,
result ? undefined : 'dashboard-visible-hidden-element',
result ? null : <ConditionalRenderingOverlay />,
renderHidden,
];
}
@@ -0,0 +1,62 @@
import { capitalize, lowerCase } from 'lodash';
import { t } from '@grafana/i18n';
import { SceneObject } from '@grafana/scenes';
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
import { RowItem } from '../scene/layout-rows/RowItem';
import { TabItem } from '../scene/layout-tabs/TabItem';
export type ObjectsWithConditionalRendering = 'panel' | 'row' | 'tab' | 'element';
const translatedObjectTypes = {
get panel() {
return t('dashboard.edit-pane.elements.panel', 'Panel');
},
get row() {
return t('dashboard.edit-pane.elements.row', 'Row');
},
get tab() {
return t('dashboard.edit-pane.elements.tab', 'Tab');
},
get element() {
return t('dashboard.edit-pane.elements.element', 'Element');
},
} as const;
const translatedObjectTypesLower: Record<ObjectsWithConditionalRendering, string> = {
get panel() {
return lowerCase(t('dashboard.edit-pane.elements.panel', 'Panel'));
},
get row() {
return lowerCase(t('dashboard.edit-pane.elements.row', 'Row'));
},
get tab() {
return lowerCase(t('dashboard.edit-pane.elements.tab', 'Tab'));
},
get element() {
return lowerCase(t('dashboard.edit-pane.elements.element', 'Element'));
},
} as const;
export function getTranslatedObjectType(type: ObjectsWithConditionalRendering): string {
return translatedObjectTypes[type] ?? capitalize(type);
}
export function getLowerTranslatedObjectType(type: ObjectsWithConditionalRendering): string {
return translatedObjectTypesLower[type] ?? type;
}
export function extractObjectType(object: SceneObject | undefined): ObjectsWithConditionalRendering {
if (!object) {
return 'element';
} else if (object instanceof AutoGridItem) {
return 'panel';
} else if (object instanceof RowItem) {
return 'row';
} else if (object instanceof TabItem) {
return 'tab';
}
return 'element';
}
@@ -1,16 +0,0 @@
import { Registry } from '@grafana/data';
import { ConditionalRenderingData } from './ConditionalRenderingData';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
import { ConditionalRenderingTimeRangeSize } from './ConditionalRenderingTimeRangeSize';
import { ConditionalRenderingVariable } from './ConditionalRenderingVariable';
import { ConditionalRenderingSerializerRegistryItem } from './types';
export const conditionalRenderingSerializerRegistry = new Registry<ConditionalRenderingSerializerRegistryItem>(() => {
return [
ConditionalRenderingGroup.serializer,
ConditionalRenderingVariable.serializer,
ConditionalRenderingData.serializer,
ConditionalRenderingTimeRangeSize.serializer,
];
});
@@ -1,53 +0,0 @@
import { RegistryItem } from '@grafana/data';
import {
ConditionalRenderingDataKind,
ConditionalRenderingGroupKind,
ConditionalRenderingTimeRangeSizeKind,
ConditionalRenderingVariableKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { ConditionalRenderingData } from './ConditionalRenderingData';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
import { ConditionalRenderingTimeRangeSize } from './ConditionalRenderingTimeRangeSize';
import { ConditionalRenderingVariable } from './ConditionalRenderingVariable';
export type ItemsWithConditionalRendering = 'panel' | 'row' | 'tab' | 'element';
export type DataConditionValue = boolean;
export type GroupConditionVisibility = 'show' | 'hide';
export type GroupConditionCondition = 'and' | 'or';
export type GroupConditionItemType = 'data' | 'timeRangeSize' | 'variable';
export type GroupConditionValue = ConditionalRenderingConditions[];
export type TimeRangeSizeConditionValue = string;
export type VariableConditionValueOperator = '=' | '!=' | '=~' | '!~';
export type VariableConditionValue = {
name: string;
operator: VariableConditionValueOperator;
value: string;
};
export type ConditionValues =
| DataConditionValue
| VariableConditionValue
| GroupConditionValue
| TimeRangeSizeConditionValue;
export type ConditionalRenderingConditions =
| ConditionalRenderingData
| ConditionalRenderingVariable
| ConditionalRenderingTimeRangeSize
| ConditionalRenderingGroup;
export type ConditionalRenderingKindTypes =
| ConditionalRenderingGroupKind
| ConditionalRenderingVariableKind
| ConditionalRenderingDataKind
| ConditionalRenderingTimeRangeSizeKind;
export interface ConditionalRenderingSerializerRegistryItem extends RegistryItem {
deserialize(model: ConditionalRenderingKindTypes): ConditionalRenderingConditions;
}
@@ -1,22 +0,0 @@
import { ReactNode } from 'react';
import { SceneObject } from '@grafana/scenes';
import { ConditionalRendering } from './ConditionalRendering';
import { ConditionalRenderingOverlay } from './ConditionalRenderingOverlay';
export function useIsConditionallyHidden(scene: SceneObject): [boolean, string | undefined, ReactNode | null] {
const state = scene.useState();
if (!('conditionalRendering' in state) || !(state.conditionalRendering instanceof ConditionalRendering)) {
return [false, undefined, null];
}
const value = state.conditionalRendering.evaluate() ?? true;
return [
!value,
value ? undefined : 'dashboard-visible-hidden-element',
value ? null : <ConditionalRenderingOverlay />,
];
}
@@ -1,33 +0,0 @@
import { lowerCase } from 'lodash';
import { t } from '@grafana/i18n';
import { SceneObject } from '@grafana/scenes';
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
import { RowItem } from '../scene/layout-rows/RowItem';
import { TabItem } from '../scene/layout-tabs/TabItem';
import { ItemsWithConditionalRendering } from './types';
export function getItemType(object: SceneObject): ItemsWithConditionalRendering {
if (object instanceof AutoGridItem) {
return 'panel';
} else if (object instanceof RowItem) {
return 'row';
} else if (object instanceof TabItem) {
return 'tab';
}
return 'element';
}
export const translatedItemType = (item: ItemsWithConditionalRendering) => {
const translations: { [key in ItemsWithConditionalRendering]: string } = {
panel: lowerCase(t('dashboard.edit-pane.elements.panel', 'Panel')),
row: lowerCase(t('dashboard.edit-pane.elements.row', 'Row')),
tab: lowerCase(t('dashboard.edit-pane.elements.tab', 'Tab')),
element: lowerCase(t('dashboard.edit-pane.elements.element', 'Element')),
};
return translations[item] || item;
};
@@ -15,10 +15,10 @@ import {
} from '@grafana/scenes';
import { createWorker } from 'app/features/dashboard-scene/saving/createDetectChangesWorker';
import { ConditionalRenderingData } from '../conditional-rendering/ConditionalRenderingData';
import { ConditionalRenderingGroup } from '../conditional-rendering/ConditionalRenderingGroup';
import { ConditionalRenderingTimeRangeSize } from '../conditional-rendering/ConditionalRenderingTimeRangeSize';
import { ConditionalRenderingVariable } from '../conditional-rendering/ConditionalRenderingVariable';
import { ConditionalRenderingData } from '../conditional-rendering/conditions/ConditionalRenderingData';
import { ConditionalRenderingTimeRangeSize } from '../conditional-rendering/conditions/ConditionalRenderingTimeRangeSize';
import { ConditionalRenderingVariable } from '../conditional-rendering/conditions/ConditionalRenderingVariable';
import { ConditionalRenderingGroup } from '../conditional-rendering/group/ConditionalRenderingGroup';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls';
import { DashboardScene, PERSISTED_PROPS } from '../scene/DashboardScene';
@@ -13,7 +13,7 @@ import {
} from '@grafana/scenes';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
import { ConditionalRenderingGroup } from '../../conditional-rendering/group/ConditionalRenderingGroup';
import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
import { getMultiVariableValues } from '../../utils/utils';
import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
@@ -29,7 +29,7 @@ export interface AutoGridItemState extends SceneObjectState {
repeatedPanels?: VizPanel[];
variableName?: string;
isHidden?: boolean;
conditionalRendering?: ConditionalRendering;
conditionalRendering?: ConditionalRenderingGroup;
}
export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements DashboardLayoutItem {
@@ -45,7 +45,7 @@ export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements
private _prevRepeatValues?: VariableValueSingle[];
public constructor(state: AutoGridItemState) {
super({ ...state, conditionalRendering: state?.conditionalRendering ?? ConditionalRendering.createEmpty() });
super({ ...state, conditionalRendering: state?.conditionalRendering ?? ConditionalRenderingGroup.createEmpty() });
this.addActivationHandler(() => this._activationHandler());
}
@@ -3,7 +3,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect';
import { useConditionalRenderingEditor } from '../../conditional-rendering/ConditionalRenderingEditor';
import { useConditionalRenderingEditor } from '../../conditional-rendering/hooks/useConditionalRenderingEditor';
import { dashboardEditActions } from '../../edit-pane/shared';
import { AutoGridItem } from './AutoGridItem';
@@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data/';
import { LazyLoader, SceneComponentProps, VizPanel } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
import { useIsConditionallyHidden } from '../../conditional-rendering/hooks/useIsConditionallyHidden';
import { useDashboardState } from '../../utils/utils';
import { renderMatchingSoloPanels, useSoloPanelContext } from '../SoloPanelContext';
import { getIsLazy } from '../layouts-shared/utils';
@@ -17,7 +17,7 @@ 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] =
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay, renderHidden] =
useIsConditionallyHidden(model);
const styles = useStyles2(getStyles);
const soloPanelContext = useSoloPanelContext();
@@ -39,53 +39,64 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
isDragged: boolean;
isDragging: boolean;
isRepeat?: boolean;
}) => (
<div
{...(addDndContainer
? { ref: model.containerRef, ['data-auto-grid-item-drop-target']: isDragging ? key : undefined }
: {})}
>
{isDragged && <div className={styles.draggedPlaceholder} />}
{isLazy ? (
<LazyLoader
key={item.state.key!}
className={cx(
conditionalRenderingClass,
styles.wrapper,
isDragged && !isRepeat && styles.draggedWrapper,
isDragged && isRepeat && styles.draggedRepeatWrapper
)}
>
<item.Component model={item} />
{conditionalRenderingOverlay}
</LazyLoader>
) : (
<div
className={cx(
conditionalRenderingClass,
styles.wrapper,
isDragged && !isRepeat && styles.draggedWrapper,
isDragged && isRepeat && styles.draggedRepeatWrapper
)}
>
<item.Component model={item} />
{conditionalRenderingOverlay}
</div>
)}
</div>
)
}) =>
isConditionallyHidden && !isEditing && !renderHidden ? null : (
<div
{...(addDndContainer
? { ref: model.containerRef, ['data-auto-grid-item-drop-target']: isDragging ? key : undefined }
: {})}
className={cx(isConditionallyHidden && !isEditing && styles.hidden)}
>
{isDragged && <div className={styles.draggedPlaceholder} />}
{
// The lazy loader causes issues when used with conditional rendering
isLazy && (!isConditionallyHidden || !renderHidden) ? (
<LazyLoader
key={item.state.key!}
className={cx(
conditionalRenderingClass,
styles.wrapper,
isDragged && !isRepeat && styles.draggedWrapper,
isDragged && isRepeat && styles.draggedRepeatWrapper
)}
>
<item.Component model={item} />
{conditionalRenderingOverlay}
</LazyLoader>
) : (
<div
className={cx(
conditionalRenderingClass,
styles.wrapper,
isDragged && !isRepeat && styles.draggedWrapper,
isDragged && isRepeat && styles.draggedRepeatWrapper
)}
>
<item.Component model={item} />
{conditionalRenderingOverlay}
</div>
)
}
</div>
)
),
[conditionalRenderingClass, conditionalRenderingOverlay, isLazy, key, model.containerRef, styles]
[
conditionalRenderingClass,
conditionalRenderingOverlay,
isLazy,
key,
model.containerRef,
styles,
isConditionallyHidden,
isEditing,
renderHidden,
]
);
if (soloPanelContext) {
return renderMatchingSoloPanels(soloPanelContext, [body, ...repeatedPanels]);
}
if (isConditionallyHidden && !isEditing) {
return null;
}
const isDragging = !!draggingKey;
const isDragged = draggingKey === key;
@@ -127,4 +138,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
background: `${theme.colors.primary.transparent}`,
zIndex: -1,
}),
hidden: css({
display: 'none',
}),
});
@@ -9,7 +9,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect';
import { useConditionalRenderingEditor } from '../../conditional-rendering/ConditionalRenderingEditor';
import { useConditionalRenderingEditor } from '../../conditional-rendering/hooks/useConditionalRenderingEditor';
import { dashboardEditActions } from '../../edit-pane/shared';
import { DashboardGridItem } from './DashboardGridItem';
@@ -17,7 +17,7 @@ import kbn from 'app/core/utils/kbn';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { ShowConfirmModalEvent } from 'app/types/events';
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
import { ConditionalRenderingGroup } from '../../conditional-rendering/group/ConditionalRenderingGroup';
import { serializeRow } from '../../serialization/layoutSerializers/RowsLayoutSerializer';
import { getElements } from '../../serialization/layoutSerializers/utils';
import { getDashboardSceneFor } from '../../utils/utils';
@@ -42,7 +42,7 @@ export interface RowItemState extends SceneObjectState {
hideHeader?: boolean;
fillScreen?: boolean;
isDropTarget?: boolean;
conditionalRendering?: ConditionalRendering;
conditionalRendering?: ConditionalRenderingGroup;
repeatByVariable?: string;
repeatedRows?: RowItem[];
/** Marks object as a repeated object and a key pointer to source object */
@@ -68,7 +68,7 @@ export class RowItem
...state,
title: state?.title ?? t('dashboard.rows-layout.row.new', 'New row'),
layout: state?.layout ?? AutoGridLayoutManager.createEmpty(),
conditionalRendering: state?.conditionalRendering ?? ConditionalRendering.createEmpty(),
conditionalRendering: state?.conditionalRendering ?? ConditionalRenderingGroup.createEmpty(),
});
this.addActivationHandler(() => this._activationHandler());
@@ -9,7 +9,7 @@ import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSel
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { useConditionalRenderingEditor } from '../../conditional-rendering/ConditionalRenderingEditor';
import { useConditionalRenderingEditor } from '../../conditional-rendering/hooks/useConditionalRenderingEditor';
import { dashboardEditActions } from '../../edit-pane/shared';
import { getQueryRunnerFor, useDashboard } from '../../utils/utils';
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
@@ -8,7 +8,7 @@ import { t } from '@grafana/i18n';
import { SceneComponentProps } from '@grafana/scenes';
import { clearButtonStyles, Icon, Tooltip, useElementSelection, usePointerDistance, useStyles2 } from '@grafana/ui';
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
import { useIsConditionallyHidden } from '../../conditional-rendering/hooks/useIsConditionallyHidden';
import { isRepeatCloneOrChildOf } from '../../utils/clone';
import { useDashboardState, useInterpolatedTitle } from '../../utils/utils';
import { DashboardScene } from '../DashboardScene';
@@ -17,7 +17,7 @@ import kbn from 'app/core/utils/kbn';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { ShowConfirmModalEvent } from 'app/types/events';
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
import { ConditionalRenderingGroup } from '../../conditional-rendering/group/ConditionalRenderingGroup';
import { serializeTab } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
import { getElements } from '../../serialization/layoutSerializers/utils';
import { getDashboardSceneFor, getDefaultVizPanel } from '../../utils/utils';
@@ -39,7 +39,7 @@ export interface TabItemState extends SceneObjectState {
layout: DashboardLayoutManager;
title?: string;
isDropTarget?: boolean;
conditionalRendering?: ConditionalRendering;
conditionalRendering?: ConditionalRenderingGroup;
repeatByVariable?: string;
repeatedTabs?: TabItem[];
/** Marks object as a repeated object and a key pointer to source object */
@@ -66,7 +66,7 @@ export class TabItem
...state,
title: state?.title ?? t('dashboard.tabs-layout.tab.new', 'New tab'),
layout: state?.layout ?? AutoGridLayoutManager.createEmpty(),
conditionalRendering: state?.conditionalRendering ?? ConditionalRendering.createEmpty(),
conditionalRendering: state?.conditionalRendering ?? ConditionalRenderingGroup.createEmpty(),
});
this.addActivationHandler(() => this._activationHandler());
@@ -9,7 +9,7 @@ import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSel
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { useConditionalRenderingEditor } from '../../conditional-rendering/ConditionalRenderingEditor';
import { useConditionalRenderingEditor } from '../../conditional-rendering/hooks/useConditionalRenderingEditor';
import { dashboardEditActions } from '../../edit-pane/shared';
import { getQueryRunnerFor, useDashboard } from '../../utils/utils';
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
@@ -7,7 +7,7 @@ import { t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { Box, Icon, Tab, TabContent, Tooltip, useElementSelection, usePointerDistance, useStyles2 } from '@grafana/ui';
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
import { useIsConditionallyHidden } from '../../conditional-rendering/hooks/useIsConditionallyHidden';
import { isRepeatCloneOrChildOf } from '../../utils/clone';
import { useDashboardState } from '../../utils/utils';
import { useSoloPanelContext } from '../SoloPanelContext';
@@ -23,9 +23,7 @@ import {
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
import { ConditionalRenderingGroup } from '../../conditional-rendering/ConditionalRenderingGroup';
import { conditionalRenderingSerializerRegistry } from '../../conditional-rendering/serializers';
import { ConditionalRenderingGroup } from '../../conditional-rendering/group/ConditionalRenderingGroup';
import { CustomTimeRangeCompare } from '../../scene/CustomTimeRangeCompare';
import { DashboardDatasourceBehaviour } from '../../scene/DashboardDatasourceBehaviour';
import { DashboardScene } from '../../scene/DashboardScene';
@@ -293,20 +291,12 @@ export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['
export function getConditionalRendering(
item: TabsLayoutTabKind | RowsLayoutRowKind | AutoGridLayoutItemKind
): ConditionalRendering {
): ConditionalRenderingGroup {
if (!item.spec.conditionalRendering) {
return ConditionalRendering.createEmpty();
return ConditionalRenderingGroup.createEmpty();
}
const rootGroup = conditionalRenderingSerializerRegistry
.get(item.spec.conditionalRendering.kind)
.deserialize(item.spec.conditionalRendering);
if (rootGroup && !(rootGroup instanceof ConditionalRenderingGroup)) {
throw new Error(`Conditional rendering must always start with a root group`);
}
return new ConditionalRendering({ rootGroup: rootGroup });
return ConditionalRenderingGroup.deserialize(item.spec.conditionalRendering);
}
export function getElements(layout: DashboardLayoutManager, scene: DashboardScene): DashboardV2Spec['elements'] {
+6 -7
View File
@@ -4506,17 +4506,12 @@
"any": "Match any",
"label": "Match rules"
},
"label": "Group",
"visibility": {
"hide": "Hide",
"label": "{{type}} visibility",
"show": "Show"
}
},
"shared": {
"delete-condition": "Delete Condition",
"unsupported-item-type": "This condition is not supported by the element, hence it will be ignored."
},
"time-range-size": {
"info": "Show or hide the {{type}} if the dashboard time range is shorter than the selected time frame.",
"invalid-message": "Invalid interval",
@@ -4545,17 +4540,21 @@
"not-matches": "Not matches"
},
"value": "Value"
},
"wrapper": {
"delete-condition": "Delete Condition",
"unsupported-object-type": "This condition is not supported by the element, hence it will be ignored."
}
},
"editor": {
"info": "Set rules to control {{type}} visibility by matching any or all rules.",
"not-supported-for-custom-grid": "Conditional rendering is not supported for the custom grid layout. Switch to auto grid to use conditional rendering.",
"unsupported-item-type": "Conditional rendering not supported for this item type"
"unsupported-object-type": "Conditional rendering not supported for this item type"
},
"overlay": {
"tooltip": "Element is hidden due to conditional rendering."
},
"root": {
"info": "Set rules to control {{type}} visibility by matching any or all rules.",
"title": "Show / hide rules"
}
},