mirror of https://github.com/grafana/grafana
DashboardScene: Support for dashboard PanelContext actions via state hook (#76192)
* DashboardScene: Support for dashboard PanelContext actions via state hook * Update * Progress * Update * Update * updatepull/76525/head
parent
b6fb1e52f2
commit
de8ab7efe7
@ -0,0 +1,238 @@ |
||||
import { EventBusSrv } from '@grafana/data'; |
||||
import { BackendSrv, setBackendSrv } from '@grafana/runtime'; |
||||
import { PanelContext } from '@grafana/ui'; |
||||
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; |
||||
import { findVizPanelByKey } from '../utils/utils'; |
||||
|
||||
import { getAdHocFilterSetFor, setDashboardPanelContext } from './setDashboardPanelContext'; |
||||
|
||||
const postFn = jest.fn(); |
||||
const putFn = jest.fn(); |
||||
const deleteFn = jest.fn(); |
||||
|
||||
setBackendSrv({ |
||||
post: postFn, |
||||
put: putFn, |
||||
delete: deleteFn, |
||||
} as any as BackendSrv); |
||||
|
||||
describe('setDashboardPanelContext', () => { |
||||
describe('canAddAnnotations', () => { |
||||
it('Can add when builtIn is enabled and permissions allow', () => { |
||||
const { context } = buildTestScene({ builtInAnnotationsEnabled: true, dashboardCanEdit: true, canAdd: true }); |
||||
expect(context.canAddAnnotations!()).toBe(true); |
||||
}); |
||||
|
||||
it('Can not when builtIn is disabled', () => { |
||||
const { context } = buildTestScene({ builtInAnnotationsEnabled: false, dashboardCanEdit: true, canAdd: true }); |
||||
expect(context.canAddAnnotations!()).toBe(false); |
||||
}); |
||||
|
||||
it('Can not when permission do not allow', () => { |
||||
const { context } = buildTestScene({ builtInAnnotationsEnabled: true, dashboardCanEdit: true, canAdd: false }); |
||||
expect(context.canAddAnnotations!()).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('canEditAnnotations', () => { |
||||
it('Can edit global event when user has org permission', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, orgCanEdit: true }); |
||||
expect(context.canEditAnnotations!()).toBe(true); |
||||
}); |
||||
|
||||
it('Can not edit global event when has no org permission', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, orgCanEdit: false }); |
||||
expect(context.canEditAnnotations!()).toBe(false); |
||||
}); |
||||
|
||||
it('Can edit dashboard event when has dashboard permission', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, canEdit: true }); |
||||
expect(context.canEditAnnotations!('dash-uid')).toBe(true); |
||||
}); |
||||
|
||||
it('Can not edit dashboard event when has no dashboard permission', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, canEdit: false }); |
||||
expect(context.canEditAnnotations!('dash-uid')).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('canDeleteAnnotations', () => { |
||||
it('Can delete global event when user has org permission', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, canDelete: true }); |
||||
expect(context.canDeleteAnnotations!()).toBe(true); |
||||
}); |
||||
|
||||
it('Can not delete global event when has no org permission', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, canDelete: false }); |
||||
expect(context.canDeleteAnnotations!()).toBe(false); |
||||
}); |
||||
|
||||
it('Can delete dashboard event when has dashboard permission', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, canDelete: true }); |
||||
expect(context.canDeleteAnnotations!('dash-uid')).toBe(true); |
||||
}); |
||||
|
||||
it('Can not delete dashboard event when has no dashboard permission', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, canDelete: false }); |
||||
expect(context.canDeleteAnnotations!('dash-uid')).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('onAnnotationCreate', () => { |
||||
it('should create annotation', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, canAdd: true }); |
||||
|
||||
context.onAnnotationCreate!({ from: 100, to: 200, description: 'save it', tags: [] }); |
||||
|
||||
expect(postFn).toHaveBeenCalledWith('/api/annotations', { |
||||
dashboardUID: 'dash-1', |
||||
isRegion: true, |
||||
panelId: 4, |
||||
tags: [], |
||||
text: 'save it', |
||||
time: 100, |
||||
timeEnd: 200, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('onAnnotationUpdate', () => { |
||||
it('should update annotation', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, canAdd: true }); |
||||
|
||||
context.onAnnotationUpdate!({ from: 100, to: 200, id: 'event-id-123', description: 'updated', tags: [] }); |
||||
|
||||
expect(putFn).toHaveBeenCalledWith('/api/annotations/event-id-123', { |
||||
id: 'event-id-123', |
||||
dashboardUID: 'dash-1', |
||||
isRegion: true, |
||||
panelId: 4, |
||||
tags: [], |
||||
text: 'updated', |
||||
time: 100, |
||||
timeEnd: 200, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('onAnnotationDelete', () => { |
||||
it('should update annotation', () => { |
||||
const { context } = buildTestScene({ dashboardCanEdit: true, canAdd: true }); |
||||
|
||||
context.onAnnotationDelete!('I-do-not-want-you'); |
||||
|
||||
expect(deleteFn).toHaveBeenCalledWith('/api/annotations/I-do-not-want-you'); |
||||
}); |
||||
}); |
||||
|
||||
describe('onAddAdHocFilter', () => { |
||||
it('Should add new filter set', () => { |
||||
const { scene, context } = buildTestScene({}); |
||||
|
||||
context.onAddAdHocFilter!({ key: 'hello', value: 'world', operator: '=' }); |
||||
|
||||
const set = getAdHocFilterSetFor(scene, { uid: 'my-ds-uid' }); |
||||
|
||||
expect(set.state.filters).toEqual([{ key: 'hello', value: 'world', operator: '=' }]); |
||||
}); |
||||
|
||||
it('Should update and add filter to existing set', () => { |
||||
const { scene, context } = buildTestScene({ existingFilterSet: true }); |
||||
|
||||
const set = getAdHocFilterSetFor(scene, { uid: 'my-ds-uid' }); |
||||
|
||||
set.setState({ filters: [{ key: 'existing', value: 'world', operator: '=' }] }); |
||||
|
||||
context.onAddAdHocFilter!({ key: 'hello', value: 'world', operator: '=' }); |
||||
|
||||
expect(set.state.filters.length).toBe(2); |
||||
|
||||
// Can update existing filter value without adding a new filter
|
||||
context.onAddAdHocFilter!({ key: 'hello', value: 'world2', operator: '=' }); |
||||
// Verify existing filter value updated
|
||||
expect(set.state.filters[1].value).toBe('world2'); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
interface SceneOptions { |
||||
builtInAnnotationsEnabled?: boolean; |
||||
dashboardCanEdit?: boolean; |
||||
canAdd?: boolean; |
||||
canEdit?: boolean; |
||||
canDelete?: boolean; |
||||
orgCanEdit?: boolean; |
||||
existingFilterSet?: boolean; |
||||
} |
||||
|
||||
function buildTestScene(options: SceneOptions) { |
||||
const scene = transformSaveModelToScene({ |
||||
dashboard: { |
||||
title: 'hello', |
||||
uid: 'dash-1', |
||||
schemaVersion: 38, |
||||
annotations: { |
||||
list: [ |
||||
{ |
||||
builtIn: 1, |
||||
datasource: { |
||||
type: 'grafana', |
||||
uid: '-- Grafana --', |
||||
}, |
||||
enable: options.builtInAnnotationsEnabled ?? false, |
||||
hide: true, |
||||
iconColor: 'rgba(0, 211, 255, 1)', |
||||
name: 'Annotations & Alerts', |
||||
target: { refId: 'A' }, |
||||
type: 'dashboard', |
||||
}, |
||||
], |
||||
}, |
||||
panels: [ |
||||
{ |
||||
type: 'timeseries', |
||||
id: 4, |
||||
datasource: { uid: 'my-ds-uid', type: 'prometheus' }, |
||||
targets: [], |
||||
}, |
||||
], |
||||
templating: { |
||||
list: options.existingFilterSet |
||||
? [ |
||||
{ |
||||
type: 'adhoc', |
||||
name: 'Filters', |
||||
datasource: { uid: 'my-ds-uid' }, |
||||
}, |
||||
] |
||||
: [], |
||||
}, |
||||
}, |
||||
meta: { |
||||
canEdit: options.dashboardCanEdit, |
||||
annotationsPermissions: { |
||||
dashboard: { |
||||
canAdd: options.canAdd ?? false, |
||||
canEdit: options.canEdit ?? false, |
||||
canDelete: options.canDelete ?? false, |
||||
}, |
||||
organization: { |
||||
canAdd: false, |
||||
canEdit: options.orgCanEdit ?? false, |
||||
canDelete: options.canDelete ?? false, |
||||
}, |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
const vizPanel = findVizPanelByKey(scene, 'panel-4')!; |
||||
const context: PanelContext = { |
||||
eventBus: new EventBusSrv(), |
||||
eventsScope: 'global', |
||||
}; |
||||
|
||||
setDashboardPanelContext(vizPanel, context); |
||||
|
||||
return { scene, vizPanel, context }; |
||||
} |
@ -0,0 +1,188 @@ |
||||
import { AnnotationChangeEvent, AnnotationEventUIModel, CoreApp, DataFrame } from '@grafana/data'; |
||||
import { AdHocFilterSet, dataLayers, SceneDataLayers, VizPanel } from '@grafana/scenes'; |
||||
import { DataSourceRef } from '@grafana/schema'; |
||||
import { AdHocFilterItem, PanelContext } from '@grafana/ui'; |
||||
import { deleteAnnotation, saveAnnotation, updateAnnotation } from 'app/features/annotations/api'; |
||||
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; |
||||
|
||||
import { DashboardScene } from './DashboardScene'; |
||||
|
||||
export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelContext) { |
||||
context.app = CoreApp.Dashboard; |
||||
|
||||
context.canAddAnnotations = () => { |
||||
const dashboard = getDashboardSceneFor(vizPanel); |
||||
const builtInLayer = getBuiltInAnnotationsLayer(dashboard); |
||||
|
||||
// When there is no builtin annotations query we disable the ability to add annotations
|
||||
if (!builtInLayer || !dashboard.canEditDashboard()) { |
||||
return false; |
||||
} |
||||
|
||||
// If RBAC is enabled there are additional conditions to check.
|
||||
return Boolean(dashboard.state.meta.annotationsPermissions?.dashboard.canAdd); |
||||
}; |
||||
|
||||
context.canEditAnnotations = (dashboardUID?: string) => { |
||||
const dashboard = getDashboardSceneFor(vizPanel); |
||||
|
||||
if (!dashboard.canEditDashboard()) { |
||||
return false; |
||||
} |
||||
|
||||
if (dashboardUID) { |
||||
return Boolean(dashboard.state.meta.annotationsPermissions?.dashboard.canEdit); |
||||
} |
||||
|
||||
return Boolean(dashboard.state.meta.annotationsPermissions?.organization.canEdit); |
||||
}; |
||||
|
||||
context.canDeleteAnnotations = (dashboardUID?: string) => { |
||||
const dashboard = getDashboardSceneFor(vizPanel); |
||||
|
||||
if (!dashboard.canEditDashboard()) { |
||||
return false; |
||||
} |
||||
|
||||
if (dashboardUID) { |
||||
return Boolean(dashboard.state.meta.annotationsPermissions?.dashboard.canDelete); |
||||
} |
||||
|
||||
return Boolean(dashboard.state.meta.annotationsPermissions?.organization.canDelete); |
||||
}; |
||||
|
||||
context.onAnnotationCreate = async (event: AnnotationEventUIModel) => { |
||||
const dashboard = getDashboardSceneFor(vizPanel); |
||||
|
||||
const isRegion = event.from !== event.to; |
||||
const anno = { |
||||
dashboardUID: dashboard.state.uid, |
||||
panelId: getPanelIdForVizPanel(vizPanel), |
||||
isRegion, |
||||
time: event.from, |
||||
timeEnd: isRegion ? event.to : 0, |
||||
tags: event.tags, |
||||
text: event.description, |
||||
}; |
||||
|
||||
await saveAnnotation(anno); |
||||
|
||||
reRunBuiltInAnnotationsLayer(dashboard); |
||||
|
||||
context.eventBus.publish(new AnnotationChangeEvent(anno)); |
||||
}; |
||||
|
||||
context.onAnnotationUpdate = async (event: AnnotationEventUIModel) => { |
||||
const dashboard = getDashboardSceneFor(vizPanel); |
||||
|
||||
const isRegion = event.from !== event.to; |
||||
const anno = { |
||||
id: event.id, |
||||
dashboardUID: dashboard.state.uid, |
||||
panelId: getPanelIdForVizPanel(vizPanel), |
||||
isRegion, |
||||
time: event.from, |
||||
timeEnd: isRegion ? event.to : 0, |
||||
tags: event.tags, |
||||
text: event.description, |
||||
}; |
||||
|
||||
await updateAnnotation(anno); |
||||
|
||||
reRunBuiltInAnnotationsLayer(dashboard); |
||||
|
||||
context.eventBus.publish(new AnnotationChangeEvent(anno)); |
||||
}; |
||||
|
||||
context.onAnnotationDelete = async (id: string) => { |
||||
await deleteAnnotation({ id }); |
||||
|
||||
reRunBuiltInAnnotationsLayer(getDashboardSceneFor(vizPanel)); |
||||
|
||||
context.eventBus.publish(new AnnotationChangeEvent({ id })); |
||||
}; |
||||
|
||||
context.onAddAdHocFilter = (newFilter: AdHocFilterItem) => { |
||||
const dashboard = getDashboardSceneFor(vizPanel); |
||||
|
||||
const queryRunner = getQueryRunnerFor(vizPanel); |
||||
if (!queryRunner) { |
||||
return; |
||||
} |
||||
|
||||
const filterSet = getAdHocFilterSetFor(dashboard, queryRunner.state.datasource); |
||||
updateAdHocFilterSet(filterSet, newFilter); |
||||
}; |
||||
|
||||
context.onUpdateData = (frames: DataFrame[]): Promise<boolean> => { |
||||
// TODO
|
||||
//return onUpdatePanelSnapshotData(this.props.panel, frames);
|
||||
return Promise.resolve(true); |
||||
}; |
||||
} |
||||
|
||||
function getBuiltInAnnotationsLayer(scene: DashboardScene): dataLayers.AnnotationsDataLayer | undefined { |
||||
// When there is no builtin annotations query we disable the ability to add annotations
|
||||
if (scene.state.$data instanceof SceneDataLayers) { |
||||
for (const layer of scene.state.$data.state.layers) { |
||||
if (layer instanceof dataLayers.AnnotationsDataLayer) { |
||||
if (layer.state.isEnabled && layer.state.query.builtIn) { |
||||
return layer; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return undefined; |
||||
} |
||||
|
||||
function reRunBuiltInAnnotationsLayer(scene: DashboardScene) { |
||||
const layer = getBuiltInAnnotationsLayer(scene); |
||||
if (layer) { |
||||
layer.runLayer(); |
||||
} |
||||
} |
||||
|
||||
export function getAdHocFilterSetFor(scene: DashboardScene, ds: DataSourceRef | null | undefined) { |
||||
const controls = scene.state.controls ?? []; |
||||
|
||||
for (const control of controls) { |
||||
if (control instanceof AdHocFilterSet) { |
||||
if (control.state.datasource === ds || control.state.datasource?.uid === ds?.uid) { |
||||
return control; |
||||
} |
||||
} |
||||
} |
||||
|
||||
const newSet = new AdHocFilterSet({ datasource: ds }); |
||||
|
||||
// Add it to the scene
|
||||
scene.setState({ |
||||
controls: [controls[0], newSet, ...controls.slice(1)], |
||||
}); |
||||
|
||||
return newSet; |
||||
} |
||||
|
||||
function updateAdHocFilterSet(filterSet: AdHocFilterSet, newFilter: AdHocFilterItem) { |
||||
// Check if we need to update an existing filter
|
||||
for (const filter of filterSet.state.filters) { |
||||
if (filter.key === newFilter.key) { |
||||
filterSet.setState({ |
||||
filters: filterSet.state.filters.map((f) => { |
||||
if (f.key === newFilter.key) { |
||||
return newFilter; |
||||
} |
||||
return f; |
||||
}), |
||||
}); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
// Add new filter
|
||||
filterSet.setState({ |
||||
filters: [...filterSet.state.filters, newFilter], |
||||
}); |
||||
} |
Loading…
Reference in new issue