mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
1315c67c8b
commit
1fe32479d7
@ -95,6 +95,7 @@ export const Pages = {
|
|||||||
*/
|
*/
|
||||||
addAnnotationCTA: Components.CallToActionCard.button('Add annotation query'),
|
addAnnotationCTA: Components.CallToActionCard.button('Add annotation query'),
|
||||||
addAnnotationCTAV2: Components.CallToActionCard.buttonV2('Add annotation query'),
|
addAnnotationCTAV2: Components.CallToActionCard.buttonV2('Add annotation query'),
|
||||||
|
annotations: 'data-testid list-annotations',
|
||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
name: 'Annotations settings name input',
|
name: 'Annotations settings name input',
|
||||||
@ -103,6 +104,10 @@ export const Pages = {
|
|||||||
panelFilterSelect: 'data-testid annotations-panel-filter',
|
panelFilterSelect: 'data-testid annotations-panel-filter',
|
||||||
showInLabel: 'show-in-label',
|
showInLabel: 'show-in-label',
|
||||||
previewInDashboard: 'data-testid annotations-preview',
|
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: {
|
Variables: {
|
||||||
|
@ -6,7 +6,9 @@ import { Unsubscribable } from 'rxjs';
|
|||||||
import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil, textUtil } from '@grafana/data';
|
import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil, textUtil } from '@grafana/data';
|
||||||
import { locationService, config } from '@grafana/runtime';
|
import { locationService, config } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
|
dataLayers,
|
||||||
getUrlSyncManager,
|
getUrlSyncManager,
|
||||||
|
SceneDataLayers,
|
||||||
SceneFlexLayout,
|
SceneFlexLayout,
|
||||||
sceneGraph,
|
sceneGraph,
|
||||||
SceneGridItem,
|
SceneGridItem,
|
||||||
@ -373,6 +375,14 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
this.setIsDirty();
|
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) {
|
if (event.payload.changedObject instanceof SceneGridItem) {
|
||||||
this.setIsDirty();
|
this.setIsDirty();
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import { map, of } from 'rxjs';
|
import { map, of } from 'rxjs';
|
||||||
|
|
||||||
import { DataQueryRequest, DataSourceApi, LoadingState, PanelData } from '@grafana/data';
|
import { AnnotationQuery, DataQueryRequest, DataSourceApi, LoadingState, PanelData } from '@grafana/data';
|
||||||
import { SceneDataLayers, SceneGridItem, SceneGridLayout, SceneTimeRange } from '@grafana/scenes';
|
import { SceneDataLayers, SceneGridItem, SceneGridLayout, SceneTimeRange, dataLayers } from '@grafana/scenes';
|
||||||
|
|
||||||
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
|
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
|
||||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
|
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||||
import { activateFullSceneTree } from '../utils/test-utils';
|
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 runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
|
||||||
const result: PanelData = {
|
const result: PanelData = {
|
||||||
state: LoadingState.Loading,
|
state: LoadingState.Loading,
|
||||||
@ -31,7 +32,9 @@ const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request:
|
|||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
getDataSourceSrv: () => {
|
getDataSourceSrv: () => {
|
||||||
getDataSourceSrvSpy();
|
return {
|
||||||
|
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
||||||
return runRequestMock(ds, request);
|
return runRequestMock(ds, request);
|
||||||
@ -44,26 +47,103 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
describe('AnnotationsEditView', () => {
|
describe('AnnotationsEditView', () => {
|
||||||
describe('Dashboard annotations state', () => {
|
describe('Dashboard annotations state', () => {
|
||||||
let annotationsView: AnnotationsEditView;
|
let annotationsView: AnnotationsEditView;
|
||||||
|
let dashboardScene: DashboardScene;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const result = await buildTestScene();
|
const result = await buildTestScene();
|
||||||
annotationsView = result.annotationsView;
|
annotationsView = result.annotationsView;
|
||||||
|
dashboardScene = result.dashboard;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the correct urlKey', () => {
|
it('should return the correct urlKey', () => {
|
||||||
expect(annotationsView.getUrlKey()).toBe('annotations');
|
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', () => {
|
it('should return the annotations length', () => {
|
||||||
expect(annotationsView.getAnnotationsLength()).toBe(1);
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { AnnotationQuery, DataTopic, PageLayoutType } from '@grafana/data';
|
import { AnnotationQuery, DataTopic, NavModel, NavModelItem, PageLayoutType, getDataSourceRef } from '@grafana/data';
|
||||||
import {
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
SceneComponentProps,
|
import { SceneComponentProps, SceneObjectBase, VizPanel, dataLayers } from '@grafana/scenes';
|
||||||
SceneDataLayerProvider,
|
|
||||||
SceneDataLayers,
|
|
||||||
SceneObjectBase,
|
|
||||||
sceneGraph,
|
|
||||||
} from '@grafana/scenes';
|
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
|
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations';
|
import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations';
|
||||||
|
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
||||||
import { AnnotationSettingsList } from './annotations';
|
import { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } from './annotations';
|
||||||
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
||||||
|
|
||||||
|
export enum MoveDirection {
|
||||||
|
UP = -1,
|
||||||
|
DOWN = 1,
|
||||||
|
}
|
||||||
|
|
||||||
export interface AnnotationsEditViewState extends DashboardEditViewState {
|
export interface AnnotationsEditViewState extends DashboardEditViewState {
|
||||||
editIndex?: number | undefined;
|
editIndex?: number | undefined;
|
||||||
}
|
}
|
||||||
@ -36,22 +38,21 @@ export class AnnotationsEditView extends SceneObjectBase<AnnotationsEditViewStat
|
|||||||
return getDashboardSceneFor(this);
|
return getDashboardSceneFor(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _dataLayers(): SceneDataLayerProvider[] {
|
public getDataLayer(editIndex: number): dataLayers.AnnotationsDataLayer {
|
||||||
return sceneGraph.getDataLayers(this._dashboard);
|
const data = dashboardSceneGraph.getDataLayers(this._dashboard);
|
||||||
}
|
const layer = data.state.layers[editIndex];
|
||||||
|
|
||||||
public getSceneDataLayers(): SceneDataLayers | undefined {
|
if (!(layer instanceof dataLayers.AnnotationsDataLayer)) {
|
||||||
const data = sceneGraph.getData(this);
|
throw new Error('AnnotationsDataLayer not found at index ' + editIndex);
|
||||||
|
|
||||||
if (!(data instanceof SceneDataLayers)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAnnotationsLength(): number {
|
public getAnnotationsLength(): number {
|
||||||
return this._dataLayers.filter((layer) => layer.topic === DataTopic.Annotations).length;
|
return dashboardSceneGraph
|
||||||
|
.getDataLayers(this._dashboard)
|
||||||
|
.state.layers.filter((layer) => layer.topic === DataTopic.Annotations).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDashboard(): DashboardScene {
|
public getDashboard(): DashboardScene {
|
||||||
@ -59,49 +60,167 @@ export class AnnotationsEditView extends SceneObjectBase<AnnotationsEditViewStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onNew = () => {
|
public onNew = () => {
|
||||||
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) => {
|
public onEdit = (idx: number) => {
|
||||||
console.log('todo: onEdit');
|
this.setState({ editIndex: idx });
|
||||||
};
|
};
|
||||||
|
|
||||||
public onMove = (idx: number, direction: number) => {
|
public onBackToList = () => {
|
||||||
console.log('todo: onMove');
|
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) => {
|
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<AnnotationsEditView>) {
|
function AnnotationsSettingsView({ model }: SceneComponentProps<AnnotationsEditView>) {
|
||||||
const dashboard = model.getDashboard();
|
const dashboard = model.getDashboard();
|
||||||
const sceneDataLayers = model.getSceneDataLayers();
|
const { layers } = dashboardSceneGraph.getDataLayers(dashboard).useState();
|
||||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||||
const { editIndex } = model.useState();
|
const { editIndex } = model.useState();
|
||||||
|
const panels = dashboardSceneGraph.getVizPanels(dashboard);
|
||||||
|
|
||||||
let annotations: AnnotationQuery[] = [];
|
const annotations: AnnotationQuery[] = dataLayersToAnnotations(layers);
|
||||||
|
|
||||||
if (sceneDataLayers) {
|
if (editIndex != null && editIndex < model.getAnnotationsLength()) {
|
||||||
const { layers } = sceneDataLayers.useState();
|
return (
|
||||||
annotations = dataLayersToAnnotations(layers);
|
<AnnotationsSettingsEditView
|
||||||
|
annotationLayer={model.getDataLayer(editIndex)}
|
||||||
|
pageNav={pageNav}
|
||||||
|
panels={panels}
|
||||||
|
editIndex={editIndex}
|
||||||
|
navModel={navModel}
|
||||||
|
dashboard={dashboard}
|
||||||
|
onUpdate={model.onUpdate}
|
||||||
|
onBackToList={model.onBackToList}
|
||||||
|
onDelete={model.onDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEditing = editIndex != null && editIndex < model.getAnnotationsLength();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
<NavToolbarActions dashboard={dashboard} />
|
||||||
{!isEditing && (
|
<AnnotationSettingsList
|
||||||
<AnnotationSettingsList
|
annotations={annotations}
|
||||||
annotations={annotations}
|
onNew={model.onNew}
|
||||||
onNew={model.onNew}
|
onEdit={model.onEdit}
|
||||||
onEdit={model.onEdit}
|
onDelete={model.onDelete}
|
||||||
onDelete={model.onDelete}
|
onMove={model.onMove}
|
||||||
onMove={model.onMove}
|
/>
|
||||||
/>
|
</Page>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Page navModel={navModel} pageNav={editAnnotationPageNav} layout={PageLayoutType.Standard}>
|
||||||
|
<NavToolbarActions dashboard={dashboard} />
|
||||||
|
<AnnotationSettingsEdit
|
||||||
|
annotation={query}
|
||||||
|
editIndex={editIndex}
|
||||||
|
panels={panels}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
onBackToList={onBackToList}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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<PanelData>({
|
||||||
|
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(<AnnotationSettingsEdit {...props} />)),
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
@ -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<HTMLInputElement>) => {
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
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<PanelFilterType>) => {
|
||||||
|
let filter =
|
||||||
|
v.value === PanelFilterType.AllPanels
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
exclude: v.value === PanelFilterType.ExcludePanels,
|
||||||
|
ids: annotation.filter?.ids ?? [],
|
||||||
|
};
|
||||||
|
onUpdate({ ...annotation, filter }, editIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddFilterPanelID = (selections: Array<SelectableValue<number>>) => {
|
||||||
|
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<number>, b: SelectableValue<number>) => {
|
||||||
|
if (a.label && b.label) {
|
||||||
|
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectablePanels: Array<SelectableValue<number>> = 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 (
|
||||||
|
<div>
|
||||||
|
<FieldSet className={styles.settingsForm}>
|
||||||
|
<Field label="Name">
|
||||||
|
<Input
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Annotations.Settings.name}
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
autoFocus={isNewAnnotation}
|
||||||
|
value={annotation.name}
|
||||||
|
onChange={onNameChange}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Data source" htmlFor="data-source-picker">
|
||||||
|
<DataSourcePicker annotations variables current={annotation.datasource} onChange={onDataSourceChange} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Enabled" description="When enabled the annotation query is issued every dashboard refresh">
|
||||||
|
<Checkbox
|
||||||
|
name="enable"
|
||||||
|
id="enable"
|
||||||
|
value={annotation.enable}
|
||||||
|
onChange={onChange}
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.enable}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
label="Hidden"
|
||||||
|
description="Annotation queries can be toggled on or off at the top of the dashboard. With this option checked this toggle will be hidden."
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
name="hide"
|
||||||
|
id="hide"
|
||||||
|
value={annotation.hide}
|
||||||
|
onChange={onChange}
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.hide}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Color" description="Color to use for the annotation event markers">
|
||||||
|
<HorizontalGroup>
|
||||||
|
<ColorValueEditor value={annotation?.iconColor} onChange={onColorChange} />
|
||||||
|
</HorizontalGroup>
|
||||||
|
</Field>
|
||||||
|
<Field label="Show in" data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.showInLabel}>
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
options={panelFilters}
|
||||||
|
value={panelFilter}
|
||||||
|
onChange={onFilterTypeChange}
|
||||||
|
data-testid={selectors.components.Annotations.annotationsTypeInput}
|
||||||
|
/>
|
||||||
|
{panelFilter !== PanelFilterType.AllPanels && (
|
||||||
|
<MultiSelect
|
||||||
|
options={selectablePanels}
|
||||||
|
value={selectablePanels.filter((panel) => annotation.filter?.ids.includes(panel.value!))}
|
||||||
|
onChange={onAddFilterPanelID}
|
||||||
|
isClearable={true}
|
||||||
|
placeholder="Choose panels"
|
||||||
|
width={100}
|
||||||
|
closeMenuOnSelect={false}
|
||||||
|
className={styles.select}
|
||||||
|
data-testid={selectors.components.Annotations.annotationsChoosePanelInput}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Field>
|
||||||
|
</FieldSet>
|
||||||
|
<FieldSet>
|
||||||
|
<h3 className="page-heading">Query</h3>
|
||||||
|
{ds?.annotations && dsi && (
|
||||||
|
<StandardAnnotationQueryEditor
|
||||||
|
datasource={ds}
|
||||||
|
datasourceInstanceSettings={dsi}
|
||||||
|
annotation={annotation}
|
||||||
|
onChange={(annotation) => onUpdate(annotation, editIndex)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{ds && !ds.annotations && (
|
||||||
|
<AngularEditorLoader
|
||||||
|
datasource={ds}
|
||||||
|
annotation={annotation}
|
||||||
|
onChange={(annotation) => onUpdate(annotation, editIndex)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FieldSet>
|
||||||
|
<Stack>
|
||||||
|
{!annotation.builtIn && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={onDeleteAndLeavePage}
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.delete}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={onBackToList}
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.apply}
|
||||||
|
>
|
||||||
|
Back to list
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
settingsForm: css({
|
||||||
|
maxWidth: theme.spacing(60),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
select: css({
|
||||||
|
marginTop: '8px',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Synthetic type
|
||||||
|
enum PanelFilterType {
|
||||||
|
AllPanels,
|
||||||
|
IncludePanels,
|
||||||
|
ExcludePanels,
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelFilters = [
|
||||||
|
{
|
||||||
|
label: 'All panels',
|
||||||
|
value: PanelFilterType.AllPanels,
|
||||||
|
description: 'Send the annotation data to all panels that support annotations',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Selected panels',
|
||||||
|
value: PanelFilterType.IncludePanels,
|
||||||
|
description: 'Send the annotations to the explicitly listed panels',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'All panels except',
|
||||||
|
value: PanelFilterType.ExcludePanels,
|
||||||
|
description: 'Do not send annotation data to the following panels',
|
||||||
|
},
|
||||||
|
];
|
@ -0,0 +1,139 @@
|
|||||||
|
import { act, render } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { AnnotationQuery } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||||
|
|
||||||
|
import { MoveDirection } from '../AnnotationsEditView';
|
||||||
|
|
||||||
|
import { AnnotationSettingsList, BUTTON_TITLE } from './AnnotationSettingsList';
|
||||||
|
|
||||||
|
const defaultDatasource = mockDataSource({
|
||||||
|
name: 'Default Test Data Source',
|
||||||
|
type: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'),
|
||||||
|
getDataSourceSrv: () => ({
|
||||||
|
getInstanceSettings: () => ({ ...defaultDatasource }),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('AnnotationSettingsEdit', () => {
|
||||||
|
const mockOnNew = jest.fn();
|
||||||
|
const mockOnEdit = jest.fn();
|
||||||
|
const mockOnMove = jest.fn();
|
||||||
|
const mockOnDelete = jest.fn();
|
||||||
|
|
||||||
|
async function setup(emptyList = false) {
|
||||||
|
const annotationQuery1: AnnotationQuery = {
|
||||||
|
name: 'test1',
|
||||||
|
datasource: defaultDatasource,
|
||||||
|
enable: true,
|
||||||
|
hide: false,
|
||||||
|
iconColor: 'blue',
|
||||||
|
};
|
||||||
|
|
||||||
|
const annotationQuery2: AnnotationQuery = {
|
||||||
|
name: 'test2',
|
||||||
|
datasource: defaultDatasource,
|
||||||
|
enable: true,
|
||||||
|
hide: false,
|
||||||
|
iconColor: 'red',
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
annotations: emptyList ? [] : [annotationQuery1, annotationQuery2],
|
||||||
|
onNew: mockOnNew,
|
||||||
|
onEdit: mockOnEdit,
|
||||||
|
onMove: mockOnMove,
|
||||||
|
onDelete: mockOnDelete,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderer: await act(async () => render(<AnnotationSettingsList {...props} />)),
|
||||||
|
user: userEvent.setup(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render with empty list message', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getByTestId },
|
||||||
|
} = await setup(true);
|
||||||
|
|
||||||
|
const emptyListBtn = getByTestId(selectors.components.CallToActionCard.buttonV2(BUTTON_TITLE));
|
||||||
|
|
||||||
|
expect(emptyListBtn).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create new annotation when empty list button is pressed', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getByTestId },
|
||||||
|
user,
|
||||||
|
} = await setup(true);
|
||||||
|
|
||||||
|
const emptyListBtn = getByTestId(selectors.components.CallToActionCard.buttonV2(BUTTON_TITLE));
|
||||||
|
|
||||||
|
await user.click(emptyListBtn);
|
||||||
|
|
||||||
|
expect(mockOnNew).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render annotation list', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getByTestId },
|
||||||
|
} = await setup();
|
||||||
|
|
||||||
|
const list = getByTestId(selectors.pages.Dashboard.Settings.Annotations.List.annotations);
|
||||||
|
|
||||||
|
expect(list.children.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should edit annotation', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getAllByRole },
|
||||||
|
user,
|
||||||
|
} = await setup();
|
||||||
|
|
||||||
|
const gridCells = getAllByRole('gridcell');
|
||||||
|
|
||||||
|
await user.click(gridCells[0]);
|
||||||
|
|
||||||
|
expect(mockOnEdit).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should move annotation up', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getAllByLabelText },
|
||||||
|
user,
|
||||||
|
} = await setup();
|
||||||
|
|
||||||
|
const moveBtns = getAllByLabelText('Move up');
|
||||||
|
|
||||||
|
await user.click(moveBtns[0]);
|
||||||
|
|
||||||
|
expect(mockOnMove).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockOnMove).toHaveBeenCalledWith(expect.anything(), MoveDirection.UP);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should move annotation down', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getAllByLabelText },
|
||||||
|
user,
|
||||||
|
} = await setup();
|
||||||
|
|
||||||
|
const moveBtns = getAllByLabelText('Move down');
|
||||||
|
|
||||||
|
await user.click(moveBtns[0]);
|
||||||
|
|
||||||
|
expect(mockOnMove).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockOnMove).toHaveBeenCalledWith(expect.anything(), MoveDirection.DOWN);
|
||||||
|
});
|
||||||
|
});
|
@ -2,19 +2,24 @@ import { css } from '@emotion/css';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { AnnotationQuery } from '@grafana/data';
|
import { AnnotationQuery } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { Button, DeleteButton, IconButton, useStyles2, VerticalGroup } from '@grafana/ui';
|
import { Button, DeleteButton, IconButton, useStyles2, VerticalGroup } from '@grafana/ui';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
import { ListNewButton } from 'app/features/dashboard/components/DashboardSettings/ListNewButton';
|
import { ListNewButton } from 'app/features/dashboard/components/DashboardSettings/ListNewButton';
|
||||||
|
|
||||||
|
import { MoveDirection } from '../AnnotationsEditView';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
annotations: AnnotationQuery[];
|
annotations: AnnotationQuery[];
|
||||||
onNew: () => void;
|
onNew: () => void;
|
||||||
onEdit: (idx: number) => void;
|
onEdit: (idx: number) => void;
|
||||||
onMove: (idx: number, dir: number) => void;
|
onMove: (idx: number, dir: MoveDirection) => void;
|
||||||
onDelete: (idx: number) => void;
|
onDelete: (idx: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const BUTTON_TITLE = 'Add annotation query';
|
||||||
|
|
||||||
export const AnnotationSettingsList = ({ annotations, onNew, onEdit, onMove, onDelete }: Props) => {
|
export const AnnotationSettingsList = ({ annotations, onNew, onEdit, onMove, onDelete }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
@ -45,7 +50,7 @@ export const AnnotationSettingsList = ({ annotations, onNew, onEdit, onMove, onD
|
|||||||
<th colSpan={3}></th>
|
<th colSpan={3}></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody data-testid={selectors.pages.Dashboard.Settings.Annotations.List.annotations}>
|
||||||
{annotations.map((annotation, idx) => (
|
{annotations.map((annotation, idx) => (
|
||||||
<tr key={`${annotation.name}-${idx}`}>
|
<tr key={`${annotation.name}-${idx}`}>
|
||||||
{annotation.builtIn ? (
|
{annotation.builtIn ? (
|
||||||
@ -65,11 +70,17 @@ export const AnnotationSettingsList = ({ annotations, onNew, onEdit, onMove, onD
|
|||||||
{dataSourceSrv.getInstanceSettings(annotation.datasource)?.name || annotation.datasource?.uid}
|
{dataSourceSrv.getInstanceSettings(annotation.datasource)?.name || annotation.datasource?.uid}
|
||||||
</td>
|
</td>
|
||||||
<td role="gridcell" style={{ width: '1%' }}>
|
<td role="gridcell" style={{ width: '1%' }}>
|
||||||
{idx !== 0 && <IconButton name="arrow-up" onClick={() => onMove(idx, -1)} tooltip="Move up" />}
|
{idx !== 0 && (
|
||||||
|
<IconButton name="arrow-up" onClick={() => onMove(idx, MoveDirection.UP)} tooltip="Move up" />
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td role="gridcell" style={{ width: '1%' }}>
|
<td role="gridcell" style={{ width: '1%' }}>
|
||||||
{annotations.length > 1 && idx !== annotations.length - 1 ? (
|
{annotations.length > 1 && idx !== annotations.length - 1 ? (
|
||||||
<IconButton name="arrow-down" onClick={() => onMove(idx, 1)} tooltip="Move down" />
|
<IconButton
|
||||||
|
name="arrow-down"
|
||||||
|
onClick={() => onMove(idx, MoveDirection.DOWN)}
|
||||||
|
tooltip="Move down"
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</td>
|
</td>
|
||||||
<td role="gridcell" style={{ width: '1%' }}>
|
<td role="gridcell" style={{ width: '1%' }}>
|
||||||
@ -92,7 +103,7 @@ export const AnnotationSettingsList = ({ annotations, onNew, onEdit, onMove, onD
|
|||||||
onClick={onNew}
|
onClick={onNew}
|
||||||
title="There are no custom annotation queries added yet"
|
title="There are no custom annotation queries added yet"
|
||||||
buttonIcon="comment-alt"
|
buttonIcon="comment-alt"
|
||||||
buttonTitle="Add annotation query"
|
buttonTitle={BUTTON_TITLE}
|
||||||
infoBoxTitle="What are annotation queries?"
|
infoBoxTitle="What are annotation queries?"
|
||||||
infoBox={{
|
infoBox={{
|
||||||
__html: `<p>Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines
|
__html: `<p>Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines
|
||||||
|
@ -1 +1,2 @@
|
|||||||
|
export { AnnotationSettingsEdit, newAnnotationName } from './AnnotationSettingsEdit';
|
||||||
export { AnnotationSettingsList } from './AnnotationSettingsList';
|
export { AnnotationSettingsList } from './AnnotationSettingsList';
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
|
SceneDataLayers,
|
||||||
SceneGridItem,
|
SceneGridItem,
|
||||||
SceneGridLayout,
|
SceneGridLayout,
|
||||||
|
SceneGridRow,
|
||||||
SceneQueryRunner,
|
SceneQueryRunner,
|
||||||
SceneRefreshPicker,
|
SceneRefreshPicker,
|
||||||
SceneTimePicker,
|
SceneTimePicker,
|
||||||
@ -8,6 +10,8 @@ import {
|
|||||||
VizPanel,
|
VizPanel,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
|
|
||||||
|
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
|
||||||
|
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||||
import { DashboardControls } from '../scene/DashboardControls';
|
import { DashboardControls } from '../scene/DashboardControls';
|
||||||
import { DashboardLinksControls } from '../scene/DashboardLinksControls';
|
import { DashboardLinksControls } from '../scene/DashboardLinksControls';
|
||||||
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
||||||
@ -91,6 +95,59 @@ describe('dashboardSceneGraph', () => {
|
|||||||
expect(dashboardSceneGraph.getPanelLinks(panelWithNoLinks)).toBeInstanceOf(VizPanelLinks);
|
expect(dashboardSceneGraph.getPanelLinks(panelWithNoLinks)).toBeInstanceOf(VizPanelLinks);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getVizPanels', () => {
|
||||||
|
let scene: DashboardScene;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
scene = buildTestScene();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return all panels', () => {
|
||||||
|
const vizPanels = dashboardSceneGraph.getVizPanels(scene);
|
||||||
|
|
||||||
|
expect(vizPanels.length).toBe(6);
|
||||||
|
expect(vizPanels[0].state.title).toBe('Panel A');
|
||||||
|
expect(vizPanels[1].state.title).toBe('Panel B');
|
||||||
|
expect(vizPanels[2].state.title).toBe('Panel C');
|
||||||
|
expect(vizPanels[3].state.title).toBe('Panel D');
|
||||||
|
expect(vizPanels[4].state.title).toBe('Panel E');
|
||||||
|
expect(vizPanels[5].state.title).toBe('Panel F');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return an empty array when scene has no panels', () => {
|
||||||
|
scene.setState({
|
||||||
|
body: new SceneGridLayout({ children: [] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const vizPanels = dashboardSceneGraph.getVizPanels(scene);
|
||||||
|
|
||||||
|
expect(vizPanels.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDataLayers', () => {
|
||||||
|
let scene: DashboardScene;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
scene = buildTestScene();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the scene data layers', () => {
|
||||||
|
const dataLayers = dashboardSceneGraph.getDataLayers(scene);
|
||||||
|
|
||||||
|
expect(dataLayers).toBeInstanceOf(SceneDataLayers);
|
||||||
|
expect(dataLayers?.state.layers.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if there are no scene data layers', () => {
|
||||||
|
scene.setState({
|
||||||
|
$data: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => dashboardSceneGraph.getDataLayers(scene)).toThrow('SceneDataLayers not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
||||||
@ -110,6 +167,26 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
$data: new SceneDataLayers({
|
||||||
|
layers: [
|
||||||
|
new DashboardAnnotationsDataLayer({
|
||||||
|
key: `annotation`,
|
||||||
|
query: {
|
||||||
|
enable: true,
|
||||||
|
hide: false,
|
||||||
|
iconColor: 'red',
|
||||||
|
name: 'a',
|
||||||
|
},
|
||||||
|
name: 'a',
|
||||||
|
isEnabled: true,
|
||||||
|
isHidden: false,
|
||||||
|
}),
|
||||||
|
new AlertStatesDataLayer({
|
||||||
|
key: 'alert-states',
|
||||||
|
name: 'Alert States',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
body: new SceneGridLayout({
|
body: new SceneGridLayout({
|
||||||
children: [
|
children: [
|
||||||
new SceneGridItem({
|
new SceneGridItem({
|
||||||
@ -131,7 +208,7 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
|||||||
}),
|
}),
|
||||||
new SceneGridItem({
|
new SceneGridItem({
|
||||||
body: new VizPanel({
|
body: new VizPanel({
|
||||||
title: 'Panel B',
|
title: 'Panel C',
|
||||||
key: 'panel-2-clone-1',
|
key: 'panel-2-clone-1',
|
||||||
pluginId: 'table',
|
pluginId: 'table',
|
||||||
$data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }),
|
$data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }),
|
||||||
@ -139,13 +216,33 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
|||||||
}),
|
}),
|
||||||
new SceneGridItem({
|
new SceneGridItem({
|
||||||
body: new VizPanel({
|
body: new VizPanel({
|
||||||
title: 'Panel B',
|
title: 'Panel D',
|
||||||
key: 'panel-with-links',
|
key: 'panel-with-links',
|
||||||
pluginId: 'table',
|
pluginId: 'table',
|
||||||
$data: new SceneQueryRunner({ key: 'data-query-runner3', queries: [{ refId: 'A' }] }),
|
$data: new SceneQueryRunner({ key: 'data-query-runner3', queries: [{ refId: 'A' }] }),
|
||||||
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
|
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
new SceneGridRow({
|
||||||
|
key: 'key',
|
||||||
|
title: 'row',
|
||||||
|
children: [
|
||||||
|
new SceneGridItem({
|
||||||
|
body: new VizPanel({
|
||||||
|
title: 'Panel E',
|
||||||
|
key: 'panel-2-clone-2',
|
||||||
|
pluginId: 'table',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
new SceneGridItem({
|
||||||
|
body: new VizPanel({
|
||||||
|
title: 'Panel F',
|
||||||
|
key: 'panel-2-clone-2',
|
||||||
|
pluginId: 'table',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
...overrides,
|
...overrides,
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { SceneTimePicker, SceneRefreshPicker, VizPanel } from '@grafana/scenes';
|
import {
|
||||||
|
SceneTimePicker,
|
||||||
|
SceneRefreshPicker,
|
||||||
|
VizPanel,
|
||||||
|
SceneGridItem,
|
||||||
|
SceneGridRow,
|
||||||
|
SceneDataLayers,
|
||||||
|
sceneGraph,
|
||||||
|
} from '@grafana/scenes';
|
||||||
|
|
||||||
import { DashboardControls } from '../scene/DashboardControls';
|
import { DashboardControls } from '../scene/DashboardControls';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
@ -49,9 +57,43 @@ function getPanelLinks(panel: VizPanel) {
|
|||||||
throw new Error('VizPanelLinks links not found');
|
throw new Error('VizPanelLinks links not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVizPanels(scene: DashboardScene): VizPanel[] {
|
||||||
|
const panels: VizPanel[] = [];
|
||||||
|
|
||||||
|
scene.state.body.forEachChild((child) => {
|
||||||
|
if (child instanceof SceneGridItem) {
|
||||||
|
if (child.state.body instanceof VizPanel) {
|
||||||
|
panels.push(child.state.body);
|
||||||
|
}
|
||||||
|
} else if (child instanceof SceneGridRow) {
|
||||||
|
child.forEachChild((child) => {
|
||||||
|
if (child instanceof SceneGridItem) {
|
||||||
|
if (child.state.body instanceof VizPanel) {
|
||||||
|
panels.push(child.state.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return panels;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataLayers(scene: DashboardScene): SceneDataLayers {
|
||||||
|
const data = sceneGraph.getData(scene);
|
||||||
|
|
||||||
|
if (!(data instanceof SceneDataLayers)) {
|
||||||
|
throw new Error('SceneDataLayers not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
export const dashboardSceneGraph = {
|
export const dashboardSceneGraph = {
|
||||||
getTimePicker,
|
getTimePicker,
|
||||||
getRefreshPicker,
|
getRefreshPicker,
|
||||||
getDashboardControls,
|
getDashboardControls,
|
||||||
getPanelLinks,
|
getPanelLinks,
|
||||||
|
getVizPanels,
|
||||||
|
getDataLayers,
|
||||||
};
|
};
|
||||||
|
@ -27,12 +27,11 @@ import {
|
|||||||
import { ColorValueEditor } from 'app/core/components/OptionsUI/color';
|
import { ColorValueEditor } from 'app/core/components/OptionsUI/color';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import StandardAnnotationQueryEditor from 'app/features/annotations/components/StandardAnnotationQueryEditor';
|
import StandardAnnotationQueryEditor from 'app/features/annotations/components/StandardAnnotationQueryEditor';
|
||||||
|
import { AngularEditorLoader } from 'app/features/dashboard-scene/settings/annotations/AngularEditorLoader';
|
||||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||||
|
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
import { DashboardModel } from '../../state/DashboardModel';
|
||||||
|
|
||||||
import { AngularEditorLoader } from './AngularEditorLoader';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editIdx: number;
|
editIdx: number;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
|
Loading…
Reference in New Issue
Block a user