mirror of https://github.com/grafana/grafana
Dashboards: Add conditional rendering (#100330)
* Dashboards: Add conditional rendering * Updates * Fixes * Code improvements * Code improvements * limit condition choices, add delete and clean up ui * add basic variable condition * add conditional rendering based on time range interval * adjust failing test * remove deprecated pseudo locale file * extract conditional rendering from behaviour to state property * clean up behaviour initialisation * clean up ts errors * adjust data condition to account for RowItem * Fix subscribes * notify change when deleting condition * fix hidden row item error * address comments * subscribe to panel data change in data condition * Remove loop labels --------- Co-authored-by: Sergej-Vlasov <sergej.s.vlasov@gmail.com> Co-authored-by: oscarkilhed <oscar.kilhed@grafana.com>pull/102054/head^2
parent
2dca2503b9
commit
0f45f2696e
@ -0,0 +1,20 @@ |
||||
import { IconButton, Stack, Text } from '@grafana/ui'; |
||||
import { t } from 'app/core/internationalization'; |
||||
|
||||
type Props = { |
||||
title: string; |
||||
onDelete: () => void; |
||||
}; |
||||
|
||||
export const ConditionHeader = ({ title, onDelete }: Props) => { |
||||
return ( |
||||
<Stack direction="row" justifyContent="space-between"> |
||||
<Text variant="h6">{title}</Text> |
||||
<IconButton |
||||
aria-label={t('dashboard.conditional-rendering.shared.delete-condition', 'Delete Condition')} |
||||
name="trash-alt" |
||||
onClick={() => onDelete()} |
||||
/> |
||||
</Stack> |
||||
); |
||||
}; |
@ -0,0 +1,45 @@ |
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; |
||||
|
||||
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup'; |
||||
|
||||
export interface ConditionalRenderingState extends SceneObjectState { |
||||
rootGroup: ConditionalRenderingGroup; |
||||
} |
||||
|
||||
export class ConditionalRendering extends SceneObjectBase<ConditionalRenderingState> { |
||||
public static Component = ConditionalRenderingRenderer; |
||||
|
||||
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 in order 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(); |
||||
} |
||||
|
||||
public static createEmpty(): ConditionalRendering { |
||||
return new ConditionalRendering({ rootGroup: ConditionalRenderingGroup.createEmpty() }); |
||||
} |
||||
} |
||||
|
||||
function ConditionalRenderingRenderer({ model }: SceneComponentProps<ConditionalRendering>) { |
||||
const { rootGroup } = model.useState(); |
||||
|
||||
return <rootGroup.Component model={rootGroup} />; |
||||
} |
@ -0,0 +1,61 @@ |
||||
import { ReactNode } from 'react'; |
||||
|
||||
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; |
||||
|
||||
import { ConditionalRendering } from './ConditionalRendering'; |
||||
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup'; |
||||
import { ConditionValues } from './shared'; |
||||
|
||||
export interface ConditionalRenderingBaseState<V = ConditionValues> extends SceneObjectState { |
||||
value: V; |
||||
} |
||||
|
||||
export abstract class ConditionalRenderingBase< |
||||
S extends ConditionalRenderingBaseState<ConditionValues>, |
||||
> extends SceneObjectBase<S> { |
||||
public static Component = ConditionalRenderingBaseRenderer; |
||||
|
||||
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 in order to allow children to subscribe to variable changes etc.
|
||||
this.forEachChild((child) => { |
||||
if (!child.isActive) { |
||||
this._subs.add(child.activate()); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public abstract readonly title: string; |
||||
|
||||
public abstract evaluate(): boolean; |
||||
|
||||
public abstract render(): ReactNode; |
||||
|
||||
public abstract onDelete(): void; |
||||
|
||||
public getConditionalLogicRoot(): ConditionalRendering { |
||||
return sceneGraph.getAncestor(this, ConditionalRendering); |
||||
} |
||||
|
||||
public getRootGroup(): ConditionalRenderingGroup { |
||||
return this.getConditionalLogicRoot().state.rootGroup; |
||||
} |
||||
|
||||
public setStateAndNotify(state: Partial<S>) { |
||||
this.setState(state); |
||||
this.getConditionalLogicRoot().notifyChange(); |
||||
} |
||||
} |
||||
|
||||
function ConditionalRenderingBaseRenderer({ |
||||
model, |
||||
}: SceneComponentProps<ConditionalRenderingBase<ConditionalRenderingBaseState>>) { |
||||
return model.render(); |
||||
} |
@ -0,0 +1,139 @@ |
||||
import { ReactNode, useMemo } from 'react'; |
||||
|
||||
import { PanelData, SelectableValue } from '@grafana/data'; |
||||
import { SceneComponentProps, SceneDataProvider, sceneGraph } from '@grafana/scenes'; |
||||
import { RadioButtonGroup, Stack } from '@grafana/ui'; |
||||
import { t } from 'app/core/internationalization'; |
||||
|
||||
import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem'; |
||||
import { RowItem } from '../scene/layout-rows/RowItem'; |
||||
|
||||
import { ConditionHeader } from './ConditionHeader'; |
||||
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase'; |
||||
import { handleDeleteNonGroupCondition } from './shared'; |
||||
|
||||
export type DataConditionValue = boolean; |
||||
|
||||
type ConditionalRenderingDataState = ConditionalRenderingBaseState<DataConditionValue>; |
||||
|
||||
export class ConditionalRenderingData extends ConditionalRenderingBase<ConditionalRenderingDataState> { |
||||
public get title(): string { |
||||
return t('dashboard.conditional-rendering.data.label', 'Data'); |
||||
} |
||||
|
||||
public constructor(state: ConditionalRenderingDataState) { |
||||
super(state); |
||||
|
||||
this.addActivationHandler(() => this._activationHandler()); |
||||
} |
||||
|
||||
private _activationHandler() { |
||||
let panelDataProviders: SceneDataProvider[] = []; |
||||
const item = this.getConditionalLogicRoot().parent; |
||||
if (item instanceof ResponsiveGridItem) { |
||||
const panelData = sceneGraph.getData(item.state.body); |
||||
if (panelData) { |
||||
panelDataProviders.push(panelData); |
||||
} |
||||
} |
||||
// extract multiple panel data from RowItem
|
||||
if (item instanceof RowItem) { |
||||
const panels = item.getLayout().getVizPanels(); |
||||
for (const panel of panels) { |
||||
const panelData = sceneGraph.getData(panel); |
||||
if (panelData) { |
||||
panelDataProviders.push(panelData); |
||||
} |
||||
} |
||||
} |
||||
panelDataProviders.forEach((d) => { |
||||
this._subs.add( |
||||
d.subscribeToState(() => { |
||||
this.getConditionalLogicRoot().notifyChange(); |
||||
}) |
||||
); |
||||
}); |
||||
} |
||||
|
||||
public evaluate(): boolean { |
||||
const { value } = this.state; |
||||
|
||||
// enable/disable condition
|
||||
if (!value) { |
||||
return true; |
||||
} |
||||
|
||||
let data: PanelData[] = []; |
||||
|
||||
// get ResponsiveGridItem or RowItem
|
||||
const item = this.getConditionalLogicRoot().parent; |
||||
|
||||
// extract single panel data from ResponsiveGridItem
|
||||
if (item instanceof ResponsiveGridItem) { |
||||
const panelData = sceneGraph.getData(item.state.body).state.data; |
||||
if (panelData) { |
||||
data.push(panelData); |
||||
} |
||||
} |
||||
|
||||
// extract multiple panel data from RowItem
|
||||
if (item instanceof RowItem) { |
||||
const panels = item.getLayout().getVizPanels(); |
||||
for (const panel of panels) { |
||||
const panelData = sceneGraph.getData(panel).state.data; |
||||
if (panelData) { |
||||
data.push(panelData); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// early return if no panel data
|
||||
if (!data.length) { |
||||
return false; |
||||
} |
||||
|
||||
for (let panelDataIdx = 0; panelDataIdx < data.length; panelDataIdx++) { |
||||
const series = data[panelDataIdx]?.series ?? []; |
||||
|
||||
for (let seriesIdx = 0; seriesIdx < series.length; seriesIdx++) { |
||||
if (series[seriesIdx].length > 0) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
public render(): ReactNode { |
||||
return <ConditionalRenderingDataRenderer model={this} />; |
||||
} |
||||
|
||||
public onDelete() { |
||||
handleDeleteNonGroupCondition(this); |
||||
} |
||||
} |
||||
|
||||
function ConditionalRenderingDataRenderer({ model }: SceneComponentProps<ConditionalRenderingData>) { |
||||
const { value } = model.useState(); |
||||
|
||||
const enableConditionOptions: Array<SelectableValue<true | false>> = useMemo( |
||||
() => [ |
||||
{ label: t('dashboard.conditional-rendering.data.enable', 'Enable'), value: true }, |
||||
{ label: t('dashboard.conditional-rendering.data.disable', 'Disable'), value: false }, |
||||
], |
||||
[] |
||||
); |
||||
|
||||
return ( |
||||
<Stack direction="column"> |
||||
<ConditionHeader title={model.title} onDelete={() => model.onDelete()} /> |
||||
<RadioButtonGroup |
||||
fullWidth |
||||
options={enableConditionOptions} |
||||
value={value} |
||||
onChange={(value) => model.setStateAndNotify({ value: value })} |
||||
/> |
||||
</Stack> |
||||
); |
||||
} |
@ -0,0 +1,24 @@ |
||||
import { t } from 'app/core/internationalization'; |
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; |
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; |
||||
|
||||
import { ConditionalRendering } from './ConditionalRendering'; |
||||
|
||||
export function useConditionalRenderingEditor( |
||||
conditionalRendering?: ConditionalRendering |
||||
): OptionsPaneCategoryDescriptor | null { |
||||
if (!conditionalRendering) { |
||||
return null; |
||||
} |
||||
|
||||
return new OptionsPaneCategoryDescriptor({ |
||||
title: t('dashboard.conditional-rendering.title', 'Conditional rendering options'), |
||||
id: 'conditional-rendering-options', |
||||
isOpenDefault: true, |
||||
}).addItem( |
||||
new OptionsPaneItemDescriptor({ |
||||
title: t('dashboard.conditional-rendering.title', 'Conditional rendering options'), |
||||
render: () => <conditionalRendering.Component model={conditionalRendering} />, |
||||
}) |
||||
); |
||||
} |
@ -0,0 +1,159 @@ |
||||
import { css } from '@emotion/css'; |
||||
import { Fragment, ReactNode, useMemo } from 'react'; |
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; |
||||
import { SceneComponentProps } from '@grafana/scenes'; |
||||
import { Divider, Dropdown, Field, Menu, RadioButtonGroup, Stack, ToolbarButton, useStyles2 } from '@grafana/ui'; |
||||
import { t, Trans } from 'app/core/internationalization'; |
||||
|
||||
import { ConditionHeader } from './ConditionHeader'; |
||||
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase'; |
||||
import { ConditionalRenderingData } from './ConditionalRenderingData'; |
||||
import { ConditionalRenderingInterval } from './ConditionalRenderingInterval'; |
||||
import { ConditionalRenderingVariable } from './ConditionalRenderingVariable'; |
||||
import { ConditionalRenderingConditions } from './shared'; |
||||
|
||||
export type GroupConditionValue = ConditionalRenderingConditions[]; |
||||
export interface ConditionalRenderingGroupState extends ConditionalRenderingBaseState<GroupConditionValue> { |
||||
condition: 'and' | 'or'; |
||||
} |
||||
|
||||
export class ConditionalRenderingGroup extends ConditionalRenderingBase<ConditionalRenderingGroupState> { |
||||
public get title(): string { |
||||
return t('dashboard.conditional-rendering.group.label', 'Group'); |
||||
} |
||||
|
||||
public evaluate(): boolean { |
||||
if (this.state.value.length === 0) { |
||||
return true; |
||||
} |
||||
|
||||
if (this.state.condition === 'and') { |
||||
return this.state.value.every((entry) => entry.evaluate()); |
||||
} |
||||
|
||||
return this.state.value.some((entry) => entry.evaluate()); |
||||
} |
||||
|
||||
public render(): ReactNode { |
||||
return <ConditionalRenderingGroupRenderer model={this} />; |
||||
} |
||||
|
||||
public changeCondition(condition: 'and' | 'or') { |
||||
this.setStateAndNotify({ condition }); |
||||
} |
||||
|
||||
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.getConditionalLogicRoot().notifyChange(); |
||||
} |
||||
|
||||
public static createEmpty(): ConditionalRenderingGroup { |
||||
return new ConditionalRenderingGroup({ condition: 'and', value: [] }); |
||||
} |
||||
|
||||
public onDelete() { |
||||
const rootGroup = this.getRootGroup(); |
||||
if (this === rootGroup) { |
||||
this.getConditionalLogicRoot().setState({ rootGroup: ConditionalRenderingGroup.createEmpty() }); |
||||
} else { |
||||
rootGroup.setState({ value: rootGroup.state.value.filter((condition) => condition !== this) }); |
||||
} |
||||
this.getConditionalLogicRoot().notifyChange(); |
||||
} |
||||
} |
||||
|
||||
function ConditionalRenderingGroupRenderer({ model }: SceneComponentProps<ConditionalRenderingGroup>) { |
||||
const styles = useStyles2(getStyles); |
||||
const { condition, value } = model.useState(); |
||||
|
||||
const conditionsOptions: Array<SelectableValue<'and' | 'or'>> = useMemo( |
||||
() => [ |
||||
{ label: t('dashboard.conditional-rendering.group.condition.meet-all', 'Meet all'), value: 'and' }, |
||||
{ label: t('dashboard.conditional-rendering.group.condition.meet-any', 'Meet any'), value: 'or' }, |
||||
], |
||||
[] |
||||
); |
||||
|
||||
return ( |
||||
<Stack direction="column"> |
||||
<ConditionHeader title={model.title} onDelete={() => model.onDelete()} /> |
||||
<Field label={t('dashboard.conditional-rendering.group.condition.label', 'Evaluate conditions')}> |
||||
<RadioButtonGroup |
||||
fullWidth |
||||
options={conditionsOptions} |
||||
value={condition} |
||||
onChange={(value) => model.changeCondition(value!)} |
||||
/> |
||||
</Field> |
||||
|
||||
<Divider spacing={1} /> |
||||
|
||||
{value.map((entry) => ( |
||||
<Fragment key={entry!.state.key}> |
||||
{/* @ts-expect-error */} |
||||
<entry.Component model={entry} /> |
||||
|
||||
<div className={styles.entryDivider}> |
||||
<Divider spacing={1} /> |
||||
<p className={styles.entryDividerText}> {condition}</p> |
||||
<Divider spacing={1} /> |
||||
</div> |
||||
</Fragment> |
||||
))} |
||||
|
||||
<div className={styles.addButtonContainer}> |
||||
<Dropdown |
||||
overlay={ |
||||
<Menu> |
||||
<Menu.Item |
||||
label={t('dashboard.conditional-rendering.group.add.data', 'Data')} |
||||
onClick={() => model.addItem(new ConditionalRenderingData({ value: true }))} |
||||
/> |
||||
<Menu.Item |
||||
label={t('dashboard.conditional-rendering.group.add.interval', 'Interval')} |
||||
onClick={() => model.addItem(new ConditionalRenderingInterval({ value: '7d' }))} |
||||
/> |
||||
<Menu.Item |
||||
label={t('dashboard.conditional-rendering.group.add.variable', 'Variable value')} |
||||
onClick={() => |
||||
model.addItem(new ConditionalRenderingVariable({ value: { name: '', operator: '=', value: '' } })) |
||||
} |
||||
/> |
||||
</Menu> |
||||
} |
||||
> |
||||
<ToolbarButton icon="plus" iconSize="xs" variant="canvas"> |
||||
<Trans i18nKey="dashboard.conditional-rendering.group.add.button">Add condition based on</Trans> |
||||
</ToolbarButton> |
||||
</Dropdown> |
||||
</div> |
||||
</Stack> |
||||
); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
entryDivider: css({ |
||||
display: 'flex', |
||||
flexDirection: 'row', |
||||
alignItems: 'center', |
||||
}), |
||||
entryDividerText: css({ |
||||
margin: 0, |
||||
padding: theme.spacing(0, 2), |
||||
textTransform: 'capitalize', |
||||
}), |
||||
addButtonContainer: css({ |
||||
display: 'flex', |
||||
flexDirection: 'row', |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
}), |
||||
}); |
@ -0,0 +1,83 @@ |
||||
import { ReactNode, useState } from 'react'; |
||||
|
||||
import { rangeUtil } from '@grafana/data'; |
||||
import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; |
||||
import { Field, Input, Stack } from '@grafana/ui'; |
||||
import { t } from 'app/core/internationalization'; |
||||
|
||||
import { ConditionHeader } from './ConditionHeader'; |
||||
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase'; |
||||
import { handleDeleteNonGroupCondition } from './shared'; |
||||
|
||||
export type IntervalConditionValue = string; |
||||
type ConditionalRenderingIntervalState = ConditionalRenderingBaseState<IntervalConditionValue>; |
||||
|
||||
export class ConditionalRenderingInterval extends ConditionalRenderingBase<ConditionalRenderingIntervalState> { |
||||
public get title(): string { |
||||
return t('dashboard.conditional-rendering.interval.label', 'Time range interval'); |
||||
} |
||||
|
||||
public constructor(state: ConditionalRenderingIntervalState) { |
||||
super(state); |
||||
|
||||
this.addActivationHandler(() => this._activationHandler()); |
||||
} |
||||
|
||||
private _activationHandler() { |
||||
this._subs.add( |
||||
sceneGraph.getTimeRange(this).subscribeToState(() => { |
||||
this.getConditionalLogicRoot().notifyChange(); |
||||
}) |
||||
); |
||||
} |
||||
|
||||
public evaluate(): boolean { |
||||
try { |
||||
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; |
||||
} |
||||
} catch { |
||||
return false; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
public render(): ReactNode { |
||||
return <ConditionalRenderingIntervalRenderer model={this} />; |
||||
} |
||||
|
||||
public onDelete() { |
||||
handleDeleteNonGroupCondition(this); |
||||
} |
||||
} |
||||
|
||||
function ConditionalRenderingIntervalRenderer({ model }: SceneComponentProps<ConditionalRenderingInterval>) { |
||||
const { value } = model.useState(); |
||||
const [isValid, setIsValid] = useState(validateIntervalRegex.test(value)); |
||||
|
||||
return ( |
||||
<Stack direction="column"> |
||||
<ConditionHeader title={model.title} onDelete={() => model.onDelete()} /> |
||||
<Field |
||||
invalid={!isValid} |
||||
error={t('dashboard.conditional-rendering.interval.invalid-message', 'Invalid interval')} |
||||
label={t('dashboard.conditional-rendering.interval.input-label', 'Value')} |
||||
> |
||||
<Input |
||||
value={value} |
||||
onChange={(e) => { |
||||
setIsValid(validateIntervalRegex.test(e.currentTarget.value)); |
||||
model.setStateAndNotify({ value: e.currentTarget.value }); |
||||
}} |
||||
/> |
||||
</Field> |
||||
</Stack> |
||||
); |
||||
} |
||||
|
||||
export const validateIntervalRegex = /^(-?\d+(?:\.\d+)?)(ms|[Mwdhmsy])?$/; |
@ -0,0 +1,126 @@ |
||||
import { css } from '@emotion/css'; |
||||
import { ReactNode, useMemo } from 'react'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { SceneComponentProps, sceneGraph, VariableDependencyConfig } from '@grafana/scenes'; |
||||
import { Combobox, ComboboxOption, Field, Input, Stack, useStyles2 } from '@grafana/ui'; |
||||
import { t } from 'app/core/internationalization'; |
||||
|
||||
import { ConditionHeader } from './ConditionHeader'; |
||||
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase'; |
||||
import { handleDeleteNonGroupCondition } from './shared'; |
||||
|
||||
export type VariableConditionValue = { |
||||
name: string; |
||||
operator: '=' | '!='; |
||||
value: string; |
||||
}; |
||||
|
||||
type ConditionalRenderingVariableState = ConditionalRenderingBaseState<VariableConditionValue>; |
||||
|
||||
export class ConditionalRenderingVariable extends ConditionalRenderingBase<ConditionalRenderingVariableState> { |
||||
public get title(): string { |
||||
return t('dashboard.conditional-rendering.variable.label', 'Variable'); |
||||
} |
||||
|
||||
protected _variableDependency = new VariableDependencyConfig(this, { |
||||
onAnyVariableChanged: (v) => { |
||||
if (v.state.name === this.state.value.name) { |
||||
this.getConditionalLogicRoot().notifyChange(); |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
public evaluate(): boolean { |
||||
if (!this.state.value.name) { |
||||
return true; |
||||
} |
||||
const variable = sceneGraph.getVariables(this).state.variables.find((v) => v.state.name === this.state.value.name); |
||||
|
||||
// name is defined but no variable found - return false
|
||||
if (!variable) { |
||||
return false; |
||||
} |
||||
|
||||
const value = variable.getValue(); |
||||
|
||||
let hit = Array.isArray(value) ? value.includes(this.state.value.value) : value === this.state.value.value; |
||||
|
||||
if (this.state.value.operator === '!=') { |
||||
hit = !hit; |
||||
} |
||||
|
||||
return hit; |
||||
} |
||||
|
||||
public render(): ReactNode { |
||||
return <ConditionalRenderingVariableRenderer model={this} />; |
||||
} |
||||
|
||||
public onDelete() { |
||||
handleDeleteNonGroupCondition(this); |
||||
} |
||||
} |
||||
|
||||
function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<ConditionalRenderingVariable>) { |
||||
const variables = useMemo(() => sceneGraph.getVariables(model), [model]); |
||||
const variableNames = useMemo( |
||||
() => variables.state.variables.map((v) => ({ value: v.state.name, label: v.state.label ?? v.state.name })), |
||||
[variables.state.variables] |
||||
); |
||||
const operatorOptions: Array<ComboboxOption<'=' | '!='>> = useMemo( |
||||
() => [ |
||||
{ value: '=', description: t('dashboard.conditional-rendering.variable.operator.equals', 'Equals') }, |
||||
{ value: '!=', description: t('dashboard.conditional-rendering.variable.operator.not-equal', 'Not equal') }, |
||||
], |
||||
[] |
||||
); |
||||
const { value } = model.useState(); |
||||
|
||||
const styles = useStyles2(getStyles); |
||||
|
||||
return ( |
||||
<Stack direction="column"> |
||||
<ConditionHeader title={model.title} onDelete={() => model.onDelete()} /> |
||||
<Stack direction="column"> |
||||
<Stack direction="row" gap={0.5} grow={1}> |
||||
<Field |
||||
label={t('dashboard.conditional-rendering.variable.select-variable', 'Select variable')} |
||||
className={styles.variableNameSelect} |
||||
> |
||||
<Combobox |
||||
options={variableNames} |
||||
value={value.name} |
||||
onChange={(option) => model.setStateAndNotify({ value: { ...value, name: option.value } })} |
||||
/> |
||||
</Field> |
||||
<Field |
||||
label={t('dashboard.conditional-rendering.variable.select-operator', 'Operator')} |
||||
className={styles.operatorSelect} |
||||
> |
||||
<Combobox |
||||
options={operatorOptions} |
||||
value={value.operator} |
||||
onChange={(option) => model.setStateAndNotify({ value: { ...value, operator: option.value } })} |
||||
/> |
||||
</Field> |
||||
</Stack> |
||||
<Field label={t('dashboard.conditional-rendering.variable.value-input', 'Value')}> |
||||
<Input |
||||
value={value.value} |
||||
onChange={(e) => model.setStateAndNotify({ value: { ...value, value: e.currentTarget.value } })} |
||||
/> |
||||
</Field> |
||||
</Stack> |
||||
</Stack> |
||||
); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
variableNameSelect: css({ |
||||
flexGrow: 1, |
||||
}), |
||||
operatorSelect: css({ |
||||
width: theme.spacing(12), |
||||
}), |
||||
}); |
@ -0,0 +1,25 @@ |
||||
import { ConditionalRenderingData, DataConditionValue } from './ConditionalRenderingData'; |
||||
import { ConditionalRenderingGroup, GroupConditionValue } from './ConditionalRenderingGroup'; |
||||
import { ConditionalRenderingInterval, IntervalConditionValue } from './ConditionalRenderingInterval'; |
||||
import { ConditionalRenderingVariable, VariableConditionValue } from './ConditionalRenderingVariable'; |
||||
|
||||
export type ConditionValues = |
||||
| DataConditionValue |
||||
| VariableConditionValue |
||||
| GroupConditionValue |
||||
| IntervalConditionValue; |
||||
|
||||
export type ConditionalRenderingConditions = |
||||
| ConditionalRenderingData |
||||
| ConditionalRenderingVariable |
||||
| ConditionalRenderingInterval |
||||
| ConditionalRenderingGroup; |
||||
|
||||
type NonGroupConditions = Exclude<ConditionalRenderingConditions, ConditionalRenderingGroup>; |
||||
|
||||
export const handleDeleteNonGroupCondition = (model: NonGroupConditions) => { |
||||
if (model.parent instanceof ConditionalRenderingGroup) { |
||||
model.parent.setState({ value: model.parent.state.value.filter((condition) => condition !== model) }); |
||||
model.getConditionalLogicRoot().notifyChange(); |
||||
} |
||||
}; |
@ -1,54 +1,9 @@ |
||||
import { Switch } from '@grafana/ui'; |
||||
import { t } from 'app/core/internationalization'; |
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; |
||||
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 { ResponsiveGridItem } from './ResponsiveGridItem'; |
||||
|
||||
export function getOptions(model: ResponsiveGridItem): OptionsPaneCategoryDescriptor { |
||||
const category = new OptionsPaneCategoryDescriptor({ |
||||
title: t('dashboard.responsive-layout.item-options.title', 'Layout options'), |
||||
id: 'layout-options', |
||||
isOpenDefault: false, |
||||
}); |
||||
|
||||
category.addItem( |
||||
new OptionsPaneItemDescriptor({ |
||||
title: t('dashboard.responsive-layout.item-options.hide-no-data', 'Hide when no data'), |
||||
render: () => <GridItemNoDataToggle item={model} />, |
||||
}) |
||||
); |
||||
|
||||
category.addItem( |
||||
new OptionsPaneItemDescriptor({ |
||||
title: t('dashboard.responsive-layout.item-options.repeat.variable.title', 'Repeat by variable'), |
||||
description: t( |
||||
'dashboard.responsive-layout.item-options.repeat.variable.description', |
||||
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.' |
||||
), |
||||
render: () => <RepeatByOption item={model} />, |
||||
}) |
||||
); |
||||
|
||||
return category; |
||||
} |
||||
|
||||
function GridItemNoDataToggle({ item }: { item: ResponsiveGridItem }) { |
||||
const { hideWhenNoData } = item.useState(); |
||||
|
||||
return <Switch value={hideWhenNoData} id="hide-when-no-data" onChange={() => item.toggleHideWhenNoData()} />; |
||||
} |
||||
|
||||
function RepeatByOption({ item }: { item: ResponsiveGridItem }) { |
||||
const { variableName } = item.useState(); |
||||
|
||||
return ( |
||||
<RepeatRowSelect2 |
||||
id="repeat-by-variable-select" |
||||
sceneContext={item} |
||||
repeat={variableName} |
||||
onChange={(value?: string) => item.setRepeatByVariable(value)} |
||||
/> |
||||
); |
||||
return useConditionalRenderingEditor(model.state.conditionalRendering)!; |
||||
} |
||||
|
Loading…
Reference in new issue