Dynamic Dashboard: Redesign conditional rendering [2] (#103112)

This commit is contained in:
Bogdan Matei
2025-04-02 20:50:36 +03:00
committed by GitHub
parent c372012abc
commit 0e1cb44ad9
38 changed files with 1156 additions and 690 deletions
@@ -600,8 +600,9 @@ TabsLayoutTabKind: {
}
TabsLayoutTabSpec: {
title?: string
layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind
title?: string
layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind
conditionalRendering?: ConditionalRenderingGroupKind
}
PanelSpec: {
@@ -924,8 +925,9 @@ ConditionalRenderingGroupKind: {
}
ConditionalRenderingGroupSpec: {
visibility: "show" | "hide"
condition: "and" | "or"
items: [...ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeIntervalKind]
items: [...ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeRangeSizeKind]
}
ConditionalRenderingVariableKind: {
@@ -948,11 +950,11 @@ ConditionalRenderingDataSpec: {
value: bool
}
ConditionalRenderingTimeIntervalKind: {
kind: "ConditionalRenderingTimeInterval"
spec: ConditionalRenderingTimeIntervalSpec
ConditionalRenderingTimeRangeSizeKind: {
kind: "ConditionalRenderingTimeRangeSize"
spec: ConditionalRenderingTimeRangeSizeSpec
}
ConditionalRenderingTimeIntervalSpec: {
ConditionalRenderingTimeRangeSizeSpec: {
value: string
}
@@ -868,8 +868,9 @@ func NewDashboardConditionalRenderingGroupKind() *DashboardConditionalRenderingG
// +k8s:openapi-gen=true
type DashboardConditionalRenderingGroupSpec struct {
Condition DashboardConditionalRenderingGroupSpecCondition `json:"condition"`
Items []DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind `json:"items"`
Visibility DashboardConditionalRenderingGroupSpecVisibility `json:"visibility"`
Condition DashboardConditionalRenderingGroupSpecCondition `json:"condition"`
Items []DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind `json:"items"`
}
// NewDashboardConditionalRenderingGroupSpec creates a new DashboardConditionalRenderingGroupSpec object.
@@ -928,27 +929,27 @@ func NewDashboardConditionalRenderingDataSpec() *DashboardConditionalRenderingDa
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingTimeIntervalKind struct {
Kind string `json:"kind"`
Spec DashboardConditionalRenderingTimeIntervalSpec `json:"spec"`
type DashboardConditionalRenderingTimeRangeSizeKind struct {
Kind string `json:"kind"`
Spec DashboardConditionalRenderingTimeRangeSizeSpec `json:"spec"`
}
// NewDashboardConditionalRenderingTimeIntervalKind creates a new DashboardConditionalRenderingTimeIntervalKind object.
func NewDashboardConditionalRenderingTimeIntervalKind() *DashboardConditionalRenderingTimeIntervalKind {
return &DashboardConditionalRenderingTimeIntervalKind{
Kind: "ConditionalRenderingTimeInterval",
Spec: *NewDashboardConditionalRenderingTimeIntervalSpec(),
// NewDashboardConditionalRenderingTimeRangeSizeKind creates a new DashboardConditionalRenderingTimeRangeSizeKind object.
func NewDashboardConditionalRenderingTimeRangeSizeKind() *DashboardConditionalRenderingTimeRangeSizeKind {
return &DashboardConditionalRenderingTimeRangeSizeKind{
Kind: "ConditionalRenderingTimeRangeSize",
Spec: *NewDashboardConditionalRenderingTimeRangeSizeSpec(),
}
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingTimeIntervalSpec struct {
type DashboardConditionalRenderingTimeRangeSizeSpec struct {
Value string `json:"value"`
}
// NewDashboardConditionalRenderingTimeIntervalSpec creates a new DashboardConditionalRenderingTimeIntervalSpec object.
func NewDashboardConditionalRenderingTimeIntervalSpec() *DashboardConditionalRenderingTimeIntervalSpec {
return &DashboardConditionalRenderingTimeIntervalSpec{}
// NewDashboardConditionalRenderingTimeRangeSizeSpec creates a new DashboardConditionalRenderingTimeRangeSizeSpec object.
func NewDashboardConditionalRenderingTimeRangeSizeSpec() *DashboardConditionalRenderingTimeRangeSizeSpec {
return &DashboardConditionalRenderingTimeRangeSizeSpec{}
}
// +k8s:openapi-gen=true
@@ -1063,8 +1064,9 @@ func NewDashboardTabsLayoutTabKind() *DashboardTabsLayoutTabKind {
// +k8s:openapi-gen=true
type DashboardTabsLayoutTabSpec struct {
Title *string `json:"title,omitempty"`
Layout DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind `json:"layout"`
Title *string `json:"title,omitempty"`
Layout DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind `json:"layout"`
ConditionalRendering *DashboardConditionalRenderingGroupKind `json:"conditionalRendering,omitempty"`
}
// NewDashboardTabsLayoutTabSpec creates a new DashboardTabsLayoutTabSpec object.
@@ -1788,6 +1790,14 @@ const (
DashboardRepeatOptionsDirectionV DashboardRepeatOptionsDirection = "v"
)
// +k8s:openapi-gen=true
type DashboardConditionalRenderingGroupSpecVisibility string
const (
DashboardConditionalRenderingGroupSpecVisibilityShow DashboardConditionalRenderingGroupSpecVisibility = "show"
DashboardConditionalRenderingGroupSpecVisibilityHide DashboardConditionalRenderingGroupSpecVisibility = "hide"
)
// +k8s:openapi-gen=true
type DashboardConditionalRenderingGroupSpecCondition string
@@ -2126,33 +2136,33 @@ func (resource *DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRow
}
// +k8s:openapi-gen=true
type DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind struct {
ConditionalRenderingVariableKind *DashboardConditionalRenderingVariableKind `json:"ConditionalRenderingVariableKind,omitempty"`
ConditionalRenderingDataKind *DashboardConditionalRenderingDataKind `json:"ConditionalRenderingDataKind,omitempty"`
ConditionalRenderingTimeIntervalKind *DashboardConditionalRenderingTimeIntervalKind `json:"ConditionalRenderingTimeIntervalKind,omitempty"`
type DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind struct {
ConditionalRenderingVariableKind *DashboardConditionalRenderingVariableKind `json:"ConditionalRenderingVariableKind,omitempty"`
ConditionalRenderingDataKind *DashboardConditionalRenderingDataKind `json:"ConditionalRenderingDataKind,omitempty"`
ConditionalRenderingTimeRangeSizeKind *DashboardConditionalRenderingTimeRangeSizeKind `json:"ConditionalRenderingTimeRangeSizeKind,omitempty"`
}
// NewDashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind creates a new DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind object.
func NewDashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind() *DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind {
return &DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind{}
// NewDashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind creates a new DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind object.
func NewDashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind() *DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind {
return &DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind{}
}
// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind` as JSON.
func (resource DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind) MarshalJSON() ([]byte, error) {
// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind` as JSON.
func (resource DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind) 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)
if resource.ConditionalRenderingTimeRangeSizeKind != nil {
return json.Marshal(resource.ConditionalRenderingTimeRangeSizeKind)
}
return []byte("null"), nil
}
// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind` from JSON.
func (resource *DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind) UnmarshalJSON(raw []byte) error {
// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind` from JSON.
func (resource *DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind) UnmarshalJSON(raw []byte) error {
if raw == nil {
return nil
}
@@ -2177,13 +2187,13 @@ func (resource *DashboardConditionalRenderingVariableKindOrConditionalRenderingD
resource.ConditionalRenderingDataKind = &dashboardConditionalRenderingDataKind
return nil
case "ConditionalRenderingTimeInterval":
var dashboardConditionalRenderingTimeIntervalKind DashboardConditionalRenderingTimeIntervalKind
if err := json.Unmarshal(raw, &dashboardConditionalRenderingTimeIntervalKind); err != nil {
case "ConditionalRenderingTimeRangeSize":
var dashboardConditionalRenderingTimeRangeSizeKind DashboardConditionalRenderingTimeRangeSizeKind
if err := json.Unmarshal(raw, &dashboardConditionalRenderingTimeRangeSizeKind); err != nil {
return err
}
resource.ConditionalRenderingTimeIntervalKind = &dashboardConditionalRenderingTimeIntervalKind
resource.ConditionalRenderingTimeRangeSizeKind = &dashboardConditionalRenderingTimeRangeSizeKind
return nil
case "ConditionalRenderingVariable":
var dashboardConditionalRenderingVariableKind DashboardConditionalRenderingVariableKind
@@ -33,10 +33,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataSpec(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupSpec(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeIntervalKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeIntervalKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeIntervalSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeIntervalSpec(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeSpec(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableSpec(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableKind(ref),
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableSpec(ref),
@@ -904,6 +904,13 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupSpec(r
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"visibility": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"condition": {
SchemaProps: spec.SchemaProps{
Default: "",
@@ -917,22 +924,22 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupSpec(r
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind"),
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind"),
},
},
},
},
},
},
Required: []string{"condition", "items"},
Required: []string{"visibility", "condition", "items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind"},
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind"},
}
}
func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeIntervalKind(ref common.ReferenceCallback) common.OpenAPIDefinition {
func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeKind(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
@@ -948,7 +955,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeInterva
"spec": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeIntervalSpec"),
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec"),
},
},
},
@@ -956,11 +963,11 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeInterva
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeIntervalSpec"},
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec"},
}
}
func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeIntervalSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
@@ -1008,7 +1015,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKin
}
}
func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind(ref common.ReferenceCallback) common.OpenAPIDefinition {
func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
@@ -1024,16 +1031,16 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKin
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataKind"),
},
},
"ConditionalRenderingTimeIntervalKind": {
"ConditionalRenderingTimeRangeSizeKind": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeIntervalKind"),
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeIntervalKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKind"},
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKind"},
}
}
@@ -4000,12 +4007,17 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabSpec(ref common.Re
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind"),
},
},
"conditionalRendering": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind"),
},
},
},
Required: []string{"layout"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind"},
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind"},
}
}
@@ -32,9 +32,9 @@ API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardTimeSettingsSpec,QuickRanges
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardV2alpha1FieldConfigSourceOverrides,Properties
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,LibraryPanelStatus,Warnings
API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind,ConditionalRenderingDataKind
API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind,ConditionalRenderingTimeIntervalKind
API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeIntervalKind,ConditionalRenderingVariableKind
API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingDataKind
API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingTimeRangeSizeKind
API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingVariableKind
API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutItemKindOrGridLayoutRowKind,GridLayoutItemKind
API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutItemKindOrGridLayoutRowKind,GridLayoutRowKind
API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,AutoGridLayoutKind
@@ -515,7 +515,7 @@ GridLayoutItemKind: {
GridLayoutRowKind: {
kind: "GridLayoutRow"
spec: GridLayoutRowSpec
spec: GridLayoutRowSpec
}
GridLayoutRowSpec: {
@@ -602,6 +602,7 @@ TabsLayoutTabKind: {
TabsLayoutTabSpec: {
title?: string
layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind
conditionalRendering?: ConditionalRenderingGroupKind
}
PanelSpec: {
@@ -928,8 +929,9 @@ ConditionalRenderingGroupKind: {
}
ConditionalRenderingGroupSpec: {
visibility: "show" | "hide"
condition: "and" | "or"
items: [...ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeIntervalKind]
items: [...ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeRangeSizeKind]
}
ConditionalRenderingVariableKind: {
@@ -952,11 +954,11 @@ ConditionalRenderingDataSpec: {
value: bool
}
ConditionalRenderingTimeIntervalKind: {
kind: "ConditionalRenderingTimeInterval"
spec: ConditionalRenderingTimeIntervalSpec
}
ConditionalRenderingTimeRangeSizeKind: {
kind: "ConditionalRenderingTimeRangeSize"
spec: ConditionalRenderingTimeRangeSizeSpec
}
ConditionalRenderingTimeIntervalSpec: {
ConditionalRenderingTimeRangeSizeSpec: {
value: string
}
@@ -756,11 +756,13 @@ export const defaultConditionalRenderingGroupKind = (): ConditionalRenderingGrou
});
export interface ConditionalRenderingGroupSpec {
visibility: "show" | "hide";
condition: "and" | "or";
items: (ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeIntervalKind)[];
items: (ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeRangeSizeKind)[];
}
export const defaultConditionalRenderingGroupSpec = (): ConditionalRenderingGroupSpec => ({
visibility: "show",
condition: "and",
items: [],
});
@@ -805,21 +807,21 @@ export const defaultConditionalRenderingDataSpec = (): ConditionalRenderingDataS
value: false,
});
export interface ConditionalRenderingTimeIntervalKind {
kind: "ConditionalRenderingTimeInterval";
spec: ConditionalRenderingTimeIntervalSpec;
export interface ConditionalRenderingTimeRangeSizeKind {
kind: "ConditionalRenderingTimeRangeSize";
spec: ConditionalRenderingTimeRangeSizeSpec;
}
export const defaultConditionalRenderingTimeIntervalKind = (): ConditionalRenderingTimeIntervalKind => ({
kind: "ConditionalRenderingTimeInterval",
spec: defaultConditionalRenderingTimeIntervalSpec(),
export const defaultConditionalRenderingTimeRangeSizeKind = (): ConditionalRenderingTimeRangeSizeKind => ({
kind: "ConditionalRenderingTimeRangeSize",
spec: defaultConditionalRenderingTimeRangeSizeSpec(),
});
export interface ConditionalRenderingTimeIntervalSpec {
export interface ConditionalRenderingTimeRangeSizeSpec {
value: string;
}
export const defaultConditionalRenderingTimeIntervalSpec = (): ConditionalRenderingTimeIntervalSpec => ({
export const defaultConditionalRenderingTimeRangeSizeSpec = (): ConditionalRenderingTimeRangeSizeSpec => ({
value: "",
});
@@ -912,6 +914,7 @@ export const defaultTabsLayoutTabKind = (): TabsLayoutTabKind => ({
export interface TabsLayoutTabSpec {
title?: string;
layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind;
conditionalRendering?: ConditionalRenderingGroupKind;
}
export const defaultTabsLayoutTabSpec = (): TabsLayoutTabSpec => ({
@@ -711,11 +711,13 @@ export const defaultConditionalRenderingGroupKind = (): ConditionalRenderingGrou
});
export interface ConditionalRenderingGroupSpec {
visibility: "show" | "hide";
condition: "and" | "or";
items: (ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeIntervalKind)[];
items: (ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeRangeSizeKind)[];
}
export const defaultConditionalRenderingGroupSpec = (): ConditionalRenderingGroupSpec => ({
visibility: "show",
condition: "and",
items: [],
});
@@ -760,21 +762,21 @@ export const defaultConditionalRenderingDataSpec = (): ConditionalRenderingDataS
value: false,
});
export interface ConditionalRenderingTimeIntervalKind {
kind: "ConditionalRenderingTimeInterval";
spec: ConditionalRenderingTimeIntervalSpec;
export interface ConditionalRenderingTimeRangeSizeKind {
kind: "ConditionalRenderingTimeRangeSize";
spec: ConditionalRenderingTimeRangeSizeSpec;
}
export const defaultConditionalRenderingTimeIntervalKind = (): ConditionalRenderingTimeIntervalKind => ({
kind: "ConditionalRenderingTimeInterval",
spec: defaultConditionalRenderingTimeIntervalSpec(),
export const defaultConditionalRenderingTimeRangeSizeKind = (): ConditionalRenderingTimeRangeSizeKind => ({
kind: "ConditionalRenderingTimeRangeSize",
spec: defaultConditionalRenderingTimeRangeSizeSpec(),
});
export interface ConditionalRenderingTimeIntervalSpec {
export interface ConditionalRenderingTimeRangeSizeSpec {
value: string;
}
export const defaultConditionalRenderingTimeIntervalSpec = (): ConditionalRenderingTimeIntervalSpec => ({
export const defaultConditionalRenderingTimeRangeSizeSpec = (): ConditionalRenderingTimeRangeSizeSpec => ({
value: "",
});
@@ -867,6 +869,7 @@ export const defaultTabsLayoutTabKind = (): TabsLayoutTabKind => ({
export interface TabsLayoutTabSpec {
title?: string;
layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind;
conditionalRendering?: ConditionalRenderingGroupKind;
}
export const defaultTabsLayoutTabSpec = (): TabsLayoutTabSpec => ({
@@ -102,11 +102,7 @@ export function getDashboardGridStyles(theme: GrafanaTheme2) {
},
'.dashboard-visible-hidden-element': {
opacity: 0.6,
'&:hover': {
opacity: 1,
},
position: 'relative',
},
// Universal style for marking drop targets when dragging between layouts
@@ -1,20 +0,0 @@
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>
);
};
@@ -1,7 +1,13 @@
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { t } from 'app/core/internationalization';
import { ConditionalRenderingChangedEvent } from '../edit-pane/shared';
import { ConditionalRenderingBase } from './ConditionalRenderingBase';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
import { ItemsWithConditionalRendering } from './types';
import { getItemType } from './utils';
export interface ConditionalRenderingState extends SceneObjectState {
rootGroup: ConditionalRenderingGroup;
@@ -10,6 +16,14 @@ export interface ConditionalRenderingState extends SceneObjectState {
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: this.getItemType() }
);
}
public constructor(state: ConditionalRenderingState) {
super(state);
@@ -18,7 +32,7 @@ export class ConditionalRendering extends SceneObjectBase<ConditionalRenderingSt
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.
// We need this to allow children to subscribe to variable changes etc.
this.forEachChild((child) => {
if (!child.isActive) {
this._subs.add(child.activate());
@@ -32,19 +46,32 @@ export class ConditionalRendering extends SceneObjectBase<ConditionalRenderingSt
public notifyChange() {
this.parent?.forceRender();
this.parent?.publishEvent(new ConditionalRenderingChangedEvent(this), true);
}
public static createEmpty(): ConditionalRendering {
return new ConditionalRendering({ rootGroup: ConditionalRenderingGroup.createEmpty() });
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.Component model={rootGroup} />;
return rootGroup.render(false);
}
@@ -1,21 +1,19 @@
import { ReactNode } from 'react';
import { ReactElement } from 'react';
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { Alert, Icon, IconButton, Stack, Text, Tooltip } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ConditionalRendering } from './ConditionalRendering';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
import { ConditionalRenderingKindTypes } from './serializers';
import { ConditionValues } from './shared';
import { ConditionalRenderingKindTypes, ConditionValues, ItemsWithConditionalRendering } from './types';
export interface ConditionalRenderingBaseState<V = ConditionValues> extends SceneObjectState {
value: V;
}
export abstract class ConditionalRenderingBase<
S extends ConditionalRenderingBaseState<ConditionValues>,
S extends ConditionalRenderingBaseState = ConditionalRenderingBaseState,
> extends SceneObjectBase<S> {
public static Component = ConditionalRenderingBaseRenderer;
public constructor(state: S) {
super(state);
@@ -25,7 +23,7 @@ export abstract class ConditionalRenderingBase<
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.
// We need this to allow children to subscribe to variable changes, etc.
this.forEachChild((child) => {
if (!child.isActive) {
this._subs.add(child.activate());
@@ -33,32 +31,93 @@ export abstract class ConditionalRenderingBase<
});
}
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 abstract render(): ReactNode;
public abstract onDelete(): void;
public getConditionalLogicRoot(): ConditionalRendering {
return sceneGraph.getAncestor(this, ConditionalRendering);
public onDelete() {
this._getConditionalLogicRoot().deleteItem(this);
}
public getRootGroup(): ConditionalRenderingGroup {
return this.getConditionalLogicRoot().state.rootGroup;
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.getConditionalLogicRoot().notifyChange();
this.notifyChange();
}
private _getConditionalLogicRoot(): ConditionalRendering {
return sceneGraph.getAncestor(this, ConditionalRendering);
}
}
function ConditionalRenderingBaseRenderer({
function ConditionalRenderingBaseRenderer<T extends ConditionalRenderingBase>({
model,
}: SceneComponentProps<ConditionalRenderingBase<ConditionalRenderingBaseState>>) {
return model.render();
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="baseline">
<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={() => model.onDelete()}
/>
</Stack>
</Stack>
);
}
@@ -1,25 +1,39 @@
import { ReactNode, useMemo } from 'react';
import { useMemo } from 'react';
import { PanelData, SelectableValue } from '@grafana/data';
import { SceneComponentProps, SceneDataProvider, sceneGraph } from '@grafana/scenes';
import { PanelData } from '@grafana/data';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { ConditionalRenderingDataKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { RadioButtonGroup, Stack } from '@grafana/ui';
import { Combobox, ComboboxOption } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { AutoGridItem } 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;
import { ConditionalRenderingSerializerRegistryItem, DataConditionValue, ItemsWithConditionalRendering } from './types';
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.data.label', 'Data');
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: this.getItemType() }
);
}
public constructor(state: ConditionalRenderingDataState) {
@@ -29,122 +43,90 @@ export class ConditionalRenderingData extends ConditionalRenderingBase<Condition
}
private _activationHandler() {
let panelDataProviders: SceneDataProvider[] = [];
const item = this.getConditionalLogicRoot().parent;
if (item instanceof AutoGridItem) {
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);
}
}
if (!this.isItemSupported()) {
return;
}
panelDataProviders.forEach((d) => {
this._subs.add(
d.subscribeToState(() => {
this.getConditionalLogicRoot().notifyChange();
})
);
});
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 {
const { value } = this.state;
// enable/disable condition
if (!value) {
if (!this.isItemSupported()) {
return true;
}
let data: PanelData[] = [];
const hasData = this._hasData();
return (this.state.value && hasData) || (!this.state.value && !hasData);
}
// get ResponsiveGridItem or RowItem
const item = this.getConditionalLogicRoot().parent;
public serialize(): ConditionalRenderingDataKind {
return { kind: 'ConditionalRenderingData', spec: { value: this.state.value } };
}
private _hasData(): boolean {
const item = this.getItem();
let data: PanelData | undefined;
// extract single panel data from ResponsiveGridItem
if (item instanceof AutoGridItem) {
const panelData = sceneGraph.getData(item.state.body).state.data;
if (panelData) {
data.push(panelData);
}
data = sceneGraph.getData(item.state.body).state.data;
}
// 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) {
if (!data) {
return false;
}
for (let panelDataIdx = 0; panelDataIdx < data.length; panelDataIdx++) {
const series = data[panelDataIdx]?.series ?? [];
const series = data?.series ?? [];
for (let seriesIdx = 0; seriesIdx < series.length; seriesIdx++) {
if (series[seriesIdx].length > 0) {
return true;
}
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 static deserialize(model: ConditionalRenderingDataKind): ConditionalRenderingData {
return new ConditionalRenderingData({ value: model.spec.value });
}
public onDelete() {
handleDeleteNonGroupCondition(this);
}
public serialize(): ConditionalRenderingDataKind {
return {
kind: 'ConditionalRenderingData',
spec: {
value: this.state.value,
},
};
public static createEmpty(): ConditionalRenderingData {
return new ConditionalRenderingData({ value: true });
}
}
function ConditionalRenderingDataRenderer({ model }: SceneComponentProps<ConditionalRenderingData>) {
const { value } = model.useState();
const enableConditionOptions: Array<SelectableValue<true | false>> = useMemo(
const enableConditionOptions: Array<ComboboxOption<1 | 0>> = useMemo(
() => [
{ label: t('dashboard.conditional-rendering.data.enable', 'Enable'), value: true },
{ label: t('dashboard.conditional-rendering.data.disable', 'Disable'), value: false },
{ 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 (
<Stack direction="column">
<ConditionHeader title={model.title} onDelete={() => model.onDelete()} />
<RadioButtonGroup
fullWidth
options={enableConditionOptions}
value={value}
onChange={(value) => model.setStateAndNotify({ value: value })}
/>
</Stack>
<Combobox
options={enableConditionOptions}
value={enableConditionOption}
onChange={({ value }) => model.setStateAndNotify({ value: Boolean(value) })}
/>
);
}
@@ -1,3 +1,4 @@
import { Icon, Stack, Tooltip } 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';
@@ -11,13 +12,23 @@ export function useConditionalRenderingEditor(
return null;
}
const title = t('dashboard.conditional-rendering.root.title', 'Show / hide rules');
return new OptionsPaneCategoryDescriptor({
title: t('dashboard.conditional-rendering.title', 'Conditional rendering options'),
title,
id: 'conditional-rendering-options',
renderTitle: () => (
<Stack direction="row" gap={1} alignItems="center">
<div>{title}</div>
<Tooltip content={conditionalRendering.info}>
<Icon name={!conditionalRendering.evaluate() ? 'eye-slash' : 'eye'} />
</Tooltip>
</Stack>
),
isOpenDefault: true,
}).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.conditional-rendering.title', 'Conditional rendering options'),
title,
render: () => <conditionalRendering.Component model={conditionalRendering} />,
})
);
@@ -1,27 +1,45 @@
import { css } from '@emotion/css';
import { Fragment, ReactNode, useMemo } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { Divider, Dropdown, Field, Menu, RadioButtonGroup, Stack, ToolbarButton, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { Stack } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { ConditionHeader } from './ConditionHeader';
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase';
import { ConditionalRenderingData } from './ConditionalRenderingData';
import { ConditionalRenderingInterval } from './ConditionalRenderingInterval';
import { ConditionalRenderingGroupAdd } from './ConditionalRenderingGroupAdd';
import { ConditionalRenderingGroupCondition } from './ConditionalRenderingGroupCondition';
import { ConditionalRenderingGroupVisibility } from './ConditionalRenderingGroupVisibility';
import { ConditionalRenderingTimeRangeSize } from './ConditionalRenderingTimeRangeSize';
import { ConditionalRenderingVariable } from './ConditionalRenderingVariable';
import { ConditionalRenderingConditions } from './shared';
import { conditionalRenderingSerializerRegistry } from './serializers';
import {
ConditionalRenderingKindTypes,
ConditionalRenderingSerializerRegistryItem,
GroupConditionCondition,
GroupConditionItemType,
GroupConditionVisibility,
GroupConditionValue,
} from './types';
export type GroupConditionValue = ConditionalRenderingConditions[];
export interface ConditionalRenderingGroupState extends ConditionalRenderingBaseState<GroupConditionValue> {
condition: 'and' | 'or';
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.group.label', 'Group');
return t('dashboard.conditional-rendering.conditions.group.label', 'Group');
}
public get info(): undefined {
return undefined;
}
public evaluate(): boolean {
@@ -29,22 +47,30 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase<Conditio
return true;
}
if (this.state.condition === 'and') {
return this.state.value.every((entry) => entry.evaluate());
}
const value =
this.state.condition === 'and'
? this.state.value.every((entry) => entry.evaluate())
: this.state.value.some((entry) => entry.evaluate());
return this.state.value.some((entry) => entry.evaluate());
return this.state.visibility === 'show' ? value : !value;
}
public render(): ReactNode {
return <ConditionalRenderingGroupRenderer model={this} />;
public changeVisibility(visibility: GroupConditionVisibility) {
this.setStateAndNotify({ visibility });
}
public changeCondition(condition: 'and' | 'or') {
public changeCondition(condition: GroupConditionCondition) {
this.setStateAndNotify({ condition });
}
public addItem(item: ConditionalRenderingConditions) {
public addItem(itemType: GroupConditionItemType) {
const item =
itemType === 'data'
? ConditionalRenderingData.createEmpty()
: itemType === 'variable'
? ConditionalRenderingVariable.createEmpty(sceneGraph.getVariables(this).state.variables[0].state.name)
: ConditionalRenderingTimeRangeSize.createEmpty();
// 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] });
@@ -53,30 +79,22 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase<Conditio
item.activate();
}
this.getConditionalLogicRoot().notifyChange();
this.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();
public removeItem(key: string) {
this.setStateAndNotify({ value: this.state.value.filter((condition) => condition.state.key !== key) });
}
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())
@@ -84,92 +102,48 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase<Conditio
},
};
}
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 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' },
],
[]
);
const { condition, visibility, value } = model.useState();
const { variables } = sceneGraph.getVariables(model).useState();
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 direction="column" gap={2}>
<ConditionalRenderingGroupVisibility
itemType={model.getItemType()}
value={visibility}
onChange={(value) => model.changeVisibility(value)}
/>
{value.length > 1 && (
<ConditionalRenderingGroupCondition value={condition} onChange={(value) => model.changeCondition(value)} />
)}
{value.map((entry) => entry.render())}
<ConditionalRenderingGroupAdd
itemType={model.getItemType()}
hasVariables={variables.length > 0}
onAdd={(itemType) => model.addItem(itemType)}
/>
</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,48 @@
import { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { ValuePicker } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { GroupConditionItemType, ItemsWithConditionalRendering } from './types';
interface Props {
itemType: ItemsWithConditionalRendering;
hasVariables: boolean;
onAdd: (itemType: GroupConditionItemType) => void;
}
export const ConditionalRenderingGroupAdd = ({ itemType, hasVariables, onAdd }: Props) => {
const options = useMemo<Array<SelectableValue<GroupConditionItemType>>>(() => {
const allOptions: Array<SelectableValue<GroupConditionItemType>> = [
{ label: t('dashboard.conditional-rendering.conditions.group.add.data', 'Query result'), value: 'data' },
{
label: t('dashboard.conditional-rendering.conditions.group.add.variable', 'Template variable'),
value: 'variable',
isDisabled: !hasVariables,
},
{
label: t('dashboard.conditional-rendering.conditions.group.add.time-range-size', 'Time range less than'),
value: 'timeRangeSize',
},
];
if (itemType !== 'panel') {
allOptions.shift();
}
return allOptions;
}, [itemType, hasVariables]);
return (
<ValuePicker
isFullWidth
size="sm"
icon="plus"
variant="secondary"
label={t('dashboard.conditional-rendering.conditions.group.add.button', 'Add rule')}
options={options}
onChange={({ value }) => onAdd(value!)}
/>
);
};
@@ -0,0 +1,40 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Field, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { GroupConditionCondition } from './types';
interface Props {
value: GroupConditionCondition;
onChange: (value: GroupConditionCondition) => void;
}
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 (AND)'), value: 'and' },
{ label: t('dashboard.conditional-rendering.conditions.group.condition.any', 'Match any (OR)'), value: 'or' },
],
[]
);
return (
<Field
label={t('dashboard.conditional-rendering.conditions.group.condition.label', 'Match rules')}
className={styles.container}
>
<RadioButtonGroup fullWidth options={options} value={value} onChange={onChange} />
</Field>
);
};
const getStyles = () => ({
container: css({
margin: 0,
}),
});
@@ -0,0 +1,44 @@
import { css } from '@emotion/css';
import { capitalize } from 'lodash';
import { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Field, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { GroupConditionVisibility, ItemsWithConditionalRendering } from './types';
interface Props {
itemType: ItemsWithConditionalRendering;
value: GroupConditionVisibility;
onChange: (value: GroupConditionVisibility) => void;
}
export const ConditionalRenderingGroupVisibility = ({ itemType, value, onChange }: Props) => {
const styles = useStyles2(getStyles);
const options: Array<SelectableValue<GroupConditionVisibility>> = useMemo(
() => [
{ label: t('dashboard.conditional-rendering.conditions.group.visibility.show', 'Show'), value: 'show' },
{ label: t('dashboard.conditional-rendering.conditions.group.visibility.hide', 'Hide'), value: 'hide' },
],
[]
);
return (
<Field
label={t('dashboard.conditional-rendering.conditions.group.visibility.label', '{{type}} visibility', {
type: capitalize(itemType),
})}
className={styles.container}
>
<RadioButtonGroup fullWidth options={options} value={value} onChange={onChange} />
</Field>
);
};
const getStyles = () => ({
container: css({
margin: 0,
}),
});
@@ -1,93 +0,0 @@
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/v2alpha1/types.spec.gen';
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);
}
public serialize(): ConditionalRenderingTimeIntervalKind {
return {
kind: 'ConditionalRenderingTimeInterval',
spec: {
value: this.state.value,
},
};
}
}
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,67 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, Tooltip, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
export const ConditionalRenderingOverlay = () => {
const styles = useStyles2(getStyles);
return (
<div className={styles.container}>
<Tooltip
content={t(
'dashboard.conditional-rendering.overlay.tooltip',
'Element is hidden due to conditional rendering.'
)}
>
<Icon name="eye-slash" />
</Tooltip>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
width: '100%',
height: '100%',
bottom: 0,
right: 0,
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
transition: 'all 0.2s ease',
},
'&:before': css({
content: '""',
opacity: 0.6,
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
backgroundColor: theme.colors.background.canvas,
pointerEvents: 'none',
}),
'& > svg': css({
height: '48px',
width: '48px',
maxWidth: '75%',
maxHeight: '75%',
}),
'.dashboard-visible-hidden-element:hover > &': css({
width: '30px',
height: '30px',
top: 'unset',
left: 'unset',
right: 0,
bottom: 0,
}),
}),
});
@@ -0,0 +1,219 @@
import { css } from '@emotion/css';
import { useEffect, useMemo, useState } from 'react';
import { rangeUtil, SelectableValue } from '@grafana/data';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { ConditionalRenderingTimeRangeSizeKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { Field, Select, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase';
import { ConditionalRenderingSerializerRegistryItem, TimeRangeSizeConditionValue } from './types';
type ConditionalRenderingTimeRangeSizeState = ConditionalRenderingBaseState<TimeRangeSizeConditionValue>;
export class ConditionalRenderingTimeRangeSize extends ConditionalRenderingBase<ConditionalRenderingTimeRangeSizeState> {
public static Component = ConditionalRenderingTimeRangeSizeRenderer;
public static serializer: ConditionalRenderingSerializerRegistryItem = {
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: this.getItemType() }
);
}
public constructor(state: ConditionalRenderingTimeRangeSizeState) {
super(state);
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
this._subs.add(sceneGraph.getTimeRange(this).subscribeToState(() => this.notifyChange()));
}
public evaluate(): boolean {
try {
if (!validateIntervalRegex.test(this.state.value)) {
return true;
}
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 serialize(): ConditionalRenderingTimeRangeSizeKind {
return { kind: 'ConditionalRenderingTimeRangeSize', spec: { value: this.state.value } };
}
public static deserialize(model: ConditionalRenderingTimeRangeSizeKind): ConditionalRenderingTimeRangeSize {
return new ConditionalRenderingTimeRangeSize({ value: model.spec.value });
}
public static createEmpty(): ConditionalRenderingTimeRangeSize {
return new ConditionalRenderingTimeRangeSize({ value: '7d' });
}
}
function ConditionalRenderingTimeRangeSizeRenderer({ model }: SceneComponentProps<ConditionalRenderingTimeRangeSize>) {
const { value } = model.useState();
const [isValid, setIsValid] = useState(validateIntervalRegex.test(value));
const styles = useStyles2(getStyles);
useEffect(() => setIsValid(validateIntervalRegex.test(value)), [value]);
const staticOptions = useMemo<Array<SelectableValue<string>>>(
() => [
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.minutes', '{{value}} minutes', {
value: 5,
}),
value: '5m',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.minutes', '{{value}} minutes', {
value: 15,
}),
value: '15m',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.minutes', '{{value}} minutes', {
value: 30,
}),
value: '30m',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.hour', '{{value}} hour', {
value: 1,
}),
value: '1h',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.hours', '{{value}} hours', {
value: 3,
}),
value: '3h',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.hours', '{{value}} hours', {
value: 6,
}),
value: '6h',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.hours', '{{value}} hours', {
value: 12,
}),
value: '12h',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.hours', '{{value}} hours', {
value: 24,
}),
value: '24h',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.days', '{{value}} days', {
value: 2,
}),
value: '2d',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.days', '{{value}} days', {
value: 7,
}),
value: '7d',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.days', '{{value}} days', {
value: 30,
}),
value: '30d',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.days', '{{value}} days', {
value: 90,
}),
value: '90d',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.months', '{{value}} months', {
value: 6,
}),
value: '6M',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.year', '{{value}} year', {
value: 1,
}),
value: '1y',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.years', '{{value}} years', {
value: 2,
}),
value: '2y',
},
{
label: t('dashboard.conditional-rendering.conditions.time-range-size.values.years', '{{value}} years', {
value: 5,
}),
value: '5y',
},
],
[]
);
const options = useMemo(() => {
if (staticOptions.find((option) => option.value === value)) {
return staticOptions;
}
return [{ label: value, value }, ...staticOptions];
}, [staticOptions, value]);
return (
<Field
invalid={!isValid}
error={t('dashboard.conditional-rendering.conditions.time-range-size.invalid-message', 'Invalid interval')}
className={styles.container}
>
<Select
isClearable={false}
allowCustomValue
onCreateOption={(value) => model.setStateAndNotify({ value })}
value={value}
options={options}
onChange={({ value }) => model.setStateAndNotify({ value })}
/>
</Field>
);
}
const getStyles = () => ({
container: css({
margin: 0,
}),
});
const validateIntervalRegex = /^(\d+(?:\.\d+)?)[Mwdhmsy]$/;
@@ -1,33 +1,44 @@
import { css } from '@emotion/css';
import { ReactNode, useMemo } from 'react';
import { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps, sceneGraph, VariableDependencyConfig } from '@grafana/scenes';
import { ConditionalRenderingVariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { Combobox, ComboboxOption, Field, Input, Stack, useStyles2 } from '@grafana/ui';
import { Box, Combobox, ComboboxOption, 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 VariableConditionValue = {
name: string;
operator: '=' | '!=';
value: string;
};
import {
ConditionalRenderingSerializerRegistryItem,
VariableConditionValue,
VariableConditionValueOperator,
} from './types';
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.variable.label', 'Variable');
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: this.getItemType() }
);
}
protected _variableDependency = new VariableDependencyConfig(this, {
onAnyVariableChanged: (v) => {
if (v.state.name === this.state.value.name) {
this.getConditionalLogicRoot().notifyChange();
this.notifyChange();
}
},
});
@@ -37,25 +48,19 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase<Condi
return true;
}
const variable = sceneGraph.getVariables(this).state.variables.find((v) => v.state.name === this.state.value.name);
const variable = sceneGraph.getVariables(this).getByName(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;
const variableValue = variable.getValue() ?? '';
if (this.state.value.operator === '!=') {
hit = !hit;
}
const hit = Array.isArray(variableValue)
? variableValue.includes(this.state.value.value)
: variableValue === this.state.value.value;
return hit;
}
public render(): ReactNode {
return <ConditionalRenderingVariableRenderer model={this} />;
return this.state.value.operator === '!=' ? !hit : hit;
}
public serialize(): ConditionalRenderingVariableKind {
@@ -69,70 +74,67 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase<Condi
};
}
public onDelete() {
handleDeleteNonGroupCondition(this);
public static deserialize(model: ConditionalRenderingVariableKind): ConditionalRenderingVariable {
return new ConditionalRenderingVariable({
value: {
name: model.spec.variable,
operator: model.spec.operator === 'equals' ? '=' : '!=',
value: model.spec.value,
},
});
}
public static createEmpty(name: string): ConditionalRenderingVariable {
return new ConditionalRenderingVariable({ value: { name, operator: '=', value: '' } });
}
}
function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps<ConditionalRenderingVariable>) {
const { value } = model.useState();
const variables = useMemo(() => sceneGraph.getVariables(model), [model]);
const variableNames = useMemo(
const variableNames: Array<ComboboxOption<string>> = 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(
const operatorOptions: Array<ComboboxOption<VariableConditionValueOperator>> = useMemo(
() => [
{ value: '=', description: t('dashboard.conditional-rendering.variable.operator.equals', 'Equals') },
{ value: '!=', description: t('dashboard.conditional-rendering.variable.operator.not-equal', 'Not equal') },
{ value: '=', description: t('dashboard.conditional-rendering.conditions.variable.operator.equals', 'Equals') },
{
value: '!=',
description: t('dashboard.conditional-rendering.conditions.variable.operator.not-equals', 'Not equals'),
},
],
[]
);
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 } })}
<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) => model.setStateAndNotify({ value: { ...value, name: option.value } })}
/>
</Field>
</Box>
<Combobox
width="auto"
minWidth={10}
options={operatorOptions}
value={value.operator}
onChange={(option) => model.setStateAndNotify({ value: { ...value, operator: option.value } })}
/>
</Stack>
<Input
placeholder={t('dashboard.conditional-rendering.conditions.variable.value', 'Value')}
value={value.value}
onChange={(e) => model.setStateAndNotify({ value: { ...value, value: e.currentTarget.value } })}
/>
</Stack>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
variableNameSelect: css({
flexGrow: 1,
}),
operatorSelect: css({
width: theme.spacing(12),
}),
});
@@ -1,96 +1,16 @@
import { Registry, RegistryItem } from '@grafana/data';
import {
ConditionalRenderingGroupKind,
ConditionalRenderingVariableKind,
ConditionalRenderingDataKind,
ConditionalRenderingTimeIntervalKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { Registry } from '@grafana/data';
import { ConditionalRenderingData } from './ConditionalRenderingData';
import { ConditionalRenderingGroup } from './ConditionalRenderingGroup';
import { ConditionalRenderingInterval } from './ConditionalRenderingInterval';
import { ConditionalRenderingTimeRangeSize } from './ConditionalRenderingTimeRangeSize';
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,
});
}
}
import { ConditionalRenderingSerializerRegistryItem } from './types';
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(),
},
ConditionalRenderingGroup.serializer,
ConditionalRenderingVariable.serializer,
ConditionalRenderingData.serializer,
ConditionalRenderingTimeRangeSize.serializer,
];
});
@@ -1,25 +0,0 @@
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();
}
};
@@ -0,0 +1,53 @@
import { RegistryItem } from '@grafana/data';
import {
ConditionalRenderingDataKind,
ConditionalRenderingGroupKind,
ConditionalRenderingTimeRangeSizeKind,
ConditionalRenderingVariableKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
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;
}
@@ -0,0 +1,22 @@
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 />,
];
}
@@ -0,0 +1,19 @@
import { SceneObject } from '@grafana/scenes';
import { AutoGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem';
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';
}
@@ -24,7 +24,12 @@ import { getDashboardSceneFor } from '../utils/utils';
import { DashboardOutline } from './DashboardOutline';
import { ElementEditPane } from './ElementEditPane';
import { ElementSelection } from './ElementSelection';
import { NewObjectAddedToCanvasEvent, ObjectRemovedFromCanvasEvent, ObjectsReorderedOnCanvasEvent } from './shared';
import {
ConditionalRenderingChangedEvent,
NewObjectAddedToCanvasEvent,
ObjectRemovedFromCanvasEvent,
ObjectsReorderedOnCanvasEvent,
} from './shared';
import { useEditableElement } from './useEditableElement';
export interface DashboardEditPaneState extends SceneObjectState {
@@ -66,6 +71,12 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
this.forceRender();
})
);
this._subs.add(
dashboard.subscribeToEvent(ConditionalRenderingChangedEvent, ({ payload }) => {
this.forceRender();
})
);
}
public enableSelection() {
@@ -66,3 +66,7 @@ export class ObjectRemovedFromCanvasEvent extends BusEventWithPayload<SceneObjec
export class ObjectsReorderedOnCanvasEvent extends BusEventWithPayload<SceneObject> {
static type = 'objects-reordered-on-canvas';
}
export class ConditionalRenderingChangedEvent extends BusEventWithPayload<SceneObject> {
static type = 'conditional-rendering-changed';
}
@@ -17,7 +17,7 @@ import { createWorker } from 'app/features/dashboard-scene/saving/createDetectCh
import { ConditionalRenderingData } from '../conditional-rendering/ConditionalRenderingData';
import { ConditionalRenderingGroup } from '../conditional-rendering/ConditionalRenderingGroup';
import { ConditionalRenderingInterval } from '../conditional-rendering/ConditionalRenderingInterval';
import { ConditionalRenderingTimeRangeSize } from '../conditional-rendering/ConditionalRenderingTimeRangeSize';
import { ConditionalRenderingVariable } from '../conditional-rendering/ConditionalRenderingVariable';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls';
@@ -150,7 +150,7 @@ export class DashboardSceneChangeTracker {
if (payload.changedObject instanceof ConditionalRenderingVariable) {
return true;
}
if (payload.changedObject instanceof ConditionalRenderingInterval) {
if (payload.changedObject instanceof ConditionalRenderingTimeRangeSize) {
return true;
}
if (payload.changedObject instanceof ConditionalRenderingGroup) {
@@ -4,7 +4,8 @@ import { GrafanaTheme2 } from '@grafana/data/';
import { SceneComponentProps } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import { useDashboardState, useIsConditionallyHidden } from '../../utils/utils';
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
import { useDashboardState } from '../../utils/utils';
import { AutoGridItem } from './ResponsiveGridItem';
import { DRAGGED_ITEM_HEIGHT, DRAGGED_ITEM_LEFT, DRAGGED_ITEM_TOP, DRAGGED_ITEM_WIDTH } from './const';
@@ -13,7 +14,8 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
const { body, repeatedPanels, key } = model.useState();
const { draggingKey } = model.getParentGrid().useState();
const { isEditing } = useDashboardState(model);
const isConditionallyHidden = useIsConditionallyHidden(model);
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] =
useIsConditionallyHidden(model);
const styles = useStyles2(getStyles);
if (isConditionallyHidden && !isEditing) {
@@ -26,11 +28,9 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
return repeatedPanels ? (
<>
{repeatedPanels.map((item) => (
<div
className={cx(isConditionallyHidden && 'dashboard-visible-hidden-element', styles.wrapper)}
key={item.state.key}
>
<div className={cx(conditionalRenderingClass, styles.wrapper)} key={item.state.key}>
<item.Component model={item} />
{conditionalRenderingOverlay}
</div>
))}
</>
@@ -38,14 +38,9 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
<div ref={model.containerRef} data-auto-grid-item-drop-target={isDragging ? key : undefined}>
{isDragged && <div className={styles.draggedPlaceholder} />}
<div
className={cx(
isConditionallyHidden && 'dashboard-visible-hidden-element',
styles.wrapper,
isDragged && styles.draggedWrapper
)}
>
<div className={cx(!isDragged && conditionalRenderingClass, styles.wrapper, isDragged && styles.draggedWrapper)}>
<body.Component model={body} />
{conditionalRenderingOverlay}
</div>
</div>
);
@@ -8,8 +8,9 @@ import { SceneComponentProps } from '@grafana/scenes';
import { clearButtonStyles, Icon, Tooltip, useElementSelection, usePointerDistance, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
import { useIsClone } from '../../utils/clone';
import { useDashboardState, useInterpolatedTitle, useIsConditionallyHidden } from '../../utils/utils';
import { useDashboardState, useInterpolatedTitle } from '../../utils/utils';
import { DashboardScene } from '../DashboardScene';
import { RowItem } from './RowItem';
@@ -18,7 +19,8 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
const { layout, collapse: isCollapsed, fillScreen, hideHeader: isHeaderHidden, isDropTarget, key } = model.useState();
const isClone = useIsClone(model);
const { isEditing } = useDashboardState(model);
const isConditionallyHidden = useIsConditionallyHidden(model);
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] =
useIsConditionallyHidden(model);
const { isSelected, onSelect, isSelectable } = useElementSelection(key);
const title = useInterpolatedTitle(model);
const { rows } = model.getParentLayout().useState();
@@ -54,12 +56,13 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
data-dashboard-drop-target-key={model.state.key}
className={cx(
styles.wrapper,
!isCollapsed && styles.wrapperNotCollapsed,
dragSnapshot.isDragging && styles.dragging,
isEditing && !isCollapsed && styles.wrapperEditing,
isEditing && isCollapsed && styles.wrapperEditingCollapsed,
isCollapsed && styles.wrapperCollapsed,
shouldGrow && styles.wrapperGrow,
isConditionallyHidden && 'dashboard-visible-hidden-element',
conditionalRenderingClass,
!isClone && isSelected && 'dashboard-selected-element',
!isClone && !isSelected && selectableHighlight && 'dashboard-selectable-element',
isDropTarget && 'dashboard-drop-target'
@@ -86,11 +89,7 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
>
{(!isHeaderHidden || isEditing) && (
<div
className={cx(
isHeaderHidden && 'dashboard-visible-hidden-element',
styles.rowHeader,
'dashboard-row-header'
)}
className={cx(styles.rowHeader, 'dashboard-row-header')}
onMouseEnter={isSelectable ? onHeaderEnter : undefined}
onMouseLeave={isSelectable ? onHeaderLeave : undefined}
{...dragProvided.dragHandleProps}
@@ -129,6 +128,7 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
</div>
)}
{!isCollapsed && <layout.Component model={layout} />}
{conditionalRenderingOverlay}
</div>
)}
</Draggable>
@@ -170,6 +170,11 @@ function getStyles(theme: GrafanaTheme2) {
}),
rowTitleHidden: css({
textDecoration: 'line-through',
opacity: 0.6,
'&:hover': css({
opacity: 1,
}),
}),
rowTitleNested: css({
fontSize: theme.typography.body.fontSize,
@@ -183,9 +188,12 @@ function getStyles(theme: GrafanaTheme2) {
flexDirection: 'column',
width: '100%',
minHeight: '100px',
}),
wrapperNotCollapsed: css({
'> div:nth-child(2)': {
marginLeft: theme.spacing(3),
position: 'relative',
'&:before': {
content: '""',
position: 'absolute',
@@ -15,6 +15,7 @@ import store from 'app/core/store';
import kbn from 'app/core/utils/kbn';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
import { serializeTab } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
import { getElements } from '../../serialization/layoutSerializers/utils';
import { getDashboardSceneFor, getDefaultVizPanel } from '../../utils/utils';
@@ -41,6 +42,7 @@ export interface TabItemState extends SceneObjectState {
*/
isNew?: boolean;
isDropTarget?: boolean;
conditionalRendering?: ConditionalRendering;
}
export class TabItem
@@ -64,7 +66,20 @@ 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(),
});
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
const deactivate = this.state.conditionalRendering?.activate();
return () => {
if (deactivate) {
deactivate();
}
};
}
public getEditableElementInfo(): EditableDashboardElementInfo {
@@ -5,6 +5,7 @@ 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 { useConditionalRenderingEditor } from '../../conditional-rendering/ConditionalRenderingEditor';
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
import { useEditPaneInputAutoFocus } from '../layouts-shared/utils';
@@ -26,7 +27,18 @@ export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[]
const layoutCategory = useLayoutCategory(layout);
return [tabCategory, layoutCategory];
const editOptions = [tabCategory, layoutCategory];
const conditionalRenderingCategory = useMemo(
() => useConditionalRenderingEditor(model.state.conditionalRendering),
[model]
);
if (conditionalRenderingCategory) {
editOptions.push(conditionalRenderingCategory);
}
return editOptions;
}
function TabTitleInput({ tab }: { tab: TabItem }) {
@@ -6,6 +6,7 @@ import { locationUtil, textUtil } from '@grafana/data';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { Tab, useElementSelection, usePointerDistance, useStyles2 } from '@grafana/ui';
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
import { useDashboardState } from '../../utils/utils';
import { TabItem } from './TabItem';
@@ -25,6 +26,11 @@ export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
const href = textUtil.sanitize(locationUtil.getUrlForPartial(location, { [urlKey]: mySlug }));
const styles = useStyles2(getStyles);
const pointerDistance = usePointerDistance();
const [isConditionallyHidden] = useIsConditionallyHidden(model);
if (isConditionallyHidden && !isEditing && !isActive) {
return null;
}
return (
<Draggable key={key!} draggableId={key!} index={myIndex} isDragDisabled={!isEditing}>
@@ -39,6 +45,7 @@ export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
ref={model.containerRef}
truncate
className={cx(
isConditionallyHidden && styles.hidden,
isSelected && 'dashboard-selected-element',
isSelectable && !isSelected && 'dashboard-selectable-element',
isDropTarget && 'dashboard-drop-target'
@@ -74,4 +81,11 @@ const getStyles = () => ({
dragging: css({
cursor: 'move',
}),
hidden: css({
opacity: 0.4,
'&:hover': css({
opacity: 1,
}),
}),
});
@@ -7,6 +7,7 @@ import { TabItem } from '../../scene/layout-tabs/TabItem';
import { TabsLayoutManager } from '../../scene/layout-tabs/TabsLayoutManager';
import { layoutDeserializerRegistry } from './layoutSerializerRegistry';
import { getConditionalRendering } from './utils';
export function serializeTabsLayout(layoutManager: TabsLayoutManager): DashboardV2Spec['layout'] {
return {
@@ -19,13 +20,21 @@ export function serializeTabsLayout(layoutManager: TabsLayoutManager): Dashboard
export function serializeTab(tab: TabItem): TabsLayoutTabKind {
const layout = tab.state.layout.serialize();
return {
const tabKind: TabsLayoutTabKind = {
kind: 'TabsLayoutTab',
spec: {
title: tab.state.title,
layout: layout,
},
};
const conditionalRenderingRootGroup = tab.state.conditionalRendering?.serialize();
// Only serialize the conditional rendering if it has items
if (conditionalRenderingRootGroup?.spec.items.length) {
tabKind.spec.conditionalRendering = conditionalRenderingRootGroup;
}
return tabKind;
}
export function deserializeTabsLayout(
@@ -55,5 +64,6 @@ export function deserializeTab(
return new TabItem({
title: tab.spec.title,
layout: layoutDeserializerRegistry.get(layout.kind).deserialize(layout, elements, preload, panelIdGenerator),
conditionalRendering: getConditionalRendering(tab),
});
}
@@ -18,6 +18,7 @@ import {
PanelKind,
PanelQueryKind,
QueryVariableKind,
TabsLayoutTabKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
@@ -238,13 +239,16 @@ export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['
return sceneState.serialize();
}
export function getConditionalRendering(item: RowsLayoutRowKind | AutoGridLayoutItemKind): ConditionalRendering {
export function getConditionalRendering(
item: TabsLayoutTabKind | RowsLayoutRowKind | AutoGridLayoutItemKind
): ConditionalRendering {
if (!item.spec.conditionalRendering) {
return ConditionalRendering.createEmpty();
}
const rootGroup = conditionalRenderingSerializerRegistry
.get(item.spec.conditionalRendering.kind)
.serializer.deserialize(item.spec.conditionalRendering);
.deserialize(item.spec.conditionalRendering);
if (rootGroup && !(rootGroup instanceof ConditionalRenderingGroup)) {
throw new Error(`Conditional rendering must always start with a root group`);
@@ -23,8 +23,6 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { AutoGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem';
import { RowItem } from '../scene/layout-rows/RowItem';
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
@@ -456,12 +454,6 @@ export function useDashboardState(scene: SceneObject): DashboardSceneState {
return dashboard.useState();
}
export function useIsConditionallyHidden(scene: RowItem | AutoGridItem): boolean {
const { conditionalRendering } = scene.useState();
return !(conditionalRendering?.evaluate() ?? true);
}
export function useInterpolatedTitle<T extends SceneObjectState & { title?: string }>(scene: SceneObject<T>): string {
const { title } = scene.useState();
+58 -34
View File
@@ -1423,43 +1423,67 @@
"paste-tab": "Paste tab"
},
"conditional-rendering": {
"data": {
"disable": "Disable",
"enable": "Enable",
"label": "Data"
},
"group": {
"add": {
"button": "Add condition based on",
"data": "Data",
"interval": "Interval",
"variable": "Variable value"
"conditions": {
"data": {
"disable": "No data",
"enable": "Has data",
"info": "Show or hide the {{type}} based on query results.",
"label": "Query result"
},
"condition": {
"label": "Evaluate conditions",
"meet-all": "Meet all",
"meet-any": "Meet any"
"group": {
"add": {
"button": "Add rule",
"data": "Query result",
"time-range-size": "Time range less than",
"variable": "Template variable"
},
"condition": {
"all": "Match all (AND)",
"any": "Match any (OR)",
"label": "Match rules"
},
"label": "Group",
"visibility": {
"hide": "Hide",
"label": "{{type}} visibility",
"show": "Show"
}
},
"label": "Group"
},
"interval": {
"input-label": "Value",
"invalid-message": "Invalid interval",
"label": "Time range interval"
},
"shared": {
"delete-condition": "Delete Condition"
},
"title": "Conditional rendering options",
"variable": {
"label": "Variable",
"operator": {
"equals": "Equals",
"not-equal": "Not equal"
"shared": {
"delete-condition": "Delete Condition",
"unsupported-item-type": "This condition is not supported by the element, hence it will be ignored."
},
"select-operator": "Operator",
"select-variable": "Select variable",
"value-input": "Value"
"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",
"label": "Time range less than",
"values": {
"days": "{{value}} days",
"hour": "{{value}} hour",
"hours": "{{value}} hours",
"minutes": "{{value}} minutes",
"months": "{{value}} months",
"year": "{{value}} year",
"years": "{{value}} years"
}
},
"variable": {
"info": "Show or hide the {{type}} dynamically based on the variable value.",
"label": "Template variable",
"name": "Name",
"operator": {
"equals": "Equals",
"not-equals": "Not equals"
},
"value": "Value"
}
},
"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"
}
},
"default-layout": {