From 1fe32479d7622394215f1a27a621f10441dd97c6 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:06:46 +0200 Subject: [PATCH] Scenes: Annotations functionality in settings (#81361) * wip listing/viewing annotations * list annotations in settings * fix tests * wip edit mode * PR mods * move, delete, edit, add new * edit annotation in settings * Annotations functionality * revert change * add ui tests, move angularEditorLoader * remove flaky test * refactor * bump scenes version, refactor getVizPanels, refactor edit page * fix nav breadcrumbs * annotation set dirty flag, add overlay to edit view * PR mods * PR mods * remove flaky test * change dirty flag setting logic for anotations * change button variants --- .../src/selectors/pages.ts | 5 + .../dashboard-scene/scene/DashboardScene.tsx | 10 + .../settings/AnnotationsEditView.test.tsx | 104 +++++- .../settings/AnnotationsEditView.tsx | 199 ++++++++--- .../annotations}/AngularEditorLoader.tsx | 0 .../AnnotationSettingsEdit.test.tsx | 226 ++++++++++++ .../annotations/AnnotationSettingsEdit.tsx | 321 ++++++++++++++++++ .../AnnotationSettingsList.test.tsx | 139 ++++++++ .../annotations/AnnotationSettingsList.tsx | 21 +- .../settings/annotations/index.tsx | 1 + .../utils/dashboardSceneGraph.test.ts | 101 +++++- .../utils/dashboardSceneGraph.ts | 44 ++- .../AnnotationSettingsEdit.tsx | 3 +- 13 files changed, 1112 insertions(+), 62 deletions(-) rename public/app/features/{dashboard/components/AnnotationSettings => dashboard-scene/settings/annotations}/AngularEditorLoader.tsx (100%) create mode 100644 public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.test.tsx create mode 100644 public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx create mode 100644 public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsList.test.tsx diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 20bb47d9029..4acc843136e 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -95,6 +95,7 @@ export const Pages = { */ addAnnotationCTA: Components.CallToActionCard.button('Add annotation query'), addAnnotationCTAV2: Components.CallToActionCard.buttonV2('Add annotation query'), + annotations: 'data-testid list-annotations', }, Settings: { name: 'Annotations settings name input', @@ -103,6 +104,10 @@ export const Pages = { panelFilterSelect: 'data-testid annotations-panel-filter', showInLabel: 'show-in-label', previewInDashboard: 'data-testid annotations-preview', + delete: 'data-testid annotations-delete', + apply: 'data-testid annotations-apply', + enable: 'data-testid annotation-enable', + hide: 'data-testid annotation-hide', }, }, Variables: { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 3ab4222078d..7f1c7706f1a 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -6,7 +6,9 @@ import { Unsubscribable } from 'rxjs'; import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil, textUtil } from '@grafana/data'; import { locationService, config } from '@grafana/runtime'; import { + dataLayers, getUrlSyncManager, + SceneDataLayers, SceneFlexLayout, sceneGraph, SceneGridItem, @@ -373,6 +375,14 @@ export class DashboardScene extends SceneObjectBase { this.setIsDirty(); } } + if (event.payload.changedObject instanceof SceneDataLayers) { + this.setIsDirty(); + } + if (event.payload.changedObject instanceof dataLayers.AnnotationsDataLayer) { + if (!Object.prototype.hasOwnProperty.call(event.payload.partialUpdate, 'data')) { + this.setIsDirty(); + } + } if (event.payload.changedObject instanceof SceneGridItem) { this.setIsDirty(); } diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx index 5ac0d3c824d..0ed532e2f70 100644 --- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.test.tsx @@ -1,16 +1,17 @@ import { map, of } from 'rxjs'; -import { DataQueryRequest, DataSourceApi, LoadingState, PanelData } from '@grafana/data'; -import { SceneDataLayers, SceneGridItem, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { AnnotationQuery, DataQueryRequest, DataSourceApi, LoadingState, PanelData } from '@grafana/data'; +import { SceneDataLayers, SceneGridItem, SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardScene } from '../scene/DashboardScene'; +import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { activateFullSceneTree } from '../utils/test-utils'; -import { AnnotationsEditView } from './AnnotationsEditView'; +import { AnnotationsEditView, MoveDirection } from './AnnotationsEditView'; +import { newAnnotationName } from './annotations/AnnotationSettingsEdit'; -const getDataSourceSrvSpy = jest.fn(); const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => { const result: PanelData = { state: LoadingState.Loading, @@ -31,7 +32,9 @@ const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getDataSourceSrv: () => { - getDataSourceSrvSpy(); + return { + getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }), + }; }, getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => { return runRequestMock(ds, request); @@ -44,26 +47,103 @@ jest.mock('@grafana/runtime', () => ({ describe('AnnotationsEditView', () => { describe('Dashboard annotations state', () => { let annotationsView: AnnotationsEditView; + let dashboardScene: DashboardScene; beforeEach(async () => { const result = await buildTestScene(); annotationsView = result.annotationsView; + dashboardScene = result.dashboard; }); it('should return the correct urlKey', () => { expect(annotationsView.getUrlKey()).toBe('annotations'); }); - it('should return the scene data layers', () => { - const dataLayers = annotationsView.getSceneDataLayers(); - - expect(dataLayers).toBeInstanceOf(SceneDataLayers); - expect(dataLayers?.state.layers.length).toBe(2); - }); - it('should return the annotations length', () => { expect(annotationsView.getAnnotationsLength()).toBe(1); }); + + it('should return 0 if no annotations', () => { + dashboardScene.setState({ + $data: new SceneDataLayers({ layers: [] }), + }); + + expect(annotationsView.getAnnotationsLength()).toBe(0); + }); + + it('should add a new annotation and group it with the other annotations', () => { + const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard()); + + expect(dataLayers?.state.layers.length).toBe(2); + + annotationsView.onNew(); + + expect(dataLayers?.state.layers.length).toBe(3); + expect(dataLayers?.state.layers[1].state.name).toBe(newAnnotationName); + expect(dataLayers?.state.layers[1].isActive).toBe(true); + }); + + it('should move an annotation up one position', () => { + const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard()); + + annotationsView.onNew(); + + expect(dataLayers?.state.layers.length).toBe(3); + expect(dataLayers?.state.layers[0].state.name).toBe('test'); + + annotationsView.onMove(1, MoveDirection.UP); + + expect(dataLayers?.state.layers.length).toBe(3); + expect(dataLayers?.state.layers[0].state.name).toBe(newAnnotationName); + }); + + it('should move an annotation down one position', () => { + const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard()); + + annotationsView.onNew(); + + expect(dataLayers?.state.layers.length).toBe(3); + expect(dataLayers?.state.layers[0].state.name).toBe('test'); + + annotationsView.onMove(0, MoveDirection.DOWN); + + expect(dataLayers?.state.layers.length).toBe(3); + expect(dataLayers?.state.layers[0].state.name).toBe(newAnnotationName); + }); + + it('should delete annotation at index', () => { + const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard()); + + expect(dataLayers?.state.layers.length).toBe(2); + + annotationsView.onDelete(0); + + expect(dataLayers?.state.layers.length).toBe(1); + expect(dataLayers?.state.layers[0].state.name).toBe('Alert States'); + }); + + it('should update an annotation at index', () => { + const dataLayers = dashboardSceneGraph.getDataLayers(annotationsView.getDashboard()); + + expect(dataLayers?.state.layers[0].state.name).toBe('test'); + + const annotation: AnnotationQuery = { + ...(dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query, + }; + + annotation.name = 'new name'; + annotation.hide = true; + annotation.enable = false; + annotation.iconColor = 'blue'; + annotationsView.onUpdate(annotation, 0); + + expect(dataLayers?.state.layers.length).toBe(2); + expect(dataLayers?.state.layers[0].state.name).toBe('new name'); + expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.name).toBe('new name'); + expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.hide).toBe(true); + expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.enable).toBe(false); + expect((dataLayers?.state.layers[0] as dataLayers.AnnotationsDataLayer).state.query.iconColor).toBe('blue'); + }); }); }); diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx index a7b78675914..131b72c98bb 100644 --- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx @@ -1,24 +1,26 @@ import React from 'react'; -import { AnnotationQuery, DataTopic, PageLayoutType } from '@grafana/data'; -import { - SceneComponentProps, - SceneDataLayerProvider, - SceneDataLayers, - SceneObjectBase, - sceneGraph, -} from '@grafana/scenes'; +import { AnnotationQuery, DataTopic, NavModel, NavModelItem, PageLayoutType, getDataSourceRef } from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { SceneComponentProps, SceneObjectBase, VizPanel, dataLayers } from '@grafana/scenes'; import { Page } from 'app/core/components/Page/Page'; +import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardScene } from '../scene/DashboardScene'; import { NavToolbarActions } from '../scene/NavToolbarActions'; import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations'; +import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync'; -import { AnnotationSettingsList } from './annotations'; +import { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } from './annotations'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; +export enum MoveDirection { + UP = -1, + DOWN = 1, +} + export interface AnnotationsEditViewState extends DashboardEditViewState { editIndex?: number | undefined; } @@ -36,22 +38,21 @@ export class AnnotationsEditView extends SceneObjectBase layer.topic === DataTopic.Annotations).length; + return dashboardSceneGraph + .getDataLayers(this._dashboard) + .state.layers.filter((layer) => layer.topic === DataTopic.Annotations).length; } public getDashboard(): DashboardScene { @@ -59,49 +60,167 @@ export class AnnotationsEditView extends SceneObjectBase { - console.log('todo: onNew'); + const newAnnotationQuery: AnnotationQuery = { + name: newAnnotationName, + enable: true, + datasource: getDataSourceRef(getDataSourceSrv().getInstanceSettings(null)!), + iconColor: 'red', + }; + + const newAnnotation = new DashboardAnnotationsDataLayer({ + key: `annotations-${newAnnotationQuery.name}`, + query: newAnnotationQuery, + name: newAnnotationQuery.name, + isEnabled: Boolean(newAnnotationQuery.enable), + isHidden: Boolean(newAnnotationQuery.hide), + }); + + const data = dashboardSceneGraph.getDataLayers(this._dashboard); + + const layers = [...data.state.layers]; + + //keep annotation layers together + layers.splice(this.getAnnotationsLength(), 0, newAnnotation); + + data.setState({ + layers, + }); + + newAnnotation.activate(); + + this.setState({ editIndex: this.getAnnotationsLength() - 1 }); }; public onEdit = (idx: number) => { - console.log('todo: onEdit'); + this.setState({ editIndex: idx }); }; - public onMove = (idx: number, direction: number) => { - console.log('todo: onMove'); + public onBackToList = () => { + this.setState({ editIndex: undefined }); + }; + + public onMove = (idx: number, direction: MoveDirection) => { + const data = dashboardSceneGraph.getDataLayers(this._dashboard); + + const layers = [...data.state.layers]; + const [layer] = layers.splice(idx, 1); + layers.splice(idx + direction, 0, layer); + + data.setState({ + layers, + }); }; public onDelete = (idx: number) => { - console.log('todo: onDelete'); + const data = dashboardSceneGraph.getDataLayers(this._dashboard); + + const layers = [...data.state.layers]; + layers.splice(idx, 1); + + data.setState({ + layers, + }); + }; + + public onUpdate = (annotation: AnnotationQuery, editIndex: number) => { + const layer = this.getDataLayer(editIndex); + + layer.setState({ + key: `annotations-${annotation.name}`, + name: annotation.name, + isEnabled: Boolean(annotation.enable), + isHidden: Boolean(annotation.hide), + query: annotation, + }); + + //need to rerun the layer to update the query and + //see the annotation on the panel + layer.runLayer(); }; } function AnnotationsSettingsView({ model }: SceneComponentProps) { const dashboard = model.getDashboard(); - const sceneDataLayers = model.getSceneDataLayers(); + const { layers } = dashboardSceneGraph.getDataLayers(dashboard).useState(); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const { editIndex } = model.useState(); + const panels = dashboardSceneGraph.getVizPanels(dashboard); - let annotations: AnnotationQuery[] = []; + const annotations: AnnotationQuery[] = dataLayersToAnnotations(layers); - if (sceneDataLayers) { - const { layers } = sceneDataLayers.useState(); - annotations = dataLayersToAnnotations(layers); + if (editIndex != null && editIndex < model.getAnnotationsLength()) { + return ( + + ); } - const isEditing = editIndex != null && editIndex < model.getAnnotationsLength(); - return ( - {!isEditing && ( - - )} + + + ); +} + +interface AnnotationsSettingsEditViewProps { + annotationLayer: dataLayers.AnnotationsDataLayer; + pageNav: NavModelItem; + panels: VizPanel[]; + editIndex: number; + navModel: NavModel; + dashboard: DashboardScene; + onUpdate: (annotation: AnnotationQuery, editIndex: number) => void; + onBackToList: () => void; + onDelete: (idx: number) => void; +} + +function AnnotationsSettingsEditView({ + annotationLayer, + pageNav, + navModel, + panels, + editIndex, + dashboard, + onUpdate, + onBackToList, + onDelete, +}: AnnotationsSettingsEditViewProps) { + const parentTab = pageNav.children!.find((p) => p.active)!; + parentTab.parentItem = pageNav; + const { name, query } = annotationLayer.useState(); + + const editAnnotationPageNav = { + text: name, + parentItem: parentTab, + }; + + return ( + + + ); } diff --git a/public/app/features/dashboard/components/AnnotationSettings/AngularEditorLoader.tsx b/public/app/features/dashboard-scene/settings/annotations/AngularEditorLoader.tsx similarity index 100% rename from public/app/features/dashboard/components/AnnotationSettings/AngularEditorLoader.tsx rename to public/app/features/dashboard-scene/settings/annotations/AngularEditorLoader.tsx diff --git a/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.test.tsx b/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.test.tsx new file mode 100644 index 00000000000..f2ce2c54bf3 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.test.tsx @@ -0,0 +1,226 @@ +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { of } from 'rxjs'; + +import { + AnnotationQuery, + FieldType, + LoadingState, + PanelData, + VariableSupportType, + getDefaultTimeRange, + toDataFrame, +} from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { setRunRequest } from '@grafana/runtime'; +import { mockDataSource } from 'app/features/alerting/unified/mocks'; +import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; + +import { AnnotationSettingsEdit } from './AnnotationSettingsEdit'; + +const defaultDatasource = mockDataSource({ + name: 'Default Test Data Source', + type: 'test', +}); + +const promDatasource = mockDataSource({ + name: 'Prometheus', + type: 'prometheus', +}); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getAngularLoader: () => ({ + load: () => ({ + destroy: jest.fn(), + digest: jest.fn(), + getScope: () => ({ + $watch: jest.fn(), + }), + }), + }), +})); + +jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({ + ...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'), + getDataSourceSrv: () => ({ + get: async () => ({ + ...defaultDatasource, + variables: { + getType: () => VariableSupportType.Custom, + query: jest.fn(), + editor: jest.fn().mockImplementation(LegacyVariableQueryEditor), + }, + }), + getList: () => [defaultDatasource, promDatasource], + getInstanceSettings: () => ({ ...defaultDatasource }), + }), +})); + +jest.mock('./AngularEditorLoader', () => ({ AngularEditorLoader: () => 'mocked AngularEditorLoader' })); + +const runRequestMock = jest.fn().mockReturnValue( + of({ + state: LoadingState.Done, + series: [ + toDataFrame({ + fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }], + }), + ], + timeRange: getDefaultTimeRange(), + }) +); + +setRunRequest(runRequestMock); + +describe('AnnotationSettingsEdit', () => { + const mockOnUpdate = jest.fn(); + const mockGoBackToList = jest.fn(); + const mockOnDelete = jest.fn(); + + async function setup() { + const annotationQuery: AnnotationQuery = { + name: 'test', + datasource: defaultDatasource, + enable: true, + hide: false, + iconColor: 'blue', + }; + + const props = { + annotation: annotationQuery, + onUpdate: mockOnUpdate, + editIndex: 1, + panels: [], + onBackToList: mockGoBackToList, + onDelete: mockOnDelete, + }; + + return { + anno: annotationQuery, + renderer: await act(async () => render()), + user: userEvent.setup(), + }; + } + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render', async () => { + const { + renderer: { getByTestId }, + } = await setup(); + + const nameInput = getByTestId(selectors.pages.Dashboard.Settings.Annotations.Settings.name); + const dataSourceSelect = getByTestId(selectors.components.DataSourcePicker.container); + const enableToggle = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.enable); + const hideToggle = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.hide); + const iconColorToggle = getByTestId(selectors.components.ColorSwatch.name); + const panelSelect = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.showInLabel); + const deleteAnno = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.delete); + const apply = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.apply); + + expect(nameInput).toBeInTheDocument(); + expect(dataSourceSelect).toBeInTheDocument(); + expect(enableToggle).toBeInTheDocument(); + expect(hideToggle).toBeInTheDocument(); + expect(iconColorToggle).toBeInTheDocument(); + expect(panelSelect).toBeInTheDocument(); + expect(deleteAnno).toBeInTheDocument(); + expect(apply).toBeInTheDocument(); + }); + + it('should update annotation name on change', async () => { + const { + renderer: { getByTestId }, + user, + } = await setup(); + + await user.type(getByTestId(selectors.pages.Dashboard.Settings.Annotations.Settings.name), 'new name'); + + expect(mockOnUpdate).toHaveBeenCalled(); + }); + + it('should toggle annotation enabled on change', async () => { + const { + renderer: { getByTestId }, + user, + anno, + } = await setup(); + + const annoArg = { + ...anno, + enable: !anno.enable, + }; + + const enableToggle = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.enable); + + await user.click(enableToggle); + + expect(mockOnUpdate).toHaveBeenCalledTimes(1); + expect(mockOnUpdate).toHaveBeenCalledWith(annoArg, 1); + }); + + it('should toggle annotation hide on change', async () => { + const { + renderer: { getByTestId }, + user, + anno, + } = await setup(); + + const annoArg = { + ...anno, + hide: !anno.hide, + }; + + const hideToggle = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.hide); + + await user.click(hideToggle); + + expect(mockOnUpdate).toHaveBeenCalledTimes(1); + expect(mockOnUpdate).toHaveBeenCalledWith(annoArg, 1); + }); + + it('should set annotation filter', async () => { + const { + renderer: { getByTestId }, + user, + } = await setup(); + + const panelSelect = getByTestId(selectors.components.Annotations.annotationsTypeInput); + + await user.click(panelSelect); + await user.tab(); + + expect(mockOnUpdate).toHaveBeenCalledTimes(1); + expect(mockOnUpdate).toHaveBeenCalledWith(expect.objectContaining({ filter: undefined }), 1); + }); + + it('should delete annotation', async () => { + const { + renderer: { getByTestId }, + user, + } = await setup(); + + const deleteAnno = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.delete); + + await user.click(deleteAnno); + + expect(mockOnDelete).toHaveBeenCalledTimes(1); + }); + + it('should go back to list annotation', async () => { + const { + renderer: { getByTestId }, + user, + } = await setup(); + + const goBack = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.apply); + + await user.click(goBack); + + expect(mockGoBackToList).toHaveBeenCalledTimes(1); + }); +}); diff --git a/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx b/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx new file mode 100644 index 00000000000..59f88bad3f8 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx @@ -0,0 +1,321 @@ +import { css } from '@emotion/css'; +import React, { useMemo } from 'react'; +import { useAsync } from 'react-use'; + +import { + AnnotationQuery, + DataSourceInstanceSettings, + getDataSourceRef, + GrafanaTheme2, + SelectableValue, +} from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { config, getDataSourceSrv } from '@grafana/runtime'; +import { VizPanel } from '@grafana/scenes'; +import { AnnotationPanelFilter } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; +import { + Button, + Checkbox, + Field, + FieldSet, + HorizontalGroup, + Input, + MultiSelect, + Select, + useStyles2, + Stack, +} from '@grafana/ui'; +import { ColorValueEditor } from 'app/core/components/OptionsUI/color'; +import StandardAnnotationQueryEditor from 'app/features/annotations/components/StandardAnnotationQueryEditor'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; + +import { getPanelIdForVizPanel } from '../../utils/utils'; + +import { AngularEditorLoader } from './AngularEditorLoader'; + +type Props = { + annotation: AnnotationQuery; + editIndex: number; + panels: VizPanel[]; + onUpdate: (annotation: AnnotationQuery, editIndex: number) => void; + onBackToList: () => void; + onDelete: (index: number) => void; +}; + +export const newAnnotationName = 'New annotation'; + +export const AnnotationSettingsEdit = ({ annotation, editIndex, panels, onUpdate, onBackToList, onDelete }: Props) => { + const styles = useStyles2(getStyles); + + const panelFilter = useMemo(() => { + if (!annotation.filter) { + return PanelFilterType.AllPanels; + } + return annotation.filter.exclude ? PanelFilterType.ExcludePanels : PanelFilterType.IncludePanels; + }, [annotation.filter]); + + const { value: ds } = useAsync(() => { + return getDataSourceSrv().get(annotation.datasource); + }, [annotation.datasource]); + + const dsi = getDataSourceSrv().getInstanceSettings(annotation.datasource); + + const onNameChange = (ev: React.FocusEvent) => { + onUpdate( + { + ...annotation, + name: ev.currentTarget.value, + }, + editIndex + ); + }; + + const onDataSourceChange = (ds: DataSourceInstanceSettings) => { + const dsRef = getDataSourceRef(ds); + + if (annotation.datasource?.type !== dsRef.type) { + onUpdate( + { + datasource: dsRef, + builtIn: annotation.builtIn, + enable: annotation.enable, + iconColor: annotation.iconColor, + name: annotation.name, + hide: annotation.hide, + filter: annotation.filter, + mappings: annotation.mappings, + type: annotation.type, + }, + editIndex + ); + } else { + onUpdate( + { + ...annotation, + datasource: dsRef, + }, + editIndex + ); + } + }; + + const onChange = (ev: React.FocusEvent) => { + const target = ev.currentTarget; + onUpdate( + { + ...annotation, + [target.name]: target.type === 'checkbox' ? target.checked : target.value, + }, + editIndex + ); + }; + + const onColorChange = (color?: string) => { + onUpdate( + { + ...annotation, + iconColor: color!, + }, + editIndex + ); + }; + + const onFilterTypeChange = (v: SelectableValue) => { + let filter = + v.value === PanelFilterType.AllPanels + ? undefined + : { + exclude: v.value === PanelFilterType.ExcludePanels, + ids: annotation.filter?.ids ?? [], + }; + onUpdate({ ...annotation, filter }, editIndex); + }; + + const onAddFilterPanelID = (selections: Array>) => { + if (!Array.isArray(selections)) { + return; + } + + const filter: AnnotationPanelFilter = { + exclude: panelFilter === PanelFilterType.ExcludePanels, + ids: [], + }; + + selections.forEach((selection) => selection.value && filter.ids.push(selection.value)); + onUpdate({ ...annotation, filter }, editIndex); + }; + + const onDeleteAndLeavePage = () => { + onDelete(editIndex); + onBackToList(); + }; + + const isNewAnnotation = annotation.name === newAnnotationName; + + const sortFn = (a: SelectableValue, b: SelectableValue) => { + if (a.label && b.label) { + return a.label.toLowerCase().localeCompare(b.label.toLowerCase()); + } + + return -1; + }; + + const selectablePanels: Array> = useMemo( + () => + panels + // Filtering out rows at the moment, revisit to only include panels that support annotations + // However the information to know if a panel supports annotations requires it to be already loaded + // panel.plugin?.dataSupport?.annotations + .filter((panel) => config.panels[panel.state.pluginId]) + .map((panel) => ({ + value: getPanelIdForVizPanel(panel), + label: panel.state.title ?? `Panel ${getPanelIdForVizPanel(panel)}`, + description: panel.state.description, + imgUrl: config.panels[panel.state.pluginId].info.logos.small, + })) + .sort(sortFn) ?? [], + [panels] + ); + + return ( +
+
+ + + + + + + + + + + + + + + + + + + <> +