From 3c0409fe051f45141fdd295433f8579b1a8eae3d Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Mon, 3 Nov 2025 11:33:44 +0100 Subject: [PATCH] Dashboard Controls: Add annotations to the dashboard controls menu (#112816) * feat: add a placement property to annotations model v2 * chore: update scenes to `v6.42.1` * chore: run `make gen-apps` * fix: cater for cases when there is no data layer * chore: swagger clean * chore: update api clients * fix: correct type guard * fix: display control labels in the default renderer as well for DashboardDataLayerSet --- .../kinds/v2beta1/dashboard_spec.cue | 6 ++ .../dashboard/v0alpha1/dashboard_kind.cue | 7 ++ .../apis/dashboard/v1beta1/dashboard_kind.cue | 7 ++ .../apis/dashboard/v2beta1/dashboard_spec.cue | 6 ++ .../dashboard/v2beta1/dashboard_spec_gen.go | 12 ++- .../dashboard/v2beta1/zz_generated.openapi.go | 7 ++ kinds/dashboard/dashboard_kind.cue | 7 ++ package.json | 4 +- .../src/clients/rtkq/legacy/endpoints.gen.ts | 2 + packages/grafana-schema/src/index.gen.ts | 1 + .../raw/dashboard/x/dashboard_types.gen.ts | 10 +++ .../dashboard/v2beta1/types.spec.gen.ts | 7 ++ pkg/kinds/dashboard/dashboard_spec_gen.go | 7 ++ public/api-merged.json | 4 + .../scene/DashboardControls.tsx | 15 +--- .../scene/DashboardControlsMenu.tsx | 58 +++++++++---- .../scene/DashboardDataLayerControls.tsx | 27 +++++++ .../scene/DashboardDataLayerSet.tsx | 33 +++++--- .../scene/DataLayerControl.tsx | 81 +++++++++++++++++++ .../serialization/annotations.ts | 11 +++ .../serialization/dataLayersToAnnotations.ts | 1 + .../transformSaveModelSchemaV2ToScene.ts | 1 + .../transformSaveModelToScene.ts | 1 + public/openapi3.json | 4 + yarn.lock | 22 ++--- 25 files changed, 288 insertions(+), 53 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/DashboardDataLayerControls.tsx create mode 100644 public/app/features/dashboard-scene/scene/DataLayerControl.tsx diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue index ef1490091c2..bfc23ad87cb 100644 --- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -117,6 +117,10 @@ DashboardLink: { // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu DashboardLinkPlacement: "inControlsMenu" +// Annotation Query placement. Defines where the annotation query should be displayed. +// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu +AnnotationQueryPlacement: "inControlsMenu" + // A topic is attached to DataFrame metadata in query results. // This specifies where the data should be used. DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames="Series|Annotations|AlertStates") @@ -436,6 +440,8 @@ AnnotationQuerySpec: { name: string builtIn?: bool | *false filter?: AnnotationPanelFilter + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: AnnotationQueryPlacement legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling. } diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue index 7d8a0e551bf..9222dad2bc7 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue @@ -177,6 +177,9 @@ lineage: schemas: [{ // Set to 1 for the standard annotation query all dashboards have by default. builtIn?: number | *0 + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: #AnnotationQueryPlacement + // unless datasources have migrated to the target+mapping, // they just spread their query into the base object :( ... @@ -302,6 +305,10 @@ lineage: schemas: [{ // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") + // Annotation Query placement. Defines where the annotation query should be displayed. + // - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu + #AnnotationQueryPlacement: "inControlsMenu" @cuetsy(kind="type") + // Dashboard action type #ActionType: "fetch" | "infinity" @cuetsy(kind="type") diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue index 7d8a0e551bf..9222dad2bc7 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue @@ -177,6 +177,9 @@ lineage: schemas: [{ // Set to 1 for the standard annotation query all dashboards have by default. builtIn?: number | *0 + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: #AnnotationQueryPlacement + // unless datasources have migrated to the target+mapping, // they just spread their query into the base object :( ... @@ -302,6 +305,10 @@ lineage: schemas: [{ // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") + // Annotation Query placement. Defines where the annotation query should be displayed. + // - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu + #AnnotationQueryPlacement: "inControlsMenu" @cuetsy(kind="type") + // Dashboard action type #ActionType: "fetch" | "infinity" @cuetsy(kind="type") diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index 31603c6240b..5b0d436cba0 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -121,6 +121,10 @@ DashboardLink: { // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu DashboardLinkPlacement: "inControlsMenu" +// Annotation Query placement. Defines where the annotation query should be displayed. +// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu +AnnotationQueryPlacement: "inControlsMenu" + // A topic is attached to DataFrame metadata in query results. // This specifies where the data should be used. DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames="Series|Annotations|AlertStates") @@ -440,6 +444,8 @@ AnnotationQuerySpec: { name: string builtIn?: bool | *false filter?: AnnotationPanelFilter + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: AnnotationQueryPlacement legacyOptions?: [string]: _ // Catch-all field for datasource-specific properties. Should not be available in as code tooling. } diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go index 4229e5765d1..8dd81034e3b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -30,6 +30,8 @@ type DashboardAnnotationQuerySpec struct { Name string `json:"name"` BuiltIn *bool `json:"builtIn,omitempty"` Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"` + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + Placement *string `json:"placement,omitempty"` // Catch-all field for datasource-specific properties. Should not be available in as code tooling. LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"` } @@ -37,8 +39,9 @@ type DashboardAnnotationQuerySpec struct { // NewDashboardAnnotationQuerySpec creates a new DashboardAnnotationQuerySpec object. func NewDashboardAnnotationQuerySpec() *DashboardAnnotationQuerySpec { return &DashboardAnnotationQuerySpec{ - Query: *NewDashboardDataQueryKind(), - BuiltIn: (func(input bool) *bool { return &input })(false), + Query: *NewDashboardDataQueryKind(), + BuiltIn: (func(input bool) *bool { return &input })(false), + Placement: (func(input string) *string { return &input })(DashboardAnnotationQueryPlacement), } } @@ -78,6 +81,11 @@ func NewDashboardAnnotationPanelFilter() *DashboardAnnotationPanelFilter { } } +// Annotation Query placement. Defines where the annotation query should be displayed. +// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu +// +k8s:openapi-gen=true +const DashboardAnnotationQueryPlacement = "inControlsMenu" + // "Off" for no shared crosshair or tooltip (default). // "Crosshair" for shared crosshair. // "Tooltip" for shared crosshair AND shared tooltip. diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go index 97fb1c10326..f8108887a27 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go @@ -760,6 +760,13 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAnnotationQuerySpec(ref common.R Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardAnnotationPanelFilter"), }, }, + "placement": { + SchemaProps: spec.SchemaProps{ + Description: "Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.", + Type: []string{"string"}, + Format: "", + }, + }, "legacyOptions": { SchemaProps: spec.SchemaProps{ Description: "Catch-all field for datasource-specific properties. Should not be available in as code tooling.", diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index e32b8b6725e..ce2510d2cfe 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -173,6 +173,9 @@ lineage: schemas: [{ // Set to 1 for the standard annotation query all dashboards have by default. builtIn?: number | *0 + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: #AnnotationQueryPlacement + // unless datasources have migrated to the target+mapping, // they just spread their query into the base object :( ... @@ -298,6 +301,10 @@ lineage: schemas: [{ // - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu #DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type") + // Annotation Query placement. Defines where the annotation query should be displayed. + // - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu + #AnnotationQueryPlacement: "inControlsMenu" @cuetsy(kind="type") + // Dashboard action type #ActionType: "fetch" | "infinity" @cuetsy(kind="type") diff --git a/package.json b/package.json index 672ca9639ed..5d7dd234e25 100644 --- a/package.json +++ b/package.json @@ -296,8 +296,8 @@ "@grafana/plugin-ui": "^0.10.10", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^6.41.0", - "@grafana/scenes-react": "^6.41.0", + "@grafana/scenes": "^6.42.1", + "@grafana/scenes-react": "^6.42.1", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts index bd6ff32f5bf..ce5c5cae8c7 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts @@ -5481,6 +5481,8 @@ export type AnnotationQuery = { iconColor?: string; /** Name of annotation. */ name?: string; + /** Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. */ + placement?: string; target?: AnnotationTarget; /** TODO -- this should not exist here, it is based on the --grafana-- datasource */ type?: string; diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index 87f2a3fd960..038e62a70a2 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -15,6 +15,7 @@ export type { DashboardLink, DashboardLinkType, DashboardLinkPlacement, + AnnotationQueryPlacement, ActionType, FetchOptions, InfinityOptions, diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index 973f10bfb21..3466df584b8 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -106,6 +106,10 @@ export interface AnnotationQuery { * Name of annotation. */ name: string; + /** + * Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + */ + placement?: AnnotationQueryPlacement; /** * TODO.. this should just be a normal query target */ @@ -363,6 +367,12 @@ export type DashboardLinkType = ('link' | 'dashboards'); */ export type DashboardLinkPlacement = 'inControlsMenu'; +/** + * Annotation Query placement. Defines where the annotation query should be displayed. + * - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu + */ +export type AnnotationQueryPlacement = 'inControlsMenu'; + /** * Dashboard action type */ diff --git a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts index e9cbe9e649a..35ccf3cdb1f 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts @@ -18,6 +18,8 @@ export interface AnnotationQuerySpec { name: string; builtIn?: boolean; filter?: AnnotationPanelFilter; + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + placement?: "inControlsMenu"; // Catch-all field for datasource-specific properties. Should not be available in as code tooling. legacyOptions?: Record; } @@ -29,6 +31,7 @@ export const defaultAnnotationQuerySpec = (): AnnotationQuerySpec => ({ iconColor: "", name: "", builtIn: false, + placement: AnnotationQueryPlacement, }); export interface DataQueryKind { @@ -62,6 +65,10 @@ export const defaultAnnotationPanelFilter = (): AnnotationPanelFilter => ({ ids: [], }); +// Annotation Query placement. Defines where the annotation query should be displayed. +// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu +export const AnnotationQueryPlacement = "inControlsMenu"; + // "Off" for no shared crosshair or tooltip (default). // "Crosshair" for shared crosshair. // "Tooltip" for shared crosshair AND shared tooltip. diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index 43877fbca7b..786fabd8de8 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -968,6 +968,8 @@ type AnnotationQuery struct { Type *string `json:"type,omitempty"` // Set to 1 for the standard annotation query all dashboards have by default. BuiltIn *float64 `json:"builtIn,omitempty"` + // Placement can be used to display the annotation query somewhere else on the dashboard other than the default location. + Placement *string `json:"placement,omitempty"` } // NewAnnotationQuery creates a new AnnotationQuery object. @@ -977,6 +979,7 @@ func NewAnnotationQuery() *AnnotationQuery { Enable: true, Hide: (func(input bool) *bool { return &input })(false), BuiltIn: (func(input float64) *float64 { return &input })(0), + Placement: (func(input string) *string { return &input })(AnnotationQueryPlacement), } } @@ -1019,6 +1022,10 @@ func NewAnnotationTarget() *AnnotationTarget { } } +// Annotation Query placement. Defines where the annotation query should be displayed. +// - "inControlsMenu" renders the annotation query in the dashboard controls dropdown menu +const AnnotationQueryPlacement = "inControlsMenu" + // A dashboard snapshot shares an interactive dashboard publicly. // It is a read-only version of a dashboard, and is not editable. // It is possible to create a snapshot of a snapshot. diff --git a/public/api-merged.json b/public/api-merged.json index f6ca6d73502..330a563bde1 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -13735,6 +13735,10 @@ "description": "Name of annotation.", "type": "string" }, + "placement": { + "description": "Placement can be used to display the annotation query somewhere else on the dashboard other than the default location.", + "type": "string" + }, "target": { "$ref": "#/definitions/AnnotationTarget" }, diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 921874732eb..767b0a934e2 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -21,6 +21,7 @@ import { PanelEditControls } from '../panel-edit/PanelEditControls'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardControlsButton } from './DashboardControlsMenu'; +import { DashboardDataLayerControls } from './DashboardDataLayerControls'; import { DashboardLinksControls } from './DashboardLinksControls'; import { DashboardScene } from './DashboardScene'; import { VariableControls } from './VariableControls'; @@ -166,7 +167,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps - + )} @@ -189,18 +190,6 @@ function DashboardControlsRenderer({ model }: SceneComponentProps - {layers.map((layer) => ( - - ))} - - ); -} - function renderHiddenVariables(dashboard: DashboardScene) { const { variables } = sceneGraph.getVariables(dashboard).useState(); const renderAsHiddenVariables = variables.filter((v) => v.UNSAFE_renderAsHidden); diff --git a/public/app/features/dashboard-scene/scene/DashboardControlsMenu.tsx b/public/app/features/dashboard-scene/scene/DashboardControlsMenu.tsx index bee89dc8565..d74717c9285 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControlsMenu.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControlsMenu.tsx @@ -2,12 +2,14 @@ import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { sceneGraph, SceneVariable } from '@grafana/scenes'; +import { SceneDataLayerProvider, sceneGraph, SceneVariable } from '@grafana/scenes'; import { DashboardLink, VariableHide } from '@grafana/schema'; import { Box, Dropdown, Menu, ToolbarButton, useStyles2 } from '@grafana/ui'; +import { isDashboardDataLayerSetState } from './DashboardDataLayerSet'; import { DashboardLinkRenderer } from './DashboardLinkRenderer'; import { DashboardScene } from './DashboardScene'; +import { DataLayerControl } from './DataLayerControl'; import { VariableValueSelectWrapper } from './VariableControls'; export const DASHBOARD_CONTROLS_MENU_ARIA_LABEL = 'Dashboard controls menu'; @@ -24,15 +26,25 @@ export function DashboardControlsButton({ dashboard }: { dashboard: DashboardSce .getVariables(dashboard)! .useState() .variables.filter((v) => v.state.hide === VariableHide.inControlsMenu); + const dataState = sceneGraph.getData(dashboard).useState(); + const annotationLayers = isDashboardDataLayerSetState(dataState) ? dataState.annotationLayers : []; + const filteredAnnotationLayers = annotationLayers.filter((layer) => layer.state.placement === 'inControlsMenu'); - if ((variables.length === 0 && filteredLinks.length === 0) || !uid) { + if ((variables.length === 0 && filteredLinks.length === 0 && filteredAnnotationLayers.length === 0) || !uid) { return null; } return ( } + overlay={ + + } > ))} - {variables.length > 0 && links.length > 0 && ( -
- -
- )} + {/* Annotation layers */} + {annotationLayers.length > 0 && + annotationLayers.map((layer, index) => ( +
0 && styles.variableItem)} key={layer.state.key}> + +
+ ))} {/* Links */} - {links.map((link, index) => ( -
- -
- ))} + {links.length > 0 && ( + <> + {(variables.length > 0 || annotationLayers.length > 0) && } + {links.map((link, index) => ( +
+ +
+ ))} + + )}
); } +function MenuDivider() { + const styles = useStyles2(getStyles); + + return ( +
+ +
+ ); +} + const getStyles = (theme: GrafanaTheme2) => ({ divider: css({ marginTop: theme.spacing(2), diff --git a/public/app/features/dashboard-scene/scene/DashboardDataLayerControls.tsx b/public/app/features/dashboard-scene/scene/DashboardDataLayerControls.tsx new file mode 100644 index 00000000000..1d5ef0f3866 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DashboardDataLayerControls.tsx @@ -0,0 +1,27 @@ +import { SceneDataLayerProvider, sceneGraph } from '@grafana/scenes'; + +import { isDashboardDataLayerSetState } from './DashboardDataLayerSet'; +import { DashboardScene } from './DashboardScene'; +import { DataLayerControl } from './DataLayerControl'; + +// Renders data layer controls for a dashboard +export function DashboardDataLayerControls({ dashboard }: { dashboard: DashboardScene }) { + // We are not using the default renderer of the data objects here, because the information of where the controls + // should be rendered (`.placement`) are set on the underlying annotation layer objects. + const state = sceneGraph.getData(dashboard).useState(); + // It is possible to render the controls for the annotation data layers in separate places using the `placement` property. + // In case it's not specified, we are rendering the controls here (default). + const isDefaultPlacement = (layer: SceneDataLayerProvider) => layer.state.placement === undefined; + + if (isDashboardDataLayerSetState(state)) { + return ( + <> + {state.annotationLayers.filter(isDefaultPlacement).map((layer) => ( + + ))} + + ); + } + + return null; +} diff --git a/public/app/features/dashboard-scene/scene/DashboardDataLayerSet.tsx b/public/app/features/dashboard-scene/scene/DashboardDataLayerSet.tsx index 49c51efedc8..3293d70f732 100644 --- a/public/app/features/dashboard-scene/scene/DashboardDataLayerSet.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardDataLayerSet.tsx @@ -6,6 +6,7 @@ import { } from '@grafana/scenes'; import { AlertStatesDataLayer } from './AlertStatesDataLayer'; +import { DataLayerControl } from './DataLayerControl'; export interface DashboardDataLayerSetState extends SceneDataLayerProviderState { alertStatesLayer?: AlertStatesDataLayer; @@ -16,6 +17,8 @@ export class DashboardDataLayerSet extends SceneDataLayerSetBase implements SceneDataLayerProvider { + public static Component = DashboardDataLayerSetRenderer; + public constructor(state: Partial) { super({ ...state, @@ -56,16 +59,24 @@ export class DashboardDataLayerSet return layers; } +} - public static Component = ({ model }: SceneComponentProps) => { - const { annotationLayers } = model.useState(); - - return ( - <> - {annotationLayers.map((layer) => ( - - ))} - - ); - }; +function DashboardDataLayerSetRenderer({ model }: SceneComponentProps) { + const { annotationLayers } = model.useState(); + + return ( + <> + {annotationLayers.map((layer) => ( + + ))} + + ); +} + +export function isDashboardDataLayerSetState(data: unknown): data is DashboardDataLayerSetState { + if (data && typeof data === 'object') { + return 'annotationLayers' in data; + } + + return false; } diff --git a/public/app/features/dashboard-scene/scene/DataLayerControl.tsx b/public/app/features/dashboard-scene/scene/DataLayerControl.tsx new file mode 100644 index 00000000000..8bc3f2f5c5b --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DataLayerControl.tsx @@ -0,0 +1,81 @@ +import { css } from '@emotion/css'; + +import { LoadingState, GrafanaTheme2 } from '@grafana/data'; +import { ControlsLabel, SceneDataLayerProvider } from '@grafana/scenes'; +import { useStyles2 } from '@grafana/ui'; + +export type Props = { + layer: SceneDataLayerProvider; + // Set to true if the control is rendered inside a drop-down menu + inMenu?: boolean; +}; + +// Renders the controls for a single data layer +export function DataLayerControl({ layer, inMenu }: Props) { + const elementId = `data-layer-${layer.state.key}`; + const { data, isHidden } = layer.useState(); + const showLoading = Boolean(data && data.state === LoadingState.Loading); + const styles = useStyles2(getStyles); + + if (isHidden) { + return null; + } + + if (inMenu) { + return ( +
+
+ +
+ layer.cancelQuery?.()} + label={layer.state.name} + description={layer.state.description} + error={layer.state.data?.errors?.[0].message} + layout={'vertical'} + className={styles.menuLabel} + /> +
+ ); + } + + return ( +
+ layer.cancelQuery?.()} + label={layer.state.name} + description={layer.state.description} + error={layer.state.data?.errors?.[0].message} + /> + +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ + display: 'flex', + }), + menuContainer: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }), + controlWrapper: css({ + '& > div': { + border: 'none', + background: 'transparent', + '&:hover': { + border: 'none', + background: 'transparent', + }, + }, + }), + menuLabel: css({ + marginTop: theme.spacing(0.5), + }), +}); diff --git a/public/app/features/dashboard-scene/serialization/annotations.ts b/public/app/features/dashboard-scene/serialization/annotations.ts index 4e9d296f46c..63aa4002675 100644 --- a/public/app/features/dashboard-scene/serialization/annotations.ts +++ b/public/app/features/dashboard-scene/serialization/annotations.ts @@ -26,6 +26,7 @@ export function transformV1ToV2AnnotationQuery( target, snapshotData, type, + placement, // unknown properties that are still available for configuration through API ...legacyOptions @@ -49,6 +50,11 @@ export function transformV1ToV2AnnotationQuery( }, }; + // Add placement if it exists + if (annotation.placement) { + result.spec.placement = annotation.placement; + } + if (dsUID) { result.spec.query.datasource = { name: dsUID, @@ -81,6 +87,11 @@ export function transformV2ToV1AnnotationQuery(annotation: AnnotationQueryKind): // TOOO: mappings }; + // Add placement if it exists + if (annotation.spec.placement) { + annoQuerySpec.placement = annotation.spec.placement; + } + if (Object.keys(dataQuery.spec).length > 0) { // @ts-expect-error DataQueryKind spec should be typed as DataQuery interface annoQuerySpec.target = { diff --git a/public/app/features/dashboard-scene/serialization/dataLayersToAnnotations.ts b/public/app/features/dashboard-scene/serialization/dataLayersToAnnotations.ts index dd16c946c0e..06b5b47c060 100644 --- a/public/app/features/dashboard-scene/serialization/dataLayersToAnnotations.ts +++ b/public/app/features/dashboard-scene/serialization/dataLayersToAnnotations.ts @@ -12,6 +12,7 @@ export function dataLayersToAnnotations(layers: SceneDataLayerProvider[]) { ...layer.state.query, enable: Boolean(layer.state.isEnabled), hide: Boolean(layer.state.isHidden), + placement: layer.state.placement, }; annotations.push(result); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 7b98e81c29e..2a2f96caa30 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -117,6 +117,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo