|
|
|
|
@ -1,6 +1,24 @@ |
|
|
|
|
import { |
|
|
|
|
FieldType, |
|
|
|
|
LoadingState, |
|
|
|
|
PanelData, |
|
|
|
|
PluginExtensionPanelContext, |
|
|
|
|
PluginExtensionTypes, |
|
|
|
|
getDefaultTimeRange, |
|
|
|
|
toDataFrame, |
|
|
|
|
} from '@grafana/data'; |
|
|
|
|
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; |
|
|
|
|
import { locationService } from '@grafana/runtime'; |
|
|
|
|
import { SceneGridItem, SceneGridLayout, SceneQueryRunner, VizPanel, VizPanelMenu } from '@grafana/scenes'; |
|
|
|
|
import { getPluginLinkExtensions, locationService } from '@grafana/runtime'; |
|
|
|
|
import { |
|
|
|
|
LocalValueVariable, |
|
|
|
|
SceneGridItem, |
|
|
|
|
SceneGridLayout, |
|
|
|
|
SceneQueryRunner, |
|
|
|
|
SceneTimeRange, |
|
|
|
|
SceneVariableSet, |
|
|
|
|
VizPanel, |
|
|
|
|
VizPanelMenu, |
|
|
|
|
} from '@grafana/scenes'; |
|
|
|
|
import { contextSrv } from 'app/core/services/context_srv'; |
|
|
|
|
import { GetExploreUrlArguments } from 'app/core/utils/explore'; |
|
|
|
|
|
|
|
|
|
@ -21,7 +39,20 @@ jest.mock('app/core/utils/explore', () => ({ |
|
|
|
|
|
|
|
|
|
jest.mock('app/core/services/context_srv'); |
|
|
|
|
|
|
|
|
|
jest.mock('@grafana/runtime', () => ({ |
|
|
|
|
...jest.requireActual('@grafana/runtime'), |
|
|
|
|
setPluginExtensionGetter: jest.fn(), |
|
|
|
|
getPluginLinkExtensions: jest.fn(), |
|
|
|
|
})); |
|
|
|
|
|
|
|
|
|
const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions); |
|
|
|
|
|
|
|
|
|
describe('panelMenuBehavior', () => { |
|
|
|
|
beforeEach(() => { |
|
|
|
|
getPluginLinkExtensionsMock.mockRestore(); |
|
|
|
|
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] }); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
beforeAll(() => { |
|
|
|
|
locationService.push('/scenes/dashboard/dash-1?from=now-5m&to=now'); |
|
|
|
|
}); |
|
|
|
|
@ -51,7 +82,7 @@ describe('panelMenuBehavior', () => { |
|
|
|
|
// Verify explore url is called with correct arguments
|
|
|
|
|
const getExploreArgs: GetExploreUrlArguments = mocks.getExploreUrl.mock.calls[0][0]; |
|
|
|
|
expect(getExploreArgs.dsRef).toEqual({ uid: 'my-uid' }); |
|
|
|
|
expect(getExploreArgs.queries).toEqual([{ query: 'buu', refId: 'A' }]); |
|
|
|
|
expect(getExploreArgs.queries).toEqual([{ query: 'QueryA', refId: 'A' }]); |
|
|
|
|
expect(getExploreArgs.scopedVars?.__sceneObject?.value).toBe(panel); |
|
|
|
|
|
|
|
|
|
// verify inspect url keeps url params and adds inspect=<panel-key>
|
|
|
|
|
@ -60,6 +91,384 @@ describe('panelMenuBehavior', () => { |
|
|
|
|
|
|
|
|
|
expect(menu.state.items?.[4].subMenu?.length).toBe(3); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('when extending panel menu from plugins', () => { |
|
|
|
|
it('should contain menu item from link extension', async () => { |
|
|
|
|
getPluginLinkExtensionsMock.mockReturnValue({ |
|
|
|
|
extensions: [ |
|
|
|
|
{ |
|
|
|
|
id: '1', |
|
|
|
|
pluginId: '...', |
|
|
|
|
type: PluginExtensionTypes.link, |
|
|
|
|
title: 'Declare incident', |
|
|
|
|
description: 'Declaring an incident in the app', |
|
|
|
|
path: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const { menu, panel } = await buildTestScene({}); |
|
|
|
|
|
|
|
|
|
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); |
|
|
|
|
|
|
|
|
|
mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); |
|
|
|
|
mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); |
|
|
|
|
|
|
|
|
|
menu.activate(); |
|
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 1)); |
|
|
|
|
|
|
|
|
|
expect(menu.state.items?.length).toBe(7); |
|
|
|
|
|
|
|
|
|
const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; |
|
|
|
|
|
|
|
|
|
expect(extensionsSubMenu).toEqual( |
|
|
|
|
expect.arrayContaining([ |
|
|
|
|
expect.objectContaining({ |
|
|
|
|
text: 'Declare incident', |
|
|
|
|
href: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
}), |
|
|
|
|
]) |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should truncate menu item title to 25 chars', async () => { |
|
|
|
|
getPluginLinkExtensionsMock.mockReturnValue({ |
|
|
|
|
extensions: [ |
|
|
|
|
{ |
|
|
|
|
id: '1', |
|
|
|
|
pluginId: '...', |
|
|
|
|
type: PluginExtensionTypes.link, |
|
|
|
|
title: 'Declare incident when pressing this amazing menu item', |
|
|
|
|
description: 'Declaring an incident in the app', |
|
|
|
|
path: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const { menu, panel } = await buildTestScene({}); |
|
|
|
|
|
|
|
|
|
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); |
|
|
|
|
|
|
|
|
|
mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); |
|
|
|
|
mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); |
|
|
|
|
|
|
|
|
|
menu.activate(); |
|
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 1)); |
|
|
|
|
|
|
|
|
|
expect(menu.state.items?.length).toBe(7); |
|
|
|
|
|
|
|
|
|
const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; |
|
|
|
|
|
|
|
|
|
expect(extensionsSubMenu).toEqual( |
|
|
|
|
expect.arrayContaining([ |
|
|
|
|
expect.objectContaining({ |
|
|
|
|
text: 'Declare incident when...', |
|
|
|
|
href: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
}), |
|
|
|
|
]) |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should pass onClick from plugin extension link to menu item', async () => { |
|
|
|
|
const expectedOnClick = jest.fn(); |
|
|
|
|
|
|
|
|
|
getPluginLinkExtensionsMock.mockReturnValue({ |
|
|
|
|
extensions: [ |
|
|
|
|
{ |
|
|
|
|
id: '1', |
|
|
|
|
pluginId: '...', |
|
|
|
|
type: PluginExtensionTypes.link, |
|
|
|
|
title: 'Declare incident when pressing this amazing menu item', |
|
|
|
|
description: 'Declaring an incident in the app', |
|
|
|
|
onClick: expectedOnClick, |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const { menu, panel } = await buildTestScene({}); |
|
|
|
|
|
|
|
|
|
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); |
|
|
|
|
|
|
|
|
|
mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); |
|
|
|
|
mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); |
|
|
|
|
|
|
|
|
|
menu.activate(); |
|
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 1)); |
|
|
|
|
|
|
|
|
|
expect(menu.state.items?.length).toBe(7); |
|
|
|
|
|
|
|
|
|
const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; |
|
|
|
|
const menuItem = extensionsSubMenu?.find((i) => (i.text = 'Declare incident when...')); |
|
|
|
|
|
|
|
|
|
menuItem?.onClick?.({} as React.MouseEvent); |
|
|
|
|
expect(expectedOnClick).toBeCalledTimes(1); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should pass context with correct values when configuring extension', async () => { |
|
|
|
|
const data: PanelData = { |
|
|
|
|
series: [ |
|
|
|
|
toDataFrame({ |
|
|
|
|
fields: [ |
|
|
|
|
{ name: 'time', type: FieldType.time }, |
|
|
|
|
{ name: 'score', type: FieldType.number }, |
|
|
|
|
], |
|
|
|
|
}), |
|
|
|
|
], |
|
|
|
|
timeRange: getDefaultTimeRange(), |
|
|
|
|
state: LoadingState.Done, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const { menu, panel } = await buildTestScene({}); |
|
|
|
|
|
|
|
|
|
panel.state.$data?.setState({ data }); |
|
|
|
|
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); |
|
|
|
|
|
|
|
|
|
menu.activate(); |
|
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 1)); |
|
|
|
|
|
|
|
|
|
const context: PluginExtensionPanelContext = { |
|
|
|
|
id: 12, |
|
|
|
|
pluginId: 'table', |
|
|
|
|
title: 'Panel A', |
|
|
|
|
timeZone: 'Africa/Abidjan', |
|
|
|
|
timeRange: { |
|
|
|
|
from: 'now-5m', |
|
|
|
|
to: 'now', |
|
|
|
|
}, |
|
|
|
|
targets: [ |
|
|
|
|
{ |
|
|
|
|
refId: 'A', |
|
|
|
|
// @ts-expect-error
|
|
|
|
|
query: 'QueryA', |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
dashboard: { |
|
|
|
|
tags: ['database', 'panel'], |
|
|
|
|
uid: 'dash-1', |
|
|
|
|
title: 'My dashboard', |
|
|
|
|
}, |
|
|
|
|
scopedVars: { |
|
|
|
|
a: { |
|
|
|
|
text: 'a', |
|
|
|
|
value: 'a', |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
data, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
expect(getPluginLinkExtensionsMock).toBeCalledWith(expect.objectContaining({ context })); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should pass context with default time zone values when configuring extension', async () => { |
|
|
|
|
const data: PanelData = { |
|
|
|
|
series: [ |
|
|
|
|
toDataFrame({ |
|
|
|
|
fields: [ |
|
|
|
|
{ name: 'time', type: FieldType.time }, |
|
|
|
|
{ name: 'score', type: FieldType.number }, |
|
|
|
|
], |
|
|
|
|
}), |
|
|
|
|
], |
|
|
|
|
timeRange: getDefaultTimeRange(), |
|
|
|
|
state: LoadingState.Done, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const { menu, panel, scene } = await buildTestScene({}); |
|
|
|
|
|
|
|
|
|
panel.state.$data?.setState({ data }); |
|
|
|
|
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); |
|
|
|
|
scene.state.$timeRange?.setState({ timeZone: undefined }); |
|
|
|
|
|
|
|
|
|
menu.activate(); |
|
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 1)); |
|
|
|
|
|
|
|
|
|
const context: PluginExtensionPanelContext = { |
|
|
|
|
id: 12, |
|
|
|
|
pluginId: 'table', |
|
|
|
|
title: 'Panel A', |
|
|
|
|
timeZone: 'browser', |
|
|
|
|
timeRange: { |
|
|
|
|
from: 'now-5m', |
|
|
|
|
to: 'now', |
|
|
|
|
}, |
|
|
|
|
targets: [ |
|
|
|
|
{ |
|
|
|
|
refId: 'A', |
|
|
|
|
// @ts-expect-error
|
|
|
|
|
query: 'QueryA', |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
dashboard: { |
|
|
|
|
tags: ['database', 'panel'], |
|
|
|
|
uid: 'dash-1', |
|
|
|
|
title: 'My dashboard', |
|
|
|
|
}, |
|
|
|
|
scopedVars: { |
|
|
|
|
a: { |
|
|
|
|
text: 'a', |
|
|
|
|
value: 'a', |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
data, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
expect(getPluginLinkExtensionsMock).toBeCalledWith(expect.objectContaining({ context })); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should contain menu item with category', async () => { |
|
|
|
|
getPluginLinkExtensionsMock.mockReturnValue({ |
|
|
|
|
extensions: [ |
|
|
|
|
{ |
|
|
|
|
id: '1', |
|
|
|
|
pluginId: '...', |
|
|
|
|
type: PluginExtensionTypes.link, |
|
|
|
|
title: 'Declare incident', |
|
|
|
|
description: 'Declaring an incident in the app', |
|
|
|
|
path: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
category: 'Incident', |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const { menu, panel } = await buildTestScene({}); |
|
|
|
|
|
|
|
|
|
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); |
|
|
|
|
|
|
|
|
|
mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); |
|
|
|
|
mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); |
|
|
|
|
|
|
|
|
|
menu.activate(); |
|
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 1)); |
|
|
|
|
|
|
|
|
|
expect(menu.state.items?.length).toBe(7); |
|
|
|
|
|
|
|
|
|
const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; |
|
|
|
|
|
|
|
|
|
expect(extensionsSubMenu).toEqual( |
|
|
|
|
expect.arrayContaining([ |
|
|
|
|
expect.objectContaining({ |
|
|
|
|
text: 'Incident', |
|
|
|
|
subMenu: expect.arrayContaining([ |
|
|
|
|
expect.objectContaining({ |
|
|
|
|
text: 'Declare incident', |
|
|
|
|
href: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
}), |
|
|
|
|
]), |
|
|
|
|
}), |
|
|
|
|
]) |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should truncate category to 25 chars', async () => { |
|
|
|
|
getPluginLinkExtensionsMock.mockReturnValue({ |
|
|
|
|
extensions: [ |
|
|
|
|
{ |
|
|
|
|
id: '1', |
|
|
|
|
pluginId: '...', |
|
|
|
|
type: PluginExtensionTypes.link, |
|
|
|
|
title: 'Declare incident', |
|
|
|
|
description: 'Declaring an incident in the app', |
|
|
|
|
path: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
category: 'Declare incident when pressing this amazing menu item', |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const { menu, panel } = await buildTestScene({}); |
|
|
|
|
|
|
|
|
|
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); |
|
|
|
|
|
|
|
|
|
mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); |
|
|
|
|
mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); |
|
|
|
|
|
|
|
|
|
menu.activate(); |
|
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 1)); |
|
|
|
|
|
|
|
|
|
expect(menu.state.items?.length).toBe(7); |
|
|
|
|
|
|
|
|
|
const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; |
|
|
|
|
|
|
|
|
|
expect(extensionsSubMenu).toEqual( |
|
|
|
|
expect.arrayContaining([ |
|
|
|
|
expect.objectContaining({ |
|
|
|
|
text: 'Declare incident when...', |
|
|
|
|
subMenu: expect.arrayContaining([ |
|
|
|
|
expect.objectContaining({ |
|
|
|
|
text: 'Declare incident', |
|
|
|
|
href: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
}), |
|
|
|
|
]), |
|
|
|
|
}), |
|
|
|
|
]) |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should contain menu item with category and append items without category after divider', async () => { |
|
|
|
|
getPluginLinkExtensionsMock.mockReturnValue({ |
|
|
|
|
extensions: [ |
|
|
|
|
{ |
|
|
|
|
id: '1', |
|
|
|
|
pluginId: '...', |
|
|
|
|
type: PluginExtensionTypes.link, |
|
|
|
|
title: 'Declare incident', |
|
|
|
|
description: 'Declaring an incident in the app', |
|
|
|
|
path: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
category: 'Incident', |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
id: '2', |
|
|
|
|
pluginId: '...', |
|
|
|
|
type: PluginExtensionTypes.link, |
|
|
|
|
title: 'Create forecast', |
|
|
|
|
description: 'Declaring an incident in the app', |
|
|
|
|
path: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const { menu, panel } = await buildTestScene({}); |
|
|
|
|
|
|
|
|
|
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); |
|
|
|
|
|
|
|
|
|
mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); |
|
|
|
|
mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); |
|
|
|
|
|
|
|
|
|
menu.activate(); |
|
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 1)); |
|
|
|
|
|
|
|
|
|
expect(menu.state.items?.length).toBe(7); |
|
|
|
|
|
|
|
|
|
const extensionsSubMenu = menu.state.items?.find((i) => i.text === 'Extensions')?.subMenu; |
|
|
|
|
|
|
|
|
|
expect(extensionsSubMenu).toEqual( |
|
|
|
|
expect.arrayContaining([ |
|
|
|
|
expect.objectContaining({ |
|
|
|
|
text: 'Incident', |
|
|
|
|
subMenu: expect.arrayContaining([ |
|
|
|
|
expect.objectContaining({ |
|
|
|
|
text: 'Declare incident', |
|
|
|
|
href: '/a/grafana-basic-app/declare-incident', |
|
|
|
|
}), |
|
|
|
|
]), |
|
|
|
|
}), |
|
|
|
|
expect.objectContaining({ |
|
|
|
|
type: 'divider', |
|
|
|
|
}), |
|
|
|
|
expect.objectContaining({ |
|
|
|
|
text: 'Create forecast', |
|
|
|
|
}), |
|
|
|
|
]) |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
interface SceneOptions {} |
|
|
|
|
@ -74,15 +483,24 @@ async function buildTestScene(options: SceneOptions) { |
|
|
|
|
pluginId: 'table', |
|
|
|
|
key: 'panel-12', |
|
|
|
|
menu, |
|
|
|
|
$variables: new SceneVariableSet({ |
|
|
|
|
variables: [new LocalValueVariable({ name: 'a', value: 'a', text: 'a' })], |
|
|
|
|
}), |
|
|
|
|
$data: new SceneQueryRunner({ |
|
|
|
|
datasource: { uid: 'my-uid' }, |
|
|
|
|
queries: [{ query: 'buu', refId: 'A' }], |
|
|
|
|
queries: [{ query: 'QueryA', refId: 'A' }], |
|
|
|
|
}), |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const scene = new DashboardScene({ |
|
|
|
|
title: 'hello', |
|
|
|
|
title: 'My dashboard', |
|
|
|
|
uid: 'dash-1', |
|
|
|
|
tags: ['database', 'panel'], |
|
|
|
|
$timeRange: new SceneTimeRange({ |
|
|
|
|
from: 'now-5m', |
|
|
|
|
to: 'now', |
|
|
|
|
timeZone: 'Africa/Abidjan', |
|
|
|
|
}), |
|
|
|
|
meta: { |
|
|
|
|
canEdit: true, |
|
|
|
|
}, |
|
|
|
|
|