Submit
Path:
~
/
/
usr
/
share
/
grafana
/
public
/
app
/
features
/
dashboard-scene
/
scene
/
File Content:
PanelMenuBehavior.tsx
import { InterpolateFunction, PanelMenuItem, PanelPlugin, PluginExtensionPanelContext, PluginExtensionPoints, getTimeZone, urlUtil, } from '@grafana/data'; import { config, getPluginLinkExtensions, locationService } from '@grafana/runtime'; import { LocalValueVariable, SceneFlexLayout, SceneGridLayout, SceneGridRow, SceneObject, VizPanel, VizPanelMenu, sceneGraph, } from '@grafana/scenes'; import { DataQuery, OptionsWithLegend } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { t } from 'app/core/internationalization'; import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; import { InspectTab } from 'app/features/inspector/types'; import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils'; import { addDataTrailPanelAction } from 'app/features/trails/dashboardIntegration'; import { ShowConfirmModalEvent } from 'app/types/events'; import { ShareModal } from '../sharing/ShareModal'; import { DashboardInteractions } from '../utils/interactions'; import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; import { LibraryVizPanel } from './LibraryVizPanel'; import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; /** * Behavior is called when VizPanelMenu is activated (ie when it's opened). */ export function panelMenuBehavior(menu: VizPanelMenu) { const asyncFunc = async () => { // hm.. add another generic param to SceneObject to specify parent type? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const panel = menu.parent as VizPanel; const plugin = panel.getPlugin(); const items: PanelMenuItem[] = []; const moreSubMenu: PanelMenuItem[] = []; const panelId = getPanelIdForVizPanel(panel); const dashboard = getDashboardSceneFor(panel); const { isEmbedded } = dashboard.state.meta; const exploreMenuItem = await getExploreMenuItem(panel); // For embedded dashboards we only have explore action for now if (isEmbedded) { if (exploreMenuItem) { menu.setState({ items: [exploreMenuItem] }); } return; } items.push({ text: t('panel.header-menu.view', `View`), iconClassName: 'eye', shortcut: 'v', onClick: () => DashboardInteractions.panelMenuItemClicked('view'), href: getViewPanelUrl(panel), }); if (dashboard.canEditDashboard()) { // We could check isEditing here but I kind of think this should always be in the menu, // and going into panel edit should make the dashboard go into edit mode is it's not already items.push({ text: t('panel.header-menu.edit', `Edit`), iconClassName: 'eye', shortcut: 'e', onClick: () => DashboardInteractions.panelMenuItemClicked('edit'), href: getEditPanelUrl(panelId), }); } items.push({ text: t('panel.header-menu.share', `Share`), iconClassName: 'share-alt', onClick: () => { DashboardInteractions.panelMenuItemClicked('share'); dashboard.showModal(new ShareModal({ panelRef: panel.getRef(), dashboardRef: dashboard.getRef() })); }, shortcut: 'p s', }); moreSubMenu.push({ text: t('panel.header-menu.duplicate', `Duplicate`), onClick: () => { DashboardInteractions.panelMenuItemClicked('duplicate'); dashboard.duplicatePanel(panel); }, shortcut: 'p d', }); moreSubMenu.push({ text: t('panel.header-menu.copy', `Copy`), onClick: () => { DashboardInteractions.panelMenuItemClicked('copy'); dashboard.copyPanel(panel); }, }); if (panel.parent instanceof LibraryVizPanel) { // TODO: Implement lib panel unlinking } else { moreSubMenu.push({ text: t('panel.header-menu.create-library-panel', `Create library panel`), onClick: () => { DashboardInteractions.panelMenuItemClicked('createLibraryPanel'); dashboard.showModal( new ShareModal({ panelRef: panel.getRef(), dashboardRef: dashboard.getRef(), activeTab: shareDashboardType.libraryPanel, }) ); }, }); } moreSubMenu.push({ text: t('panel.header-menu.new-alert-rule', `New alert rule`), onClick: (e) => onCreateAlert(panel), }); if (hasLegendOptions(panel.state.options)) { moreSubMenu.push({ text: panel.state.options.legend.showLegend ? t('panel.header-menu.hide-legend', 'Hide legend') : t('panel.header-menu.show-legend', 'Show legend'), onClick: (e) => { e.preventDefault(); toggleVizPanelLegend(panel); }, shortcut: 'p l', }); } if (dashboard.canEditDashboard() && plugin && !plugin.meta.skipDataQuery) { moreSubMenu.push({ text: t('panel.header-menu.get-help', 'Get help'), onClick: (e: React.MouseEvent) => { e.preventDefault(); onInspectPanel(panel, InspectTab.Help); }, }); } if (config.featureToggles.datatrails) { addDataTrailPanelAction(dashboard, panel, items); } if (exploreMenuItem) { items.push(exploreMenuItem); } items.push(getInspectMenuItem(plugin, panel, dashboard)); 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', text: t('panel.header-menu.more', `More...`), iconClassName: 'cube', subMenu: moreSubMenu, onClick: (e) => { e.preventDefault(); }, }); } items.push({ text: '', type: 'divider', }); items.push({ text: t('panel.header-menu.remove', `Remove`), iconClassName: 'trash-alt', onClick: () => { DashboardInteractions.panelMenuItemClicked('remove'); removePanel(dashboard, panel, true); }, shortcut: 'p r', }); menu.setState({ items }); }; asyncFunc(); } async function getExploreMenuItem(panel: VizPanel): Promise<PanelMenuItem | undefined> { const exploreUrl = await tryGetExploreUrlForPanel(panel); if (!exploreUrl) { return undefined; } return { text: t('panel.header-menu.explore', `Explore`), iconClassName: 'compass', shortcut: 'p x', onClick: () => DashboardInteractions.panelMenuItemClicked('explore'), href: exploreUrl, }; } function getInspectMenuItem( plugin: PanelPlugin | undefined, panel: VizPanel, dashboard: DashboardScene ): PanelMenuItem { const inspectSubMenu: PanelMenuItem[] = []; if (plugin && !plugin.meta.skipDataQuery) { inspectSubMenu.push({ text: t('panel.header-menu.inspect-data', `Data`), href: getInspectUrl(panel, InspectTab.Data), onClick: (e) => { e.preventDefault(); locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.Data }); DashboardInteractions.panelMenuInspectClicked(InspectTab.Data); }, }); if (dashboard instanceof DashboardScene && dashboard.state.meta.canEdit) { inspectSubMenu.push({ text: t('panel.header-menu.query', `Query`), href: getInspectUrl(panel, InspectTab.Query), onClick: (e) => { e.preventDefault(); locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.Query }); DashboardInteractions.panelMenuInspectClicked(InspectTab.Query); }, }); } } inspectSubMenu.push({ text: t('panel.header-menu.inspect-json', `Panel JSON`), href: getInspectUrl(panel, InspectTab.JSON), onClick: (e) => { e.preventDefault(); locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.JSON }); DashboardInteractions.panelMenuInspectClicked(InspectTab.JSON); }, }); return { text: t('panel.header-menu.inspect', `Inspect`), iconClassName: 'info-circle', shortcut: 'i', href: getInspectUrl(panel), onClick: (e) => { if (!e.isDefaultPrevented()) { locationService.partial({ inspect: panel.state.key, inspectTab: InspectTab.Data }); DashboardInteractions.panelMenuInspectClicked(InspectTab.Data); } }, subMenu: inspectSubMenu.length > 0 ? inspectSubMenu : undefined, }; } /** * Behavior is called when VizPanelLinksMenu is activated (when it's opened). */ export function panelLinksBehavior(panelLinksMenu: VizPanelLinksMenu) { if (!(panelLinksMenu.parent instanceof VizPanelLinks)) { throw new Error('parent of VizPanelLinksMenu must be VizPanelLinks'); } const panel = panelLinksMenu.parent.parent; if (!(panel instanceof VizPanel)) { throw new Error('parent of VizPanelLinks must be VizPanel'); } panelLinksMenu.setState({ links: getPanelLinks(panel) }); } export function getPanelLinks(panel: VizPanel) { const interpolate: InterpolateFunction = (v, scopedVars) => { return sceneGraph.interpolate(panel, v, scopedVars); }; const linkSupplier = getScenePanelLinksSupplier(panel, interpolate); if (!linkSupplier) { return []; } const panelLinks = linkSupplier.getLinks(interpolate); return panelLinks.map((panelLink) => ({ ...panelLink, onClick: (e: any, origin: any) => { DashboardInteractions.panelLinkClicked({ has_multiple_links: panelLinks.length > 1 }); panelLink.onClick?.(e, origin); }, })); } function createExtensionContext(panel: VizPanel, dashboard: DashboardScene): PluginExtensionPanelContext { const timeRange = sceneGraph.getTimeRange(panel); let queryRunner = getQueryRunnerFor(panel); const targets: DataQuery[] = queryRunner?.state.queries as DataQuery[]; const id = getPanelIdForVizPanel(panel); 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, }; } export function removePanel(dashboard: DashboardScene, panel: VizPanel, ask: boolean) { const vizPanelData = sceneGraph.getData(panel); let panelHasAlert = false; if (vizPanelData.state.data?.alertState) { panelHasAlert = true; } if (ask !== false) { const text2 = panelHasAlert && !config.unifiedAlertingEnabled ? 'Panel includes an alert rule. removing the panel will also remove the alert rule' : undefined; const confirmText = panelHasAlert ? 'YES' : undefined; appEvents.publish( new ShowConfirmModalEvent({ title: 'Remove panel', text: 'Are you sure you want to remove this panel?', text2: text2, icon: 'trash-alt', confirmText: confirmText, yesText: 'Remove', onConfirm: () => removePanel(dashboard, panel, false), }) ); return; } const panels: SceneObject[] = []; dashboard.state.body.forEachChild((child: SceneObject) => { if (child.state.key !== panel.parent?.state.key) { panels.push(child); } }); const layout = dashboard.state.body; if (layout instanceof SceneGridLayout || SceneFlexLayout) { layout.setState({ children: panels, }); } } const onCreateAlert = async (panel: VizPanel) => { DashboardInteractions.panelMenuItemClicked('create-alert'); const formValues = await scenesPanelToRuleFormValues(panel); const ruleFormUrl = urlUtil.renderUrl('/alerting/new', { defaults: JSON.stringify(formValues), returnTo: location.pathname + location.search, }); locationService.push(ruleFormUrl); DashboardInteractions.panelMenuItemClicked('create-alert'); }; export function toggleVizPanelLegend(vizPanel: VizPanel): void { const options = vizPanel.state.options; if (hasLegendOptions(options) && typeof options.legend.showLegend === 'boolean') { vizPanel.onOptionsChange({ legend: { showLegend: options.legend.showLegend ? false : true, }, }); } DashboardInteractions.panelMenuItemClicked('toggleLegend'); } function hasLegendOptions(optionsWithLegend: unknown): optionsWithLegend is OptionsWithLegend { return optionsWithLegend != null && typeof optionsWithLegend === 'object' && 'legend' in optionsWithLegend; } const onInspectPanel = (vizPanel: VizPanel, tab?: InspectTab) => { locationService.partial({ inspect: vizPanel.state.key, inspectTab: tab, }); DashboardInteractions.panelMenuInspectClicked(tab ?? InspectTab.Data); };
Edit
Rename
Chmod
Delete
FILE
FOLDER
INFO
Name
Size
Permission
Action
AlertStatesDataLayer.ts
6986 bytes
0644
DashboardAnnotationsDataLayer.test.ts
2000 bytes
0644
DashboardAnnotationsDataLayer.ts
1413 bytes
0644
DashboardControls.tsx
1283 bytes
0644
DashboardLinksControls.tsx
2088 bytes
0644
DashboardMacro.ts
966 bytes
0644
DashboardScene.test.tsx
9118 bytes
0644
DashboardScene.tsx
17603 bytes
0644
DashboardSceneRenderer.tsx
3280 bytes
0644
DashboardSceneUrlSync.test.ts
2767 bytes
0644
DashboardSceneUrlSync.ts
5530 bytes
0644
GoToSnapshotOriginButton.test.tsx
2303 bytes
0644
GoToSnapshotOriginButton.tsx
2007 bytes
0644
LibraryVizPanel.tsx
2245 bytes
0644
NavToolbarActions.test.tsx
2532 bytes
0644
NavToolbarActions.tsx
9842 bytes
0644
PanelLinks.tsx
1943 bytes
0644
PanelMenuBehavior.test.tsx
16261 bytes
0644
PanelMenuBehavior.tsx
14341 bytes
0644
PanelNotices.tsx
1237 bytes
0644
PanelRepeaterGridItem.test.tsx
3838 bytes
0644
PanelRepeaterGridItem.tsx
6699 bytes
0644
PanelTimeRange.test.tsx
2053 bytes
0644
PanelTimeRange.tsx
4071 bytes
0644
RowRepeaterBehavior.test.tsx
4316 bytes
0644
RowRepeaterBehavior.ts
5990 bytes
0644
ViewPanelScene.test.tsx
1875 bytes
0644
ViewPanelScene.tsx
2411 bytes
0644
keyboardShortcuts.ts
4630 bytes
0644
setDashboardPanelContext.test.ts
7884 bytes
0644
setDashboardPanelContext.ts
5957 bytes
0644
types.ts
353 bytes
0644
N4ST4R_ID | Naxtarrr