DashboardScene: Show plugin extensions in panel menu (#78702)
* DashboardScene: Show plugin extensions in panel menu * FIx test * Nits * Nit * Review nits
This commit is contained in:
@@ -6,7 +6,7 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { config, locationService, setPluginImportUtils } from '@grafana/runtime';
|
||||
import { config, getPluginLinkExtensions, locationService, setPluginImportUtils } from '@grafana/runtime';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
|
||||
import { setupLoadDashboardMock } from '../utils/test-utils';
|
||||
@@ -15,6 +15,8 @@ import { DashboardScenePage, Props } from './DashboardScenePage';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
setPluginExtensionGetter: jest.fn(),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
getDataSourceSrv: () => {
|
||||
return {
|
||||
get: jest.fn().mockResolvedValue({}),
|
||||
@@ -23,6 +25,8 @@ jest.mock('@grafana/runtime', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
|
||||
|
||||
function setup() {
|
||||
const context = getGrafanaContextMock();
|
||||
const props: Props = {
|
||||
@@ -97,6 +101,8 @@ describe('DashboardScenePage', () => {
|
||||
// hacky way because mocking autosizer does not work
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 });
|
||||
getPluginLinkExtensionsMock.mockRestore();
|
||||
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
|
||||
});
|
||||
|
||||
it('Can render dashboard', async () => {
|
||||
@@ -123,6 +129,7 @@ describe('DashboardScenePage', () => {
|
||||
await userEvent.click(screen.getByLabelText('Menu for panel with title Panel B'));
|
||||
|
||||
const inspectMenuItem = await screen.findAllByText('Inspect');
|
||||
|
||||
act(() => fireEvent.click(inspectMenuItem[0]));
|
||||
|
||||
expect(await screen.findByText('Inspect: Panel B')).toBeInTheDocument();
|
||||
|
||||
@@ -35,6 +35,8 @@ import { setupKeyboardShortcuts } from './keyboardShortcuts';
|
||||
export interface DashboardSceneState extends SceneObjectState {
|
||||
/** The title */
|
||||
title: string;
|
||||
/** Tags */
|
||||
tags?: string[];
|
||||
/** A uid when saved */
|
||||
uid?: string;
|
||||
/** @deprecated */
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import { InterpolateFunction, PanelMenuItem } from '@grafana/data';
|
||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { VizPanel, VizPanelMenu, sceneGraph } from '@grafana/scenes';
|
||||
import {
|
||||
InterpolateFunction,
|
||||
PanelMenuItem,
|
||||
PluginExtensionPanelContext,
|
||||
PluginExtensionPoints,
|
||||
getTimeZone,
|
||||
} from '@grafana/data';
|
||||
import { config, getPluginLinkExtensions, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import {
|
||||
LocalValueVariable,
|
||||
SceneDataTransformer,
|
||||
SceneGridRow,
|
||||
SceneQueryRunner,
|
||||
VizPanel,
|
||||
VizPanelMenu,
|
||||
sceneGraph,
|
||||
} from '@grafana/scenes';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { InspectTab } from 'app/features/inspector/types';
|
||||
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
|
||||
import { addDataTrailPanelAction } from 'app/features/trails/dashboardIntegration';
|
||||
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
@@ -14,6 +30,7 @@ import { getPanelIdForVizPanel } from '../utils/utils';
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { LibraryVizPanel } from './LibraryVizPanel';
|
||||
import { VizPanelLinks } from './PanelLinks';
|
||||
import { ShareQueryDataProvider } from './ShareQueryDataProvider';
|
||||
|
||||
/**
|
||||
* Behavior is called when VizPanelMenu is activated (ie when it's opened).
|
||||
@@ -151,6 +168,23 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
||||
subMenu: inspectSubMenu.length > 0 ? inspectSubMenu : undefined,
|
||||
});
|
||||
|
||||
if (dashboard instanceof DashboardScene) {
|
||||
const { extensions } = getPluginLinkExtensions({
|
||||
extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
|
||||
context: createExtensionContext(panel, dashboard),
|
||||
limitPerPlugin: 3,
|
||||
});
|
||||
|
||||
if (extensions.length > 0 && !dashboard.state.isEditing) {
|
||||
items.push({
|
||||
text: 'Extensions',
|
||||
iconClassName: 'plug',
|
||||
type: 'submenu',
|
||||
subMenu: createExtensionSubMenu(extensions),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (moreSubMenu.length) {
|
||||
items.push({
|
||||
type: 'submenu',
|
||||
@@ -196,3 +230,69 @@ export function getPanelLinksBehavior(panel: PanelModel) {
|
||||
panelLinksMenu.setState({ links });
|
||||
};
|
||||
}
|
||||
|
||||
function createExtensionContext(panel: VizPanel, dashboard: DashboardScene): PluginExtensionPanelContext {
|
||||
const timeRange = sceneGraph.getTimeRange(panel);
|
||||
let queryRunner = panel.state.$data;
|
||||
let targets: DataQuery[] = [];
|
||||
const id = getPanelIdForVizPanel(panel);
|
||||
|
||||
if (queryRunner instanceof SceneDataTransformer) {
|
||||
queryRunner = queryRunner.state.$data;
|
||||
}
|
||||
|
||||
if (queryRunner instanceof SceneQueryRunner) {
|
||||
targets = queryRunner.state.queries;
|
||||
}
|
||||
|
||||
if (queryRunner instanceof ShareQueryDataProvider) {
|
||||
targets = [queryRunner.state.query];
|
||||
}
|
||||
|
||||
let scopedVars = {};
|
||||
|
||||
// Handle panel repeats scenario
|
||||
if (panel.state.$variables) {
|
||||
panel.state.$variables.state.variables.forEach((variable) => {
|
||||
if (variable instanceof LocalValueVariable) {
|
||||
scopedVars = {
|
||||
...scopedVars,
|
||||
[variable.state.name]: { value: variable.getValue(), text: variable.getValueText() },
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle row repeats scenario
|
||||
if (panel.parent?.parent instanceof SceneGridRow) {
|
||||
const row = panel.parent.parent;
|
||||
if (row.state.$variables) {
|
||||
row.state.$variables.state.variables.forEach((variable) => {
|
||||
if (variable instanceof LocalValueVariable) {
|
||||
scopedVars = {
|
||||
...scopedVars,
|
||||
[variable.state.name]: { value: variable.getValue(), text: variable.getValueText() },
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
pluginId: panel.state.pluginId,
|
||||
title: panel.state.title,
|
||||
timeRange: timeRange.state.value.raw,
|
||||
timeZone: getTimeZone({
|
||||
timeZone: timeRange.getTimeZone(),
|
||||
}),
|
||||
dashboard: {
|
||||
uid: dashboard.state.uid!,
|
||||
title: dashboard.state.title,
|
||||
tags: dashboard.state.tags || [],
|
||||
},
|
||||
targets,
|
||||
scopedVars,
|
||||
data: queryRunner?.state.data,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -225,6 +225,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
||||
|
||||
return new DashboardScene({
|
||||
title: oldModel.title,
|
||||
tags: oldModel.tags || [],
|
||||
uid: oldModel.uid,
|
||||
id: oldModel.id,
|
||||
meta: oldModel.meta,
|
||||
|
||||
+10
-1
@@ -14,7 +14,7 @@ import {
|
||||
VariableSupportType,
|
||||
} from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { setPluginImportUtils } from '@grafana/runtime';
|
||||
import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime';
|
||||
import {
|
||||
MultiValueVariable,
|
||||
SceneDataLayers,
|
||||
@@ -148,8 +148,12 @@ jest.mock('@grafana/runtime', () => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
setPluginExtensionGetter: jest.fn(),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
}));
|
||||
|
||||
const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
|
||||
|
||||
jest.mock('@grafana/scenes', () => ({
|
||||
...jest.requireActual('@grafana/scenes'),
|
||||
sceneUtils: {
|
||||
@@ -159,6 +163,11 @@ jest.mock('@grafana/scenes', () => ({
|
||||
}));
|
||||
|
||||
describe('transformSceneToSaveModel', () => {
|
||||
beforeEach(() => {
|
||||
getPluginLinkExtensionsMock.mockRestore();
|
||||
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
|
||||
});
|
||||
|
||||
describe('Given a simple scene with variables', () => {
|
||||
it('Should transform back to persisted model', () => {
|
||||
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
getTimeZone,
|
||||
PanelMenuItem,
|
||||
PluginExtensionLink,
|
||||
PluginExtensionPoints,
|
||||
urlUtil,
|
||||
type PluginExtensionPanelContext,
|
||||
@@ -26,7 +25,7 @@ import {
|
||||
} from 'app/features/dashboard/utils/panel';
|
||||
import { InspectTab } from 'app/features/inspector/types';
|
||||
import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard';
|
||||
import { truncateTitle } from 'app/features/plugins/extensions/utils';
|
||||
import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { store } from 'app/store/store';
|
||||
|
||||
@@ -375,53 +374,3 @@ function createExtensionContext(panel: PanelModel, dashboard: DashboardModel): P
|
||||
data: panel.getQueryRunner().getLastResult(),
|
||||
};
|
||||
}
|
||||
|
||||
function createExtensionSubMenu(extensions: PluginExtensionLink[]): PanelMenuItem[] {
|
||||
const categorized: Record<string, PanelMenuItem[]> = {};
|
||||
const uncategorized: PanelMenuItem[] = [];
|
||||
|
||||
for (const extension of extensions) {
|
||||
const category = extension.category;
|
||||
|
||||
if (!category) {
|
||||
uncategorized.push({
|
||||
text: truncateTitle(extension.title, 25),
|
||||
href: extension.path,
|
||||
onClick: extension.onClick,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Array.isArray(categorized[category])) {
|
||||
categorized[category] = [];
|
||||
}
|
||||
|
||||
categorized[category].push({
|
||||
text: truncateTitle(extension.title, 25),
|
||||
href: extension.path,
|
||||
onClick: extension.onClick,
|
||||
});
|
||||
}
|
||||
|
||||
const subMenu = Object.keys(categorized).reduce((subMenu: PanelMenuItem[], category) => {
|
||||
subMenu.push({
|
||||
text: truncateTitle(category, 25),
|
||||
type: 'group',
|
||||
subMenu: categorized[category],
|
||||
});
|
||||
return subMenu;
|
||||
}, []);
|
||||
|
||||
if (uncategorized.length > 0) {
|
||||
if (subMenu.length > 0) {
|
||||
subMenu.push({
|
||||
text: 'divider',
|
||||
type: 'divider',
|
||||
});
|
||||
}
|
||||
|
||||
Array.prototype.push.apply(subMenu, uncategorized);
|
||||
}
|
||||
|
||||
return subMenu;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
isDateTime,
|
||||
dateTime,
|
||||
PluginContextProvider,
|
||||
PluginExtensionLink,
|
||||
PanelMenuItem,
|
||||
} from '@grafana/data';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@@ -249,3 +251,53 @@ export function truncateTitle(title: string, length: number): string {
|
||||
const part = title.slice(0, length - 3);
|
||||
return `${part.trimEnd()}...`;
|
||||
}
|
||||
|
||||
export function createExtensionSubMenu(extensions: PluginExtensionLink[]): PanelMenuItem[] {
|
||||
const categorized: Record<string, PanelMenuItem[]> = {};
|
||||
const uncategorized: PanelMenuItem[] = [];
|
||||
|
||||
for (const extension of extensions) {
|
||||
const category = extension.category;
|
||||
|
||||
if (!category) {
|
||||
uncategorized.push({
|
||||
text: truncateTitle(extension.title, 25),
|
||||
href: extension.path,
|
||||
onClick: extension.onClick,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Array.isArray(categorized[category])) {
|
||||
categorized[category] = [];
|
||||
}
|
||||
|
||||
categorized[category].push({
|
||||
text: truncateTitle(extension.title, 25),
|
||||
href: extension.path,
|
||||
onClick: extension.onClick,
|
||||
});
|
||||
}
|
||||
|
||||
const subMenu = Object.keys(categorized).reduce((subMenu: PanelMenuItem[], category) => {
|
||||
subMenu.push({
|
||||
text: truncateTitle(category, 25),
|
||||
type: 'group',
|
||||
subMenu: categorized[category],
|
||||
});
|
||||
return subMenu;
|
||||
}, []);
|
||||
|
||||
if (uncategorized.length > 0) {
|
||||
if (subMenu.length > 0) {
|
||||
subMenu.push({
|
||||
text: 'divider',
|
||||
type: 'divider',
|
||||
});
|
||||
}
|
||||
|
||||
Array.prototype.push.apply(subMenu, uncategorized);
|
||||
}
|
||||
|
||||
return subMenu;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user