Dashboard: Improvements to conditional rendering (#108108)

This commit is contained in:
Bogdan Matei
2025-08-12 16:23:19 +03:00
committed by GitHub
parent db0a1bc30f
commit 80e8a6e211
14 changed files with 164 additions and 64 deletions
@@ -941,7 +941,7 @@ ConditionalRenderingVariableKind: {
ConditionalRenderingVariableSpec: {
variable: string
operator: "equals" | "notEquals"
operator: "equals" | "notEquals" | "matches" | "notMatches"
value: string
}
@@ -942,7 +942,7 @@ ConditionalRenderingVariableKind: {
ConditionalRenderingVariableSpec: {
variable: string
operator: "equals" | "notEquals"
operator: "equals" | "notEquals" | "matches" | "notMatches"
value: string
}
@@ -945,7 +945,7 @@ ConditionalRenderingVariableKind: {
ConditionalRenderingVariableSpec: {
variable: string
operator: "equals" | "notEquals"
operator: "equals" | "notEquals" | "matches" | "notMatches"
value: string
}
@@ -1859,8 +1859,10 @@ const (
type DashboardConditionalRenderingVariableSpecOperator string
const (
DashboardConditionalRenderingVariableSpecOperatorEquals DashboardConditionalRenderingVariableSpecOperator = "equals"
DashboardConditionalRenderingVariableSpecOperatorNotEquals DashboardConditionalRenderingVariableSpecOperator = "notEquals"
DashboardConditionalRenderingVariableSpecOperatorEquals DashboardConditionalRenderingVariableSpecOperator = "equals"
DashboardConditionalRenderingVariableSpecOperatorNotEquals DashboardConditionalRenderingVariableSpecOperator = "notEquals"
DashboardConditionalRenderingVariableSpecOperatorMatches DashboardConditionalRenderingVariableSpecOperator = "matches"
DashboardConditionalRenderingVariableSpecOperatorNotMatches DashboardConditionalRenderingVariableSpecOperator = "notMatches"
)
// +k8s:openapi-gen=true
@@ -946,7 +946,7 @@ ConditionalRenderingVariableKind: {
ConditionalRenderingVariableSpec: {
variable: string
operator: "equals" | "notEquals"
operator: "equals" | "notEquals" | "matches" | "notMatches"
value: string
}
@@ -1884,8 +1884,10 @@ const (
type DashboardConditionalRenderingVariableSpecOperator string
const (
DashboardConditionalRenderingVariableSpecOperatorEquals DashboardConditionalRenderingVariableSpecOperator = "equals"
DashboardConditionalRenderingVariableSpecOperatorNotEquals DashboardConditionalRenderingVariableSpecOperator = "notEquals"
DashboardConditionalRenderingVariableSpecOperatorEquals DashboardConditionalRenderingVariableSpecOperator = "equals"
DashboardConditionalRenderingVariableSpecOperatorNotEquals DashboardConditionalRenderingVariableSpecOperator = "notEquals"
DashboardConditionalRenderingVariableSpecOperatorMatches DashboardConditionalRenderingVariableSpecOperator = "matches"
DashboardConditionalRenderingVariableSpecOperatorNotMatches DashboardConditionalRenderingVariableSpecOperator = "notMatches"
)
// +k8s:openapi-gen=true
@@ -934,7 +934,7 @@ ConditionalRenderingVariableKind: {
ConditionalRenderingVariableSpec: {
variable: string
operator: "equals" | "notEquals"
operator: "equals" | "notEquals" | "matches" | "notMatches"
value: string
}
@@ -700,7 +700,7 @@ export const defaultConditionalRenderingVariableKind = (): ConditionalRenderingV
export interface ConditionalRenderingVariableSpec {
variable: string;
operator: "equals" | "notEquals";
operator: "equals" | "notEquals" | "matches" | "notMatches";
value: string;
}
@@ -699,7 +699,7 @@ export const defaultConditionalRenderingVariableKind = (): ConditionalRenderingV
export interface ConditionalRenderingVariableSpec {
variable: string;
operator: "equals" | "notEquals";
operator: "equals" | "notEquals" | "matches" | "notMatches";
value: string;
}
@@ -70,15 +70,17 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase<Conditio
this.setStateAndNotify({ condition });
}
public createItem(itemType: GroupConditionItemType) {
const item =
itemType === 'data'
? ConditionalRenderingData.createEmpty()
: itemType === 'variable'
? ConditionalRenderingVariable.createEmpty(sceneGraph.getVariables(this).state.variables[0].state.name)
: ConditionalRenderingTimeRangeSize.createEmpty();
public createItem(itemType: GroupConditionItemType): ConditionalRenderingConditions {
switch (itemType) {
case 'data':
return ConditionalRenderingData.createEmpty();
return item;
case 'timeRangeSize':
return ConditionalRenderingTimeRangeSize.createEmpty();
case 'variable':
return ConditionalRenderingVariable.createEmpty(sceneGraph.getVariables(this).state.variables[0].state.name);
}
}
public addItem(item: ConditionalRenderingConditions) {
@@ -1,9 +1,12 @@
import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph, VariableDependencyConfig } from '@grafana/scenes';
import { ConditionalRenderingVariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Box, Combobox, ComboboxOption, Input, Stack } from '@grafana/ui';
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';
@@ -54,16 +57,29 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase<Condi
const variable = sceneGraph.getVariables(this).getByName(this.state.value.name);
if (!variable) {
return false;
return true;
}
const variableValue = variable.getValue() ?? '';
const hit = Array.isArray(variableValue)
? variableValue.includes(this.state.value.value)
: variableValue === this.state.value.value;
let hit: boolean;
return this.state.value.operator === '!=' ? !hit : hit;
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 {
@@ -71,7 +87,7 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase<Condi
kind: 'ConditionalRenderingVariable',
spec: {
variable: this.state.value.name,
operator: this.state.value.operator === '=' ? 'equals' : 'notEquals',
operator: this._getLongOperator(this.state.value.operator),
value: this.state.value.value,
},
};
@@ -81,7 +97,7 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase<Condi
return new ConditionalRenderingVariable({
value: {
name: model.spec.variable,
operator: model.spec.operator === 'equals' ? '=' : '!=',
operator: ConditionalRenderingVariable._getShortOperator(model.spec.operator),
value: model.spec.value,
},
});
@@ -90,14 +106,54 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase<Condi
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: Array<ComboboxOption<string>> = useMemo(
const variableNames: ComboboxOption[] = useMemo(
() => variables.state.variables.map((v) => ({ value: v.state.name, label: v.state.label ?? v.state.name })),
[variables.state.variables]
);
@@ -109,10 +165,31 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<Con
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 (
@@ -124,12 +201,14 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<Con
options={variableNames}
value={value.name}
onChange={(option) => {
dashboardEditActions.edit({
description: undoText,
source: model,
perform: () => model.setStateAndNotify({ value: { ...value, name: option.value } }),
undo: () => model.setStateAndNotify({ value: { ...value, name: value.name } }),
});
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>
@@ -140,28 +219,38 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<Con
options={operatorOptions}
value={value.operator}
onChange={(option) => {
dashboardEditActions.edit({
description: undoText,
source: model,
perform: () => model.setStateAndNotify({ value: { ...value, operator: option.value } }),
undo: () => model.setStateAndNotify({ value: { ...value, operator: value.operator } }),
});
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>
<Input
placeholder={t('dashboard.conditional-rendering.conditions.variable.value', 'Value')}
value={value.value}
onChange={(e) => {
const eventValue = e.currentTarget.value;
dashboardEditActions.edit({
description: undoText,
source: model,
perform: () => model.setStateAndNotify({ value: { ...value, value: eventValue } }),
undo: () => model.setStateAndNotify({ value: { ...value, value: value.value } }),
});
}}
/>
<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>
);
}
@@ -22,7 +22,7 @@ export type GroupConditionValue = ConditionalRenderingConditions[];
export type TimeRangeSizeConditionValue = string;
export type VariableConditionValueOperator = '=' | '!=';
export type VariableConditionValueOperator = '=' | '!=' | '=~' | '!~';
export type VariableConditionValue = {
name: string;
@@ -22,13 +22,6 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
const isLazy = useMemo(() => getIsLazy(preload), [preload]);
if (isConditionallyHidden && !isEditing) {
return null;
}
const isDragging = !!draggingKey;
const isDragged = draggingKey === key;
const Wrapper = useMemo(
() =>
memo(
@@ -69,6 +62,13 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
[conditionalRenderingClass, conditionalRenderingOverlay, isLazy, key, model.containerRef, styles]
);
if (isConditionallyHidden && !isEditing) {
return null;
}
const isDragging = !!draggingKey;
const isDragged = draggingKey === key;
return repeatedPanels ? (
<>
{repeatedPanels.map((item, index) => (
+6 -1
View File
@@ -4497,12 +4497,17 @@
}
},
"variable": {
"error": {
"invalid-regex": "Invalid regex"
},
"info": "Show or hide the {{type}} dynamically based on the variable value.",
"label": "Template variable",
"name": "Name",
"operator": {
"equals": "Equals",
"not-equals": "Not equals"
"matches": "Matches",
"not-equals": "Not equals",
"not-matches": "Not matches"
},
"value": "Value"
}