Dynamic dashboards: Persist conditional rendering (#102022)

* 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

* persist-conditional-rendering

* fix group value name and kind type

* Fix types in base

* minor style fix

* Fix subscribes

* notify change when deleting condition

* fix hidden row item error

* Remove option to have groups in groups

* fix merge issue

* address comments

* subscribe to panel data change in data condition

* Remove loop labels

* only persist conditional rendering if root group has items

* update backend types

* Serialize variable conditional rendering operator as equals notEquals

---------

Co-authored-by: Bogdan Matei <bogdan.matei@grafana.com>
Co-authored-by: Sergej-Vlasov <sergej.s.vlasov@gmail.com>
pull/92618/merge
Oscar Kilhed 4 months ago committed by GitHub
parent 8fd2a12670
commit d07b1851c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 41
      apps/dashboard/kinds/v2alpha1/dashboard_spec.cue
  2. 203
      apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go
  3. 41
      packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue
  4. 80
      packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts
  5. 80
      packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts
  6. 5
      public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx
  7. 3
      public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingBase.tsx
  8. 10
      public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx
  9. 16
      public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx
  10. 10
      public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingInterval.tsx
  11. 50
      public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx
  12. 96
      public/app/features/dashboard-scene/conditional-rendering/serializers.ts
  13. 9
      public/app/features/dashboard-scene/serialization/layoutSerializers/ResponsiveGridLayoutSerializer.ts
  14. 10
      public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts
  15. 20
      public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts

@ -564,6 +564,7 @@ RowsLayoutRowKind: {
RowsLayoutRowSpec: {
title?: string
collapsed: bool
conditionalRendering?: ConditionalRenderingGroupKind
repeat?: RowRepeatOptions
layout: GridLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind
}
@ -587,6 +588,7 @@ ResponsiveGridLayoutItemKind: {
ResponsiveGridLayoutItemSpec: {
element: ElementReference
repeat?: ResponsiveGridRepeatOptions
conditionalRendering?: ConditionalRenderingGroupKind
}
TabsLayoutKind: {
@ -921,3 +923,42 @@ AdhocVariableKind: {
kind: "AdhocVariable"
spec: AdhocVariableSpec
}
ConditionalRenderingGroupKind: {
kind: "ConditionalRenderingGroup"
spec: ConditionalRenderingGroupSpec
}
ConditionalRenderingGroupSpec: {
condition: "and" | "or"
items: [...ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeIntervalKind]
}
ConditionalRenderingVariableKind: {
kind: "ConditionalRenderingVariable"
spec: ConditionalRenderingVariableSpec
}
ConditionalRenderingVariableSpec: {
variable: string
operator: "equals" | "notEquals"
value: string
}
ConditionalRenderingDataKind: {
kind: "ConditionalRenderingData"
spec: ConditionalRenderingDataSpec
}
ConditionalRenderingDataSpec: {
value: bool
}
ConditionalRenderingTimeIntervalKind: {
kind: "ConditionalRenderingTimeInterval"
spec: ConditionalRenderingTimeIntervalSpec
}
ConditionalRenderingTimeIntervalSpec: {
value: string
}

@ -833,10 +833,11 @@ func NewDashboardRowsLayoutRowKind() *DashboardRowsLayoutRowKind {
// +k8s:openapi-gen=true
type DashboardRowsLayoutRowSpec struct {
Title *string `json:"title,omitempty"`
Collapsed bool `json:"collapsed"`
Repeat *DashboardRowRepeatOptions `json:"repeat,omitempty"`
Layout DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind `json:"layout"`
Title *string `json:"title,omitempty"`
Collapsed bool `json:"collapsed"`
ConditionalRendering *DashboardConditionalRenderingGroupKind `json:"conditionalRendering,omitempty"`
Repeat *DashboardRowRepeatOptions `json:"repeat,omitempty"`
Layout DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKind `json:"layout"`
}
// NewDashboardRowsLayoutRowSpec creates a new DashboardRowsLayoutRowSpec object.
@ -846,6 +847,105 @@ func NewDashboardRowsLayoutRowSpec() *DashboardRowsLayoutRowSpec {
}
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingGroupKind struct {
Kind string `json:"kind"`
Spec DashboardConditionalRenderingGroupSpec `json:"spec"`
}
// NewDashboardConditionalRenderingGroupKind creates a new DashboardConditionalRenderingGroupKind object.
func NewDashboardConditionalRenderingGroupKind() *DashboardConditionalRenderingGroupKind {
return &DashboardConditionalRenderingGroupKind{
Kind: "ConditionalRenderingGroup",
Spec: *NewDashboardConditionalRenderingGroupSpec(),
}
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingGroupSpec struct {
Condition DashboardConditionalRenderingGroupSpecCondition `json:"condition"`
Items []DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind `json:"items"`
}
// NewDashboardConditionalRenderingGroupSpec creates a new DashboardConditionalRenderingGroupSpec object.
func NewDashboardConditionalRenderingGroupSpec() *DashboardConditionalRenderingGroupSpec {
return &DashboardConditionalRenderingGroupSpec{}
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingVariableKind struct {
Kind string `json:"kind"`
Spec DashboardConditionalRenderingVariableSpec `json:"spec"`
}
// NewDashboardConditionalRenderingVariableKind creates a new DashboardConditionalRenderingVariableKind object.
func NewDashboardConditionalRenderingVariableKind() *DashboardConditionalRenderingVariableKind {
return &DashboardConditionalRenderingVariableKind{
Kind: "ConditionalRenderingVariable",
Spec: *NewDashboardConditionalRenderingVariableSpec(),
}
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingVariableSpec struct {
Variable string `json:"variable"`
Operator DashboardConditionalRenderingVariableSpecOperator `json:"operator"`
Value string `json:"value"`
}
// NewDashboardConditionalRenderingVariableSpec creates a new DashboardConditionalRenderingVariableSpec object.
func NewDashboardConditionalRenderingVariableSpec() *DashboardConditionalRenderingVariableSpec {
return &DashboardConditionalRenderingVariableSpec{}
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingDataKind struct {
Kind string `json:"kind"`
Spec DashboardConditionalRenderingDataSpec `json:"spec"`
}
// NewDashboardConditionalRenderingDataKind creates a new DashboardConditionalRenderingDataKind object.
func NewDashboardConditionalRenderingDataKind() *DashboardConditionalRenderingDataKind {
return &DashboardConditionalRenderingDataKind{
Kind: "ConditionalRenderingData",
Spec: *NewDashboardConditionalRenderingDataSpec(),
}
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingDataSpec struct {
Value bool `json:"value"`
}
// NewDashboardConditionalRenderingDataSpec creates a new DashboardConditionalRenderingDataSpec object.
func NewDashboardConditionalRenderingDataSpec() *DashboardConditionalRenderingDataSpec {
return &DashboardConditionalRenderingDataSpec{}
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingTimeIntervalKind struct {
Kind string `json:"kind"`
Spec DashboardConditionalRenderingTimeIntervalSpec `json:"spec"`
}
// NewDashboardConditionalRenderingTimeIntervalKind creates a new DashboardConditionalRenderingTimeIntervalKind object.
func NewDashboardConditionalRenderingTimeIntervalKind() *DashboardConditionalRenderingTimeIntervalKind {
return &DashboardConditionalRenderingTimeIntervalKind{
Kind: "ConditionalRenderingTimeInterval",
Spec: *NewDashboardConditionalRenderingTimeIntervalSpec(),
}
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingTimeIntervalSpec struct {
Value string `json:"value"`
}
// NewDashboardConditionalRenderingTimeIntervalSpec creates a new DashboardConditionalRenderingTimeIntervalSpec object.
func NewDashboardConditionalRenderingTimeIntervalSpec() *DashboardConditionalRenderingTimeIntervalSpec {
return &DashboardConditionalRenderingTimeIntervalSpec{}
}
// +k8s:openapi-gen=true
type DashboardResponsiveGridLayoutKind struct {
Kind string `json:"kind"`
@ -888,8 +988,9 @@ func NewDashboardResponsiveGridLayoutItemKind() *DashboardResponsiveGridLayoutIt
// +k8s:openapi-gen=true
type DashboardResponsiveGridLayoutItemSpec struct {
Element DashboardElementReference `json:"element"`
Repeat *DashboardResponsiveGridRepeatOptions `json:"repeat,omitempty"`
Element DashboardElementReference `json:"element"`
Repeat *DashboardResponsiveGridRepeatOptions `json:"repeat,omitempty"`
ConditionalRendering *DashboardConditionalRenderingGroupKind `json:"conditionalRendering,omitempty"`
}
// NewDashboardResponsiveGridLayoutItemSpec creates a new DashboardResponsiveGridLayoutItemSpec object.
@ -1675,6 +1776,22 @@ const (
DashboardRepeatOptionsDirectionV DashboardRepeatOptionsDirection = "v"
)
// +k8s:openapi-gen=true
type DashboardConditionalRenderingGroupSpecCondition string
const (
DashboardConditionalRenderingGroupSpecConditionAnd DashboardConditionalRenderingGroupSpecCondition = "and"
DashboardConditionalRenderingGroupSpecConditionOr DashboardConditionalRenderingGroupSpecCondition = "or"
)
// +k8s:openapi-gen=true
type DashboardConditionalRenderingVariableSpecOperator string
const (
DashboardConditionalRenderingVariableSpecOperatorEquals DashboardConditionalRenderingVariableSpecOperator = "equals"
DashboardConditionalRenderingVariableSpecOperatorNotEquals DashboardConditionalRenderingVariableSpecOperator = "notEquals"
)
// +k8s:openapi-gen=true
type DashboardTimeSettingsSpecWeekStart string
@ -1968,6 +2085,80 @@ func (resource *DashboardGridLayoutKindOrResponsiveGridLayoutKindOrTabsLayoutKin
return fmt.Errorf("could not unmarshal resource with `kind = %v`", discriminator)
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind struct {
ConditionalRenderingVariableKind *DashboardConditionalRenderingVariableKind `json:"ConditionalRenderingVariableKind,omitempty"`
ConditionalRenderingDataKind *DashboardConditionalRenderingDataKind `json:"ConditionalRenderingDataKind,omitempty"`
ConditionalRenderingTimeIntervalKind *DashboardConditionalRenderingTimeIntervalKind `json:"ConditionalRenderingTimeIntervalKind,omitempty"`
}
// NewDashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind creates a new DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind object.
func NewDashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind() *DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind {
return &DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind{}
}
// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind` as JSON.
func (resource DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind) MarshalJSON() ([]byte, error) {
if resource.ConditionalRenderingVariableKind != nil {
return json.Marshal(resource.ConditionalRenderingVariableKind)
}
if resource.ConditionalRenderingDataKind != nil {
return json.Marshal(resource.ConditionalRenderingDataKind)
}
if resource.ConditionalRenderingTimeIntervalKind != nil {
return json.Marshal(resource.ConditionalRenderingTimeIntervalKind)
}
return nil, fmt.Errorf("no value for disjunction of refs")
}
// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind` from JSON.
func (resource *DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind) UnmarshalJSON(raw []byte) error {
if raw == nil {
return nil
}
// FIXME: this is wasteful, we need to find a more efficient way to unmarshal this.
parsedAsMap := make(map[string]interface{})
if err := json.Unmarshal(raw, &parsedAsMap); err != nil {
return err
}
discriminator, found := parsedAsMap["kind"]
if !found {
return errors.New("discriminator field 'kind' not found in payload")
}
switch discriminator {
case "ConditionalRenderingData":
var dashboardConditionalRenderingDataKind DashboardConditionalRenderingDataKind
if err := json.Unmarshal(raw, &dashboardConditionalRenderingDataKind); err != nil {
return err
}
resource.ConditionalRenderingDataKind = &dashboardConditionalRenderingDataKind
return nil
case "ConditionalRenderingTimeInterval":
var dashboardConditionalRenderingTimeIntervalKind DashboardConditionalRenderingTimeIntervalKind
if err := json.Unmarshal(raw, &dashboardConditionalRenderingTimeIntervalKind); err != nil {
return err
}
resource.ConditionalRenderingTimeIntervalKind = &dashboardConditionalRenderingTimeIntervalKind
return nil
case "ConditionalRenderingVariable":
var dashboardConditionalRenderingVariableKind DashboardConditionalRenderingVariableKind
if err := json.Unmarshal(raw, &dashboardConditionalRenderingVariableKind); err != nil {
return err
}
resource.ConditionalRenderingVariableKind = &dashboardConditionalRenderingVariableKind
return nil
}
return fmt.Errorf("could not unmarshal resource with `kind = %v`", discriminator)
}
// +k8s:openapi-gen=true
type DashboardGridLayoutKindOrRowsLayoutKindOrResponsiveGridLayoutKind struct {
GridLayoutKind *DashboardGridLayoutKind `json:"GridLayoutKind,omitempty"`

@ -553,6 +553,7 @@ RowsLayoutRowSpec: {
title?: string
collapsed: bool
repeat?: RowRepeatOptions
conditionalRendering?: ConditionalRenderingGroupKind
layout: GridLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind
}
@ -575,6 +576,7 @@ ResponsiveGridLayoutItemKind: {
ResponsiveGridLayoutItemSpec: {
element: ElementReference
repeat?: ResponsiveGridRepeatOptions
conditionalRendering?: ConditionalRenderingGroupKind
}
TabsLayoutKind: {
@ -913,3 +915,42 @@ AdhocVariableKind: {
kind: "AdhocVariable"
spec: AdhocVariableSpec
}
ConditionalRenderingGroupKind: {
kind: "ConditionalRenderingGroup"
spec: ConditionalRenderingGroupSpec
}
ConditionalRenderingGroupSpec: {
condition: "and" | "or"
items: [...ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeIntervalKind]
}
ConditionalRenderingVariableKind: {
kind: "ConditionalRenderingVariable"
spec: ConditionalRenderingVariableSpec
}
ConditionalRenderingVariableSpec: {
variable: string
operator: "equals" | "notEquals"
value: string
}
ConditionalRenderingDataKind: {
kind: "ConditionalRenderingData"
spec: ConditionalRenderingDataSpec
}
ConditionalRenderingDataSpec: {
value: bool
}
ConditionalRenderingTimeIntervalKind: {
kind: "ConditionalRenderingTimeInterval"
spec: ConditionalRenderingTimeIntervalSpec
}
ConditionalRenderingTimeIntervalSpec: {
value: string
}

@ -825,6 +825,7 @@ export interface RowsLayoutRowSpec {
title?: string;
collapsed: boolean;
repeat?: RowRepeatOptions;
conditionalRendering?: ConditionalRenderingGroupKind;
layout: GridLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind;
}
@ -868,6 +869,7 @@ export const defaultResponsiveGridLayoutItemKind = (): ResponsiveGridLayoutItemK
export interface ResponsiveGridLayoutItemSpec {
element: ElementReference;
repeat?: ResponsiveGridRepeatOptions;
conditionalRendering?: ConditionalRenderingGroupKind;
}
export const defaultResponsiveGridLayoutItemSpec = (): ResponsiveGridLayoutItemSpec => ({
@ -1401,3 +1403,81 @@ export const defaultAdhocVariableKind = (): AdhocVariableKind => ({
spec: defaultAdhocVariableSpec(),
});
export interface ConditionalRenderingGroupKind {
kind: "ConditionalRenderingGroup";
spec: ConditionalRenderingGroupSpec;
}
export const defaultConditionalRenderingGroupKind = (): ConditionalRenderingGroupKind => ({
kind: "ConditionalRenderingGroup",
spec: defaultConditionalRenderingGroupSpec(),
});
export interface ConditionalRenderingGroupSpec {
condition: "and" | "or";
items: (ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeIntervalKind)[];
}
export const defaultConditionalRenderingGroupSpec = (): ConditionalRenderingGroupSpec => ({
condition: "and",
items: [],
});
export interface ConditionalRenderingVariableKind {
kind: "ConditionalRenderingVariable";
spec: ConditionalRenderingVariableSpec;
}
export const defaultConditionalRenderingVariableKind = (): ConditionalRenderingVariableKind => ({
kind: "ConditionalRenderingVariable",
spec: defaultConditionalRenderingVariableSpec(),
});
export interface ConditionalRenderingVariableSpec {
variable: string;
operator: "equals" | "notEquals";
value: string;
}
export const defaultConditionalRenderingVariableSpec = (): ConditionalRenderingVariableSpec => ({
variable: "",
operator: "equals",
value: "",
});
export interface ConditionalRenderingDataKind {
kind: "ConditionalRenderingData";
spec: ConditionalRenderingDataSpec;
}
export const defaultConditionalRenderingDataKind = (): ConditionalRenderingDataKind => ({
kind: "ConditionalRenderingData",
spec: defaultConditionalRenderingDataSpec(),
});
export interface ConditionalRenderingDataSpec {
value: boolean;
}
export const defaultConditionalRenderingDataSpec = (): ConditionalRenderingDataSpec => ({
value: false,
});
export interface ConditionalRenderingTimeIntervalKind {
kind: "ConditionalRenderingTimeInterval";
spec: ConditionalRenderingTimeIntervalSpec;
}
export const defaultConditionalRenderingTimeIntervalKind = (): ConditionalRenderingTimeIntervalKind => ({
kind: "ConditionalRenderingTimeInterval",
spec: defaultConditionalRenderingTimeIntervalSpec(),
});
export interface ConditionalRenderingTimeIntervalSpec {
value: string;
}
export const defaultConditionalRenderingTimeIntervalSpec = (): ConditionalRenderingTimeIntervalSpec => ({
value: "",
});

@ -692,6 +692,7 @@ export const defaultRowsLayoutRowKind = (): RowsLayoutRowKind => ({
export interface RowsLayoutRowSpec {
title?: string;
collapsed: boolean;
conditionalRendering?: ConditionalRenderingGroupKind;
repeat?: RowRepeatOptions;
layout: GridLayoutKind | ResponsiveGridLayoutKind | TabsLayoutKind;
}
@ -701,6 +702,84 @@ export const defaultRowsLayoutRowSpec = (): RowsLayoutRowSpec => ({
layout: defaultGridLayoutKind(),
});
export interface ConditionalRenderingGroupKind {
kind: "ConditionalRenderingGroup";
spec: ConditionalRenderingGroupSpec;
}
export const defaultConditionalRenderingGroupKind = (): ConditionalRenderingGroupKind => ({
kind: "ConditionalRenderingGroup",
spec: defaultConditionalRenderingGroupSpec(),
});
export interface ConditionalRenderingGroupSpec {
condition: "and" | "or";
items: (ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeIntervalKind)[];
}
export const defaultConditionalRenderingGroupSpec = (): ConditionalRenderingGroupSpec => ({
condition: "and",
items: [],
});
export interface ConditionalRenderingVariableKind {
kind: "ConditionalRenderingVariable";
spec: ConditionalRenderingVariableSpec;
}
export const defaultConditionalRenderingVariableKind = (): ConditionalRenderingVariableKind => ({
kind: "ConditionalRenderingVariable",
spec: defaultConditionalRenderingVariableSpec(),
});
export interface ConditionalRenderingVariableSpec {
variable: string;
operator: "equals" | "notEquals";
value: string;
}
export const defaultConditionalRenderingVariableSpec = (): ConditionalRenderingVariableSpec => ({
variable: "",
operator: "equals",
value: "",
});
export interface ConditionalRenderingDataKind {
kind: "ConditionalRenderingData";
spec: ConditionalRenderingDataSpec;
}
export const defaultConditionalRenderingDataKind = (): ConditionalRenderingDataKind => ({
kind: "ConditionalRenderingData",
spec: defaultConditionalRenderingDataSpec(),
});
export interface ConditionalRenderingDataSpec {
value: boolean;
}
export const defaultConditionalRenderingDataSpec = (): ConditionalRenderingDataSpec => ({
value: false,
});
export interface ConditionalRenderingTimeIntervalKind {
kind: "ConditionalRenderingTimeInterval";
spec: ConditionalRenderingTimeIntervalSpec;
}
export const defaultConditionalRenderingTimeIntervalKind = (): ConditionalRenderingTimeIntervalKind => ({
kind: "ConditionalRenderingTimeInterval",
spec: defaultConditionalRenderingTimeIntervalSpec(),
});
export interface ConditionalRenderingTimeIntervalSpec {
value: string;
}
export const defaultConditionalRenderingTimeIntervalSpec = (): ConditionalRenderingTimeIntervalSpec => ({
value: "",
});
export interface ResponsiveGridLayoutKind {
kind: "ResponsiveGridLayout";
spec: ResponsiveGridLayoutSpec;
@ -736,6 +815,7 @@ export const defaultResponsiveGridLayoutItemKind = (): ResponsiveGridLayoutItemK
export interface ResponsiveGridLayoutItemSpec {
element: ElementReference;
repeat?: ResponsiveGridRepeatOptions;
conditionalRendering?: ConditionalRenderingGroupKind;
}
export const defaultResponsiveGridLayoutItemSpec = (): ResponsiveGridLayoutItemSpec => ({

@ -1,4 +1,5 @@
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
@ -36,6 +37,10 @@ export class ConditionalRendering extends SceneObjectBase<ConditionalRenderingSt
public static createEmpty(): ConditionalRendering {
return new ConditionalRendering({ rootGroup: ConditionalRenderingGroup.createEmpty() });
}
public serialize(): ConditionalRenderingGroupKind {
return this.state.rootGroup.serialize();
}
}
function ConditionalRenderingRenderer({ model }: SceneComponentProps<ConditionalRendering>) {

@ -4,6 +4,7 @@ import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } fr
import { ConditionalRendering } from './ConditionalRendering';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
import { ConditionalRenderingKindTypes } from './serializers';
import { ConditionValues } from './shared';
export interface ConditionalRenderingBaseState<V = ConditionValues> extends SceneObjectState {
@ -34,6 +35,8 @@ export abstract class ConditionalRenderingBase<
public abstract readonly title: string;
public abstract serialize(): ConditionalRenderingKindTypes;
public abstract evaluate(): boolean;
public abstract render(): ReactNode;

@ -2,6 +2,7 @@ import { ReactNode, useMemo } from 'react';
import { PanelData, SelectableValue } from '@grafana/data';
import { SceneComponentProps, SceneDataProvider, sceneGraph } from '@grafana/scenes';
import { ConditionalRenderingDataKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { RadioButtonGroup, Stack } from '@grafana/ui';
import { t } from 'app/core/internationalization';
@ -112,6 +113,15 @@ export class ConditionalRenderingData extends ConditionalRenderingBase<Condition
public onDelete() {
handleDeleteNonGroupCondition(this);
}
public serialize(): ConditionalRenderingDataKind {
return {
kind: 'ConditionalRenderingData',
spec: {
value: this.state.value,
},
};
}
}
function ConditionalRenderingDataRenderer({ model }: SceneComponentProps<ConditionalRenderingData>) {

@ -3,6 +3,7 @@ import { Fragment, ReactNode, useMemo } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { Divider, Dropdown, Field, Menu, RadioButtonGroup, Stack, ToolbarButton, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
@ -68,6 +69,21 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase<Conditio
}
this.getConditionalLogicRoot().notifyChange();
}
public serialize(): ConditionalRenderingGroupKind {
if (this.state.value.some((item) => item instanceof ConditionalRenderingGroup)) {
throw new Error('ConditionalRenderingGroup cannot contain nested ConditionalRenderingGroups');
}
return {
kind: 'ConditionalRenderingGroup',
spec: {
condition: this.state.condition,
items: this.state.value
.map((condition) => condition.serialize())
.filter((item) => item.kind !== 'ConditionalRenderingGroup'),
},
};
}
}
function ConditionalRenderingGroupRenderer({ model }: SceneComponentProps<ConditionalRenderingGroup>) {

@ -2,6 +2,7 @@ import { ReactNode, useState } from 'react';
import { rangeUtil } from '@grafana/data';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { ConditionalRenderingTimeIntervalKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { Field, Input, Stack } from '@grafana/ui';
import { t } from 'app/core/internationalization';
@ -54,6 +55,15 @@ export class ConditionalRenderingInterval extends ConditionalRenderingBase<Condi
public onDelete() {
handleDeleteNonGroupCondition(this);
}
public serialize(): ConditionalRenderingTimeIntervalKind {
return {
kind: 'ConditionalRenderingTimeInterval',
spec: {
value: this.state.value,
},
};
}
}
function ConditionalRenderingIntervalRenderer({ model }: SceneComponentProps<ConditionalRenderingInterval>) {

@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import { ReactNode, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps, sceneGraph, VariableDependencyConfig } from '@grafana/scenes';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { ConditionalRenderingVariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { Combobox, ComboboxOption, Field, Input, Stack, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
@ -16,21 +16,13 @@ export type VariableConditionValue = {
value: string;
};
type ConditionalRenderingVariableState = ConditionalRenderingBaseState<VariableConditionValue>;
interface ConditionalRenderingVariableState extends 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;
@ -41,10 +33,15 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase<Condi
if (!variable) {
return false;
}
const value = variable.getValue();
let hit = Array.isArray(value) ? value.includes(this.state.value.value) : value === this.state.value.value;
let hit: boolean;
if (Array.isArray(value)) {
hit = value.includes(this.state.value.value);
} else {
hit = value === this.state.value.value;
}
if (this.state.value.operator === '!=') {
hit = !hit;
@ -57,17 +54,30 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase<Condi
return <ConditionalRenderingVariableRenderer model={this} />;
}
public serialize(): ConditionalRenderingVariableKind {
return {
kind: 'ConditionalRenderingVariable',
spec: {
variable: this.state.value.name,
operator: this.state.value.operator === '=' ? 'equals' : 'notEquals',
value: this.state.value.value,
},
};
}
public getVariableOptions(): Array<ComboboxOption<string>> {
return sceneGraph
.getVariables(this)
.state.variables.map((v) => ({ value: v.state.name, label: v.state.label ?? v.state.name }));
}
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 variableNames = useMemo(() => model.getVariableOptions(), [model]);
const operatorOptions: Array<ComboboxOption<'=' | '!='>> = useMemo(
() => [
{ value: '=', description: t('dashboard.conditional-rendering.variable.operator.equals', 'Equals') },
@ -116,11 +126,11 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<Con
);
}
const getStyles = (theme: GrafanaTheme2) => ({
const getStyles = () => ({
variableNameSelect: css({
flexGrow: 1,
}),
operatorSelect: css({
width: theme.spacing(12),
width: '6rem',
}),
});

@ -0,0 +1,96 @@
import { Registry, RegistryItem } from '@grafana/data';
import {
ConditionalRenderingGroupKind,
ConditionalRenderingVariableKind,
ConditionalRenderingDataKind,
ConditionalRenderingTimeIntervalKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { ConditionalRenderingData } from './ConditionalRenderingData';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
import { ConditionalRenderingInterval } from './ConditionalRenderingInterval';
import { ConditionalRenderingVariable } from './ConditionalRenderingVariable';
export type ConditionalRenderingKindTypes =
| ConditionalRenderingGroupKind
| ConditionalRenderingVariableKind
| ConditionalRenderingDataKind
| ConditionalRenderingTimeIntervalKind;
export interface ConditionalRenderingSerializer {
deserialize(
model: ConditionalRenderingKindTypes
): ConditionalRenderingGroup | ConditionalRenderingVariable | ConditionalRenderingData | ConditionalRenderingInterval;
}
interface ConditionalRenderingSerializerRegistryItem extends RegistryItem {
serializer: ConditionalRenderingSerializer;
}
export class ConditionalRenderingGroupSerializer implements ConditionalRenderingSerializer {
deserialize(model: ConditionalRenderingGroupKind): ConditionalRenderingGroup {
return new ConditionalRenderingGroup({
condition: model.spec.condition,
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.serializer.deserialize(item);
}),
});
}
}
export class ConditionalRenderingVariableSerializer implements ConditionalRenderingSerializer {
deserialize(model: ConditionalRenderingVariableKind): ConditionalRenderingVariable {
return new ConditionalRenderingVariable({
value: {
name: model.spec.variable,
operator: model.spec.operator === 'equals' ? '=' : '!=',
value: model.spec.value,
},
});
}
}
export class ConditionalRenderingDataSerializer implements ConditionalRenderingSerializer {
deserialize(model: ConditionalRenderingDataKind): ConditionalRenderingData {
return new ConditionalRenderingData({
value: model.spec.value,
});
}
}
export class ConditionalRenderingIntervalSerializer implements ConditionalRenderingSerializer {
deserialize(model: ConditionalRenderingTimeIntervalKind): ConditionalRenderingInterval {
return new ConditionalRenderingInterval({
value: model.spec.value,
});
}
}
export const conditionalRenderingSerializerRegistry = new Registry<ConditionalRenderingSerializerRegistryItem>(() => {
return [
{
id: 'ConditionalRenderingGroup',
name: 'Group',
serializer: new ConditionalRenderingGroupSerializer(),
},
{
id: 'ConditionalRenderingVariable',
name: 'Variable',
serializer: new ConditionalRenderingVariableSerializer(),
},
{
id: 'ConditionalRenderingData',
name: 'Data',
serializer: new ConditionalRenderingDataSerializer(),
},
{
id: 'ConditionalRenderingTimeInterval',
name: 'Time Interval',
serializer: new ConditionalRenderingIntervalSerializer(),
},
];
});

@ -7,7 +7,7 @@ import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/typ
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import { getGridItemKeyForPanelId } from '../../utils/utils';
import { buildLibraryPanel, buildVizPanel } from './utils';
import { buildLibraryPanel, buildVizPanel, getConditionalRendering } from './utils';
export class ResponsiveGridLayoutSerializer implements LayoutManagerSerializer {
serialize(layoutManager: ResponsiveGridLayoutManager): DashboardV2Spec['layout'] {
@ -35,6 +35,12 @@ export class ResponsiveGridLayoutSerializer implements LayoutManagerSerializer {
},
};
const conditionalRenderingRootGroup = child.state.conditionalRendering?.serialize();
// Only serialize the conditional rendering if it has items
if (conditionalRenderingRootGroup?.spec.items.length) {
layoutItem.spec.conditionalRendering = conditionalRenderingRootGroup;
}
if (child.state.variableName) {
layoutItem.spec.repeat = {
mode: 'variable',
@ -62,6 +68,7 @@ export class ResponsiveGridLayoutSerializer implements LayoutManagerSerializer {
key: getGridItemKeyForPanelId(panel.spec.id),
body: panel.kind === 'LibraryPanel' ? buildLibraryPanel(panel) : buildVizPanel(panel),
variableName: item.spec.repeat?.value,
conditionalRendering: getConditionalRendering(item),
});
});

@ -7,7 +7,7 @@ import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager';
import { LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
import { layoutSerializerRegistry } from './layoutSerializerRegistry';
import { getLayout } from './utils';
import { getConditionalRendering, getLayout } from './utils';
export class RowsLayoutSerializer implements LayoutManagerSerializer {
serialize(layoutManager: RowsLayoutManager): DashboardV2Spec['layout'] {
@ -28,6 +28,12 @@ export class RowsLayoutSerializer implements LayoutManagerSerializer {
},
};
const conditionalRenderingRootGroup = row.state.conditionalRendering?.serialize();
// Only serialize the conditional rendering if it has items
if (conditionalRenderingRootGroup?.spec.items.length) {
rowKind.spec.conditionalRendering = conditionalRenderingRootGroup;
}
if (row.state.$behaviors) {
for (const behavior of row.state.$behaviors) {
if (behavior instanceof RowItemRepeaterBehavior) {
@ -58,11 +64,13 @@ export class RowsLayoutSerializer implements LayoutManagerSerializer {
if (row.spec.repeat) {
behaviors.push(new RowItemRepeaterBehavior({ variableName: row.spec.repeat.value }));
}
return new RowItem({
title: row.spec.title,
isCollapsed: row.spec.collapsed,
$behaviors: behaviors,
layout: layoutSerializerRegistry.get(layout.kind).serializer.deserialize(layout, elements, preload),
conditionalRendering: getConditionalRendering(row),
});
});
return new RowsLayoutManager({ rows });

@ -12,12 +12,17 @@ import {
import { DataSourceRef } from '@grafana/schema/dist/esm/index.gen';
import {
DashboardV2Spec,
ResponsiveGridLayoutItemKind,
RowsLayoutRowKind,
LibraryPanelKind,
PanelKind,
PanelQueryKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
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 { DashboardDatasourceBehaviour } from '../../scene/DashboardDatasourceBehaviour';
import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks';
@ -202,3 +207,18 @@ export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['
}
return registryItem.serializer.serialize(sceneState);
}
export function getConditionalRendering(item: RowsLayoutRowKind | ResponsiveGridLayoutItemKind): ConditionalRendering {
if (!item.spec.conditionalRendering) {
return ConditionalRendering.createEmpty();
}
const rootGroup = conditionalRenderingSerializerRegistry
.get(item.spec.conditionalRendering.kind)
.serializer.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 });
}

Loading…
Cancel
Save