Dashboards: Refactor conditional rendering (#108596)
This commit is contained in:
@@ -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
|
||||
|
||||
+409
@@ -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);
|
||||
}
|
||||
-160
@@ -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>
|
||||
);
|
||||
}
|
||||
-141
@@ -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 }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
-204
@@ -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>
|
||||
);
|
||||
}
|
||||
-256
@@ -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>
|
||||
);
|
||||
}
|
||||
+64
@@ -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>
|
||||
);
|
||||
}
|
||||
+196
@@ -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>
|
||||
);
|
||||
}
|
||||
+80
-59
@@ -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]$/;
|
||||
+318
@@ -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();
|
||||
}
|
||||
+240
@@ -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>
|
||||
);
|
||||
}
|
||||
+10
-8
@@ -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
|
||||
+2
-14
@@ -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,
|
||||
}),
|
||||
});
|
||||
+8
-17
@@ -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';
|
||||
+30
-25
@@ -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({
|
||||
+33
@@ -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;
|
||||
}
|
||||
-22
@@ -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';
|
||||
|
||||
+56
-42
@@ -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',
|
||||
}),
|
||||
});
|
||||
|
||||
+1
-1
@@ -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'] {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user