mirror of https://github.com/grafana/grafana.git
Dashboards: Add support for full screen panel view and embedded (solo panel) route to repeated panels and new layouts (via new SoloPanelContex) (#107375)
* SoloPanelContext: First steps * working in default grid * Fixes * Update * SoloPanelPage * Fixing search layout * Fixes * Fixes * Update * remove comments * Solving panel not found * Fix * fix lint * fix lint
This commit is contained in:
parent
7434a9d725
commit
da5209be1e
|
@ -70,9 +70,8 @@ export function PublicDashboardScenePage({ route }: Props) {
|
|||
|
||||
function PublicDashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const { controls, title } = model.useState();
|
||||
const { controls, title, body } = model.useState();
|
||||
const { timePicker, refreshPicker, hideTimeControls } = controls!.useState();
|
||||
const bodyToRender = model.getBodyToRender();
|
||||
const styles = useStyles2(getStyles);
|
||||
const conf = useGetPublicDashboardConfig();
|
||||
|
||||
|
@ -108,7 +107,7 @@ function PublicDashboardSceneRenderer({ model }: SceneComponentProps<DashboardSc
|
|||
)}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<bodyToRender.Component model={bodyToRender} />
|
||||
<body.Component model={body} />
|
||||
</div>
|
||||
<PublicDashboardFooter />
|
||||
</Page>
|
||||
|
|
|
@ -65,7 +65,6 @@ import { isRepeatCloneOrChildOf } from '../utils/clone';
|
|||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { djb2Hash } from '../utils/djb2Hash';
|
||||
import { getDashboardUrl } from '../utils/getDashboardUrl';
|
||||
import { getViewPanelUrl } from '../utils/urlBuilders';
|
||||
import {
|
||||
getClosestVizPanel,
|
||||
getDashboardSceneFor,
|
||||
|
@ -81,7 +80,6 @@ import { DashboardLayoutOrchestrator } from './DashboardLayoutOrchestrator';
|
|||
import { DashboardSceneRenderer } from './DashboardSceneRenderer';
|
||||
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
import { ViewPanelScene } from './ViewPanelScene';
|
||||
import { setupKeyboardShortcuts } from './keyboardShortcuts';
|
||||
import { AutoGridItem } from './layout-auto-grid/AutoGridItem';
|
||||
import { DashboardGridItem } from './layout-default/DashboardGridItem';
|
||||
|
@ -129,8 +127,10 @@ export interface DashboardSceneState extends SceneObjectState {
|
|||
meta: Omit<DashboardMeta, 'isNew'>;
|
||||
/** Version of the dashboard */
|
||||
version?: number;
|
||||
/** Panel to view in fullscreen */
|
||||
viewPanelScene?: ViewPanelScene;
|
||||
/** Panel to inspect */
|
||||
inspectPanelKey?: string;
|
||||
/** Panel key to view in fullscreen */
|
||||
viewPanel?: string;
|
||||
/** Edit view */
|
||||
editview?: DashboardEditView;
|
||||
/** Edit panel */
|
||||
|
@ -427,7 +427,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
|||
}
|
||||
|
||||
public getPageNav(location: H.Location, navIndex: NavIndex) {
|
||||
const { meta, viewPanelScene, editPanel, title, uid } = this.state;
|
||||
const { meta, viewPanel, editPanel, title, uid } = this.state;
|
||||
const isNew = !Boolean(uid);
|
||||
|
||||
let pageNav: NavModelItem = {
|
||||
|
@ -456,11 +456,14 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
|||
}
|
||||
}
|
||||
|
||||
if (viewPanelScene) {
|
||||
if (viewPanel) {
|
||||
pageNav = {
|
||||
text: t('dashboard-scene.dashboard-scene.text.view-panel', 'View panel'),
|
||||
parentItem: pageNav,
|
||||
url: getViewPanelUrl(viewPanelScene.state.panelRef.resolve()),
|
||||
url: locationUtil.getUrlForPartial(locationService.getLocation(), {
|
||||
viewPanel: viewPanel,
|
||||
editPanel: undefined,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -474,13 +477,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
|||
return pageNav;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body (layout) or the full view panel
|
||||
*/
|
||||
public getBodyToRender(): SceneObject {
|
||||
return this.state.viewPanelScene ?? this.state.body;
|
||||
}
|
||||
|
||||
public getInitialState(): DashboardSceneState | undefined {
|
||||
return this._initialState;
|
||||
}
|
||||
|
|
|
@ -12,14 +12,16 @@ import { DashboardEditPaneSplitter } from '../edit-pane/DashboardEditPaneSplitte
|
|||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { PanelSearchLayout } from './PanelSearchLayout';
|
||||
import { SoloPanelContextProvider, useDefineSoloPanelContext } from './SoloPanelContext';
|
||||
|
||||
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
||||
const {
|
||||
controls,
|
||||
overlay,
|
||||
editview,
|
||||
body,
|
||||
editPanel,
|
||||
viewPanelScene,
|
||||
viewPanel,
|
||||
panelSearch,
|
||||
panelsPerRow,
|
||||
isEditing,
|
||||
|
@ -30,23 +32,23 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
|||
const scopesContext = useContext(ScopesContext);
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
const pageNav = model.getPageNav(location, navIndex);
|
||||
const bodyToRender = model.getBodyToRender();
|
||||
const navModel = getNavModel(navIndex, `dashboards/${type === 'snapshot' ? 'snapshots' : 'browse'}`);
|
||||
const isSettingsOpen = editview !== undefined;
|
||||
const soloPanelContext = useDefineSoloPanelContext(viewPanel);
|
||||
|
||||
// Remember scroll pos when going into view panel, edit panel or settings
|
||||
useMemo(() => {
|
||||
if (viewPanelScene || isSettingsOpen || editPanel) {
|
||||
if (viewPanel || isSettingsOpen || editPanel) {
|
||||
model.rememberScrollPos();
|
||||
}
|
||||
}, [isSettingsOpen, editPanel, viewPanelScene, model]);
|
||||
}, [isSettingsOpen, editPanel, viewPanel, model]);
|
||||
|
||||
// Restore scroll pos when coming back
|
||||
useEffect(() => {
|
||||
if (!viewPanelScene && !isSettingsOpen && !editPanel) {
|
||||
if (!viewPanel && !isSettingsOpen && !editPanel) {
|
||||
model.restoreScrollPos();
|
||||
}
|
||||
}, [isSettingsOpen, editPanel, viewPanelScene, model]);
|
||||
}, [isSettingsOpen, editPanel, viewPanel, model]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scopesContext && isEditing) {
|
||||
|
@ -70,11 +72,19 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
|||
}
|
||||
|
||||
function renderBody() {
|
||||
if (!viewPanelScene && (panelSearch || panelsPerRow)) {
|
||||
if (!viewPanel && (panelSearch || panelsPerRow)) {
|
||||
return <PanelSearchLayout panelSearch={panelSearch} panelsPerRow={panelsPerRow} dashboard={model} />;
|
||||
}
|
||||
|
||||
return <bodyToRender.Component model={bodyToRender} />;
|
||||
if (soloPanelContext) {
|
||||
return (
|
||||
<SoloPanelContextProvider value={soloPanelContext} singleMatch={true} dashboard={model}>
|
||||
<body.Component model={body} />
|
||||
</SoloPanelContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <body.Component model={body} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,21 +1,11 @@
|
|||
import { AppEvents } from '@grafana/data';
|
||||
import { LocalValueVariable, SceneQueryRunner, SceneVariableSet, VizPanel } from '@grafana/scenes';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||
import { KioskMode } from 'app/types/dashboard';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { DashboardGridItem } from './layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
|
||||
import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent';
|
||||
|
||||
describe('DashboardSceneUrlSync', () => {
|
||||
describe('Given a standard scene', () => {
|
||||
it('Should set viewPanelKey when url has viewPanel', () => {
|
||||
const scene = buildTestScene();
|
||||
scene.urlSync?.updateFromUrl({ viewPanel: '2' });
|
||||
expect(scene.state.viewPanelScene!.getUrlKey()).toBe('panel-2');
|
||||
});
|
||||
|
||||
it('Should set UNSAFE_fitPanels when url has autofitpanels', () => {
|
||||
const scene = buildTestScene();
|
||||
scene.urlSync?.updateFromUrl({ autofitpanels: '' });
|
||||
|
@ -58,54 +48,11 @@ describe('DashboardSceneUrlSync', () => {
|
|||
const scene = buildTestScene();
|
||||
scene.setState({ isEditing: false });
|
||||
scene.urlSync?.updateFromUrl({ viewPanel: 'panel-1' });
|
||||
expect(scene.state.viewPanelScene).toBeDefined();
|
||||
expect(scene.state.viewPanel).toBeDefined();
|
||||
scene.urlSync?.updateFromUrl({ editPanel: 'panel-1' });
|
||||
expect(scene.state.editPanel).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Given a viewPanelKey with clone that is not found', () => {
|
||||
const scene = buildTestScene();
|
||||
|
||||
let errorNotice = 0;
|
||||
appEvents.on(AppEvents.alertError, (evt) => errorNotice++);
|
||||
|
||||
scene.urlSync?.updateFromUrl({ viewPanel: 'A$panel-1' });
|
||||
|
||||
expect(scene.state.viewPanelScene).toBeUndefined();
|
||||
// Verify no error notice was shown
|
||||
expect(errorNotice).toBe(0);
|
||||
|
||||
// fake adding clone panel
|
||||
const layout = scene.state.body as DefaultGridLayoutManager;
|
||||
|
||||
layout.state.grid.setState({
|
||||
children: [
|
||||
new DashboardGridItem({
|
||||
key: 'griditem-1',
|
||||
x: 0,
|
||||
body: new VizPanel({
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [
|
||||
new LocalValueVariable({
|
||||
name: 'server',
|
||||
value: 'A',
|
||||
text: 'A',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
title: 'Clone Panel A',
|
||||
key: 'panel-1',
|
||||
pluginId: 'table',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Verify it subscribes to DashboardRepeatsProcessedEvent
|
||||
scene.publishEvent(new DashboardRepeatsProcessedEvent({ source: scene }));
|
||||
expect(scene.state.viewPanelScene?.getUrlKey()).toBe('A$panel-1');
|
||||
});
|
||||
});
|
||||
|
||||
function buildTestScene() {
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { KioskMode } from 'app/types/dashboard';
|
||||
|
||||
|
@ -11,18 +7,13 @@ import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
|||
import { createDashboardEditViewFor } from '../settings/utils';
|
||||
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { containsPathIdSeparator, findVizPanelByPathId } from '../utils/pathId';
|
||||
import { findEditPanel, getLibraryPanelBehavior } from '../utils/utils';
|
||||
|
||||
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
import { ViewPanelScene } from './ViewPanelScene';
|
||||
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
|
||||
import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent';
|
||||
|
||||
export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
private _viewEventSub?: Unsubscribable;
|
||||
|
||||
constructor(private _scene: DashboardScene) {}
|
||||
|
||||
getKeys(): string[] {
|
||||
|
@ -34,7 +25,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
|||
|
||||
return {
|
||||
autofitpanels: this.getAutoFitPanels(),
|
||||
viewPanel: state.viewPanelScene?.getUrlKey(),
|
||||
viewPanel: state.viewPanel,
|
||||
editview: state.editview?.getUrlKey(),
|
||||
editPanel: state.editPanel?.getUrlKey() || undefined,
|
||||
kiosk: state.kioskMode === KioskMode.Full ? '' : undefined,
|
||||
|
@ -52,7 +43,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
|||
}
|
||||
|
||||
updateFromUrl(values: SceneObjectUrlValues): void {
|
||||
const { viewPanelScene, isEditing, editPanel, shareView } = this._scene.state;
|
||||
const { viewPanel, isEditing, editPanel, shareView } = this._scene.state;
|
||||
const update: Partial<DashboardSceneState> = {};
|
||||
|
||||
if (typeof values.editview === 'string' && this._scene.canEditDashboard()) {
|
||||
|
@ -74,25 +65,9 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
|||
|
||||
// Handle view panel state
|
||||
if (typeof values.viewPanel === 'string') {
|
||||
const panel = findVizPanelByPathId(this._scene, values.viewPanel);
|
||||
|
||||
if (!panel) {
|
||||
// If we are trying to view a repeat clone that can't be found it might be that the repeats have not been processed yet
|
||||
// Here we check if the key contains the clone key so we force the repeat processing
|
||||
// It doesn't matter if the element or the ancestors are clones or not, just that the key contains the clone key
|
||||
if (containsPathIdSeparator(values.viewPanel)) {
|
||||
this._handleViewRepeatClone(values.viewPanel);
|
||||
return;
|
||||
}
|
||||
|
||||
appEvents.emit(AppEvents.alertError, ['Panel not found']);
|
||||
locationService.partial({ viewPanel: null });
|
||||
return;
|
||||
}
|
||||
|
||||
update.viewPanelScene = new ViewPanelScene({ panelRef: panel.getRef() });
|
||||
} else if (viewPanelScene && values.viewPanel === null) {
|
||||
update.viewPanelScene = undefined;
|
||||
update.viewPanel = values.viewPanel;
|
||||
} else if (viewPanel && values.viewPanel === null) {
|
||||
update.viewPanel = undefined;
|
||||
}
|
||||
|
||||
// Handle edit panel state
|
||||
|
@ -105,8 +80,8 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
|||
}
|
||||
|
||||
// We cannot simultaneously be in edit and view panel state.
|
||||
if (this._scene.state.viewPanelScene) {
|
||||
this._scene.setState({ viewPanelScene: undefined });
|
||||
if (this._scene.state.viewPanel) {
|
||||
update.viewPanel = undefined;
|
||||
}
|
||||
|
||||
// If we are not in editing (for example after full page reload)
|
||||
|
@ -159,21 +134,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private _handleViewRepeatClone(viewPanel: string) {
|
||||
if (!this._viewEventSub) {
|
||||
this._viewEventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => {
|
||||
const panel = findVizPanelByPathId(this._scene, viewPanel);
|
||||
if (panel) {
|
||||
this._viewEventSub?.unsubscribe();
|
||||
this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: panel.getRef() }) });
|
||||
this._viewEventSub = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
this._scene.state.body.activateRepeaters?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary solution, with some refactoring of PanelEditor we can remove this
|
||||
*/
|
||||
|
|
|
@ -66,7 +66,7 @@ NavToolbarActions.displayName = 'NavToolbarActions';
|
|||
* This part is split into a separate component to help test this
|
||||
*/
|
||||
export function ToolbarActions({ dashboard }: Props) {
|
||||
const { isEditing, viewPanelScene, isDirty, uid, meta, editview, editPanel, editable } = dashboard.useState();
|
||||
const { isEditing, viewPanel, isDirty, uid, meta, editview, editPanel, editable } = dashboard.useState();
|
||||
|
||||
const { isPlaying } = playlistSrv.useState();
|
||||
const [isAddPanelMenuOpen, setIsAddPanelMenuOpen] = useState(false);
|
||||
|
@ -75,7 +75,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||
const toolbarActions: ToolbarAction[] = [];
|
||||
const styles = useStyles2(getStyles);
|
||||
const isEditingPanel = Boolean(editPanel);
|
||||
const isViewingPanel = Boolean(viewPanelScene);
|
||||
const isViewingPanel = Boolean(viewPanel);
|
||||
const isEditedPanelDirty = usePanelEditDirty(editPanel);
|
||||
|
||||
const isEditingLibraryPanel = editPanel && isLibraryPanel(editPanel.state.panelRef.resolve());
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
getTimeZone,
|
||||
InterpolateFunction,
|
||||
LinkModel,
|
||||
locationUtil,
|
||||
PanelMenuItem,
|
||||
PanelPlugin,
|
||||
PluginExtensionLink,
|
||||
|
@ -37,7 +38,7 @@ import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
|||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { isRepeatCloneOrChildOf } from '../utils/clone';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { getEditPanelUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
|
||||
import { getEditPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
|
@ -90,7 +91,10 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
|||
text: t('panel.header-menu.view', `View`),
|
||||
iconClassName: 'eye',
|
||||
shortcut: 'v',
|
||||
href: getViewPanelUrl(panel),
|
||||
href: locationUtil.getUrlForPartial(locationService.getLocation(), {
|
||||
viewPanel: panel.getPathId(),
|
||||
editPanel: undefined,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import { css } from '@emotion/css';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { SceneGridRow, VizPanel, sceneGraph } from '@grafana/scenes';
|
||||
import { VizPanel, sceneGraph } from '@grafana/scenes';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { forceActivateFullSceneObjectTree } from '../utils/utils';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { DashboardGridItem } from './layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
|
||||
import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent';
|
||||
import { SoloPanelContextProvider } from './SoloPanelContext';
|
||||
|
||||
export interface Props {
|
||||
dashboard: DashboardScene;
|
||||
|
@ -24,60 +19,21 @@ const panelsPerRowCSSVar = '--panels-per-row';
|
|||
|
||||
export function PanelSearchLayout({ dashboard, panelSearch = '', panelsPerRow }: Props) {
|
||||
const { body } = dashboard.state;
|
||||
const filteredPanels: VizPanel[] = [];
|
||||
const styles = useStyles2(getStyles);
|
||||
const [_, setRepeatsUpdated] = useState('');
|
||||
const soloPanelContext = useMemo(() => new SoloPanelContextValueWithSearchStringFilter(panelSearch), [panelSearch]);
|
||||
|
||||
const bodyGrid = body instanceof DefaultGridLayoutManager ? body.state.grid : null;
|
||||
|
||||
if (!bodyGrid) {
|
||||
return <Trans i18nKey="panel-search.unsupported-layout">Unsupported layout</Trans>;
|
||||
}
|
||||
|
||||
for (const gridItem of bodyGrid.state.children) {
|
||||
if (gridItem instanceof DashboardGridItem) {
|
||||
filterPanels(gridItem, dashboard, panelSearch, filteredPanels, setRepeatsUpdated);
|
||||
} else if (gridItem instanceof SceneGridRow) {
|
||||
for (const rowItem of gridItem.state.children) {
|
||||
if (rowItem instanceof DashboardGridItem) {
|
||||
filterPanels(rowItem, dashboard, panelSearch, filteredPanels, setRepeatsUpdated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredPanels.length > 0) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.grid, { [styles.perRow]: panelsPerRow !== undefined })}
|
||||
style={{ [panelsPerRowCSSVar]: panelsPerRow } as Record<string, number>}
|
||||
>
|
||||
{filteredPanels.map((panel) => (
|
||||
<PanelSearchHit key={panel.state.key} panel={panel} />
|
||||
))}
|
||||
<SoloPanelContextProvider value={soloPanelContext} singleMatch={false} dashboard={dashboard}>
|
||||
<body.Component model={body} />
|
||||
</SoloPanelContextProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className={styles.noHits}>
|
||||
<Trans i18nKey="panel-search.no-matches">No matches found</Trans>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function PanelSearchHit({ panel }: { panel: VizPanel }) {
|
||||
useEffect(() => {
|
||||
const deactivate = forceActivateFullSceneObjectTree(panel);
|
||||
|
||||
return () => {
|
||||
deactivate?.();
|
||||
};
|
||||
}, [panel]);
|
||||
|
||||
return <panel.Component model={panel} />;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
grid: css({
|
||||
|
@ -96,37 +52,20 @@ function getStyles(theme: GrafanaTheme2) {
|
|||
};
|
||||
}
|
||||
|
||||
function filterPanels(
|
||||
gridItem: DashboardGridItem,
|
||||
dashboard: DashboardScene,
|
||||
searchString: string,
|
||||
filteredPanels: VizPanel[],
|
||||
setRepeatsUpdated: (updated: string) => void
|
||||
) {
|
||||
const interpolatedSearchString = sceneGraph.interpolate(dashboard, searchString).toLowerCase();
|
||||
export class SoloPanelContextValueWithSearchStringFilter {
|
||||
public matchFound = false;
|
||||
|
||||
// activate inactive repeat panel if one of its children will be matched
|
||||
if (gridItem.state.variableName && !gridItem.isActive) {
|
||||
const panel = gridItem.state.body;
|
||||
public constructor(private searchQuery: string) {}
|
||||
|
||||
public matches(panel: VizPanel): boolean {
|
||||
const interpolatedSearchString = sceneGraph.interpolate(panel, this.searchQuery).toLowerCase();
|
||||
const interpolatedTitle = panel.interpolate(panel.state.title, undefined, 'text').toLowerCase();
|
||||
if (interpolatedTitle.includes(interpolatedSearchString)) {
|
||||
gridItem.subscribeToEvent(DashboardRepeatsProcessedEvent, (event) => {
|
||||
const source = event.payload.source;
|
||||
if (source instanceof DashboardGridItem) {
|
||||
setRepeatsUpdated(event.payload.source.state.key ?? '');
|
||||
}
|
||||
});
|
||||
gridItem.activate();
|
||||
}
|
||||
|
||||
const match = interpolatedTitle.includes(interpolatedSearchString);
|
||||
if (match) {
|
||||
this.matchFound = true;
|
||||
}
|
||||
|
||||
const panels = gridItem.state.repeatedPanels ?? [gridItem.state.body];
|
||||
for (const panel of panels) {
|
||||
const interpolatedTitle = panel.interpolate(panel.state.title, undefined, 'text').toLowerCase();
|
||||
if (interpolatedTitle.includes(interpolatedSearchString)) {
|
||||
filteredPanels.push(panel);
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPanels;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { Box, Spinner } from '@grafana/ui';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
|
||||
export interface SoloPanelContextValue {
|
||||
matches: (VizPanel: VizPanel) => boolean;
|
||||
matchFound: boolean;
|
||||
}
|
||||
|
||||
export class SoloPanelContextWithPathIdFilter implements SoloPanelContextValue {
|
||||
public matchFound = false;
|
||||
|
||||
public constructor(public keyPath: string) {}
|
||||
|
||||
public matches(panel: VizPanel): boolean {
|
||||
// Check if keyPath is just an old legacy panel id
|
||||
if (/^\d+$/.test(this.keyPath)) {
|
||||
if (`panel-${this.keyPath}` === panel.state.key!) {
|
||||
this.matchFound = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.keyPath === panel.getPathId()) {
|
||||
this.matchFound = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const SoloPanelContext = React.createContext<SoloPanelContextValue | null>(null);
|
||||
|
||||
export function useDefineSoloPanelContext(keyPath?: string): SoloPanelContextValue | null {
|
||||
return React.useMemo(() => {
|
||||
if (!keyPath) {
|
||||
return null;
|
||||
}
|
||||
return new SoloPanelContextWithPathIdFilter(keyPath);
|
||||
}, [keyPath]);
|
||||
}
|
||||
|
||||
export function useSoloPanelContext() {
|
||||
return useContext(SoloPanelContext);
|
||||
}
|
||||
|
||||
export function renderMatchingSoloPanels(soloPanelContext: SoloPanelContextValue, panels: VizPanel[]) {
|
||||
const matches: React.ReactNode[] = [];
|
||||
for (const panel of panels) {
|
||||
if (soloPanelContext.matches(panel)) {
|
||||
matches.push(<panel.Component model={panel} key={panel.state.key} />);
|
||||
}
|
||||
}
|
||||
|
||||
return <>{matches}</>;
|
||||
}
|
||||
|
||||
export function SoloPanelContextProvider({
|
||||
children,
|
||||
value,
|
||||
singleMatch,
|
||||
dashboard,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: SoloPanelContextValue;
|
||||
singleMatch: boolean;
|
||||
dashboard: DashboardScene;
|
||||
}) {
|
||||
return (
|
||||
<SoloPanelContext.Provider value={value}>
|
||||
{children}
|
||||
<SoloPanelNotFound singleMatch={singleMatch} dashboard={dashboard} />
|
||||
</SoloPanelContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SoloPanelNotFoundProps {
|
||||
/**
|
||||
* Controls panel not found error message
|
||||
*/
|
||||
singleMatch: boolean;
|
||||
/**
|
||||
* Used to check if variables are loading
|
||||
*/
|
||||
dashboard: DashboardScene;
|
||||
}
|
||||
|
||||
export function SoloPanelNotFound({ singleMatch, dashboard }: SoloPanelNotFoundProps) {
|
||||
const context = useSoloPanelContext()!;
|
||||
const [state, setState] = useState({ matchFound: false, isLoading: true });
|
||||
|
||||
useEffect(() => {
|
||||
// This effect fires before any child layout starts rendering and checking if their panels match the solo panel filter
|
||||
// We need this polling here to check if any solo panel has matched or if any layout has marked the context as loading (for repeated panels)
|
||||
const cancelTimeout = setInterval(() => {
|
||||
setState({ matchFound: context.matchFound, isLoading: isAnyVariableLoading(dashboard) });
|
||||
}, 500);
|
||||
|
||||
return () => clearInterval(cancelTimeout);
|
||||
}, [context, dashboard]);
|
||||
|
||||
if (state.matchFound || context.matchFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (state.isLoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
backgroundColor={'primary'}
|
||||
borderColor={'weak'}
|
||||
borderStyle={'solid'}
|
||||
padding={2}
|
||||
borderRadius={'default'}
|
||||
display={'flex'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
>
|
||||
{singleMatch && <Trans i18nKey="dashboard.view-panel.not-found">Panel not found</Trans>}
|
||||
{!singleMatch && <Trans i18nKey="dashboard.search-panel.no-match">No panels matching</Trans>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function isAnyVariableLoading(scene: DashboardScene) {
|
||||
const variables = scene.state.$variables;
|
||||
if (!variables || !variables.isActive) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return variables.state.variables.some((variable) => variable.state.loading);
|
||||
}
|
|
@ -13,7 +13,7 @@ import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
|||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { findVizPanelByPathId } from '../utils/pathId';
|
||||
import { getEditPanelUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
|
||||
import { getEditPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
|
||||
import { getPanelIdForVizPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
|
@ -50,15 +50,10 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
|
|||
keybindings.addBinding({
|
||||
key: 'v',
|
||||
onTrigger: withFocusedPanel(scene, (vizPanel: VizPanel) => {
|
||||
if (scene.state.viewPanelScene) {
|
||||
locationService.push(
|
||||
locationUtil.getUrlForPartial(locationService.getLocation(), {
|
||||
viewPanel: undefined,
|
||||
})
|
||||
);
|
||||
if (scene.state.viewPanel) {
|
||||
locationService.partial({ viewPanel: undefined });
|
||||
} else {
|
||||
const url = locationUtil.stripBaseFromUrl(getViewPanelUrl(vizPanel));
|
||||
locationService.push(url);
|
||||
locationService.partial({ viewPanel: vizPanel.getPathId(), editPanel: undefined });
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -18,7 +18,6 @@ import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
|
|||
import { getMultiVariableValues } from '../../utils/utils';
|
||||
import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
|
||||
import { DashboardLayoutItem } from '../types/DashboardLayoutItem';
|
||||
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
|
||||
|
||||
import { getOptions } from './AutoGridItemEditor';
|
||||
import { AutoGridItemRenderer } from './AutoGridItemRenderer';
|
||||
|
@ -130,8 +129,6 @@ export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements
|
|||
|
||||
this.setState({ repeatedPanels });
|
||||
this._prevRepeatValues = values;
|
||||
|
||||
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
|
||||
}
|
||||
|
||||
public getPanelCount() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { useStyles2 } from '@grafana/ui';
|
|||
|
||||
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
|
||||
import { useDashboardState } from '../../utils/utils';
|
||||
import { renderMatchingSoloPanels, useSoloPanelContext } from '../SoloPanelContext';
|
||||
import { getIsLazy } from '../layouts-shared/utils';
|
||||
|
||||
import { AutoGridItem } from './AutoGridItem';
|
||||
|
@ -19,7 +20,7 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
|
|||
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] =
|
||||
useIsConditionallyHidden(model);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const soloPanelContext = useSoloPanelContext();
|
||||
const isLazy = useMemo(() => getIsLazy(preload), [preload]);
|
||||
|
||||
const Wrapper = useMemo(
|
||||
|
@ -77,6 +78,10 @@ export function AutoGridItemRenderer({ model }: SceneComponentProps<AutoGridItem
|
|||
[conditionalRenderingClass, conditionalRenderingOverlay, isLazy, key, model.containerRef, styles]
|
||||
);
|
||||
|
||||
if (soloPanelContext) {
|
||||
return renderMatchingSoloPanels(soloPanelContext, [body, ...repeatedPanels]);
|
||||
}
|
||||
|
||||
if (isConditionallyHidden && !isEditing) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useStyles2 } from '@grafana/ui';
|
|||
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { useDashboardState } from '../../utils/utils';
|
||||
import { useSoloPanelContext } from '../SoloPanelContext';
|
||||
import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions';
|
||||
import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles';
|
||||
|
||||
|
@ -18,6 +19,7 @@ export function AutoGridLayoutRenderer({ model }: SceneComponentProps<AutoGridLa
|
|||
const { layoutOrchestrator, isEditing } = useDashboardState(model);
|
||||
const layoutManager = sceneGraph.getAncestor(model, AutoGridLayoutManager);
|
||||
const { fillScreen } = layoutManager.useState();
|
||||
const soloPanelContext = useSoloPanelContext();
|
||||
|
||||
if (isHidden || !layoutOrchestrator) {
|
||||
return null;
|
||||
|
@ -25,6 +27,10 @@ export function AutoGridLayoutRenderer({ model }: SceneComponentProps<AutoGridLa
|
|||
|
||||
const showCanvasActions = !isRepeatCloneOrChildOf(model) && isEditing;
|
||||
|
||||
if (soloPanelContext) {
|
||||
return children.map((item) => <item.Component key={item.state.key} model={item} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.container, fillScreen && styles.containerFillScreen, isEditing && styles.containerEditing)}
|
||||
|
|
|
@ -20,7 +20,6 @@ import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
|
|||
import { getMultiVariableValues } from '../../utils/utils';
|
||||
import { scrollCanvasElementIntoView, scrollIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
|
||||
import { DashboardLayoutItem } from '../types/DashboardLayoutItem';
|
||||
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
|
||||
|
||||
import { getDashboardGridItemOptions } from './DashboardGridItemEditor';
|
||||
import { DashboardGridItemRenderer } from './DashboardGridItemRenderer';
|
||||
|
@ -209,8 +208,6 @@ export class DashboardGridItem
|
|||
}
|
||||
|
||||
this._prevRepeatValues = values;
|
||||
|
||||
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
|
||||
}
|
||||
|
||||
public handleVariableName() {
|
||||
|
|
|
@ -5,10 +5,13 @@ import { config } from '@grafana/runtime';
|
|||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
|
||||
|
||||
import { renderMatchingSoloPanels, useSoloPanelContext } from '../SoloPanelContext';
|
||||
|
||||
import { DashboardGridItem, RepeatDirection } from './DashboardGridItem';
|
||||
|
||||
export function DashboardGridItemRenderer({ model }: SceneComponentProps<DashboardGridItem>) {
|
||||
const { repeatedPanels = [], itemHeight, variableName, body } = model.useState();
|
||||
const soloPanelContext = useSoloPanelContext();
|
||||
const layoutStyle = useLayoutStyle(
|
||||
model.getRepeatDirection(),
|
||||
model.getPanelCount(),
|
||||
|
@ -16,6 +19,10 @@ export function DashboardGridItemRenderer({ model }: SceneComponentProps<Dashboa
|
|||
itemHeight ?? 10
|
||||
);
|
||||
|
||||
if (soloPanelContext) {
|
||||
return renderMatchingSoloPanels(soloPanelContext, [body, ...repeatedPanels]);
|
||||
}
|
||||
|
||||
if (!variableName) {
|
||||
return (
|
||||
<div className={panelWrapper} ref={model.containerRef}>
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
getLayoutOrchestratorFor,
|
||||
getDashboardSceneFor,
|
||||
} from '../../utils/utils';
|
||||
import { useSoloPanelContext } from '../SoloPanelContext';
|
||||
import { AutoGridItem } from '../layout-auto-grid/AutoGridItem';
|
||||
import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions';
|
||||
import { clearClipboard, getDashboardGridItemFromClipboard } from '../layouts-shared/paste';
|
||||
|
@ -565,6 +566,11 @@ function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps<Default
|
|||
const hasClonedParents = isRepeatCloneOrChildOf(model);
|
||||
const styles = useStyles2(getStyles);
|
||||
const showCanvasActions = isEditing && config.featureToggles.dashboardNewLayouts && !hasClonedParents;
|
||||
const soloPanelContext = useSoloPanelContext();
|
||||
|
||||
if (soloPanelContext) {
|
||||
return children.map((child) => <child.Component model={child} key={child.state.key!} />);
|
||||
}
|
||||
|
||||
// If we are top level layout and we have no children, show empty state
|
||||
if (model.parent === dashboard && children.length === 0) {
|
||||
|
@ -585,6 +591,20 @@ function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps<Default
|
|||
);
|
||||
}
|
||||
|
||||
const OriginalSceneGridRowRenderer = SceneGridRow.Component;
|
||||
// @ts-expect-error
|
||||
SceneGridRow.Component = SceneGridRowRenderer;
|
||||
|
||||
function SceneGridRowRenderer({ model }: SceneComponentProps<SceneGridRow>) {
|
||||
const soloPanelContext = useSoloPanelContext();
|
||||
|
||||
if (soloPanelContext) {
|
||||
return model.state.children.map((child) => <child.Component model={child} key={child.state.key!} />);
|
||||
}
|
||||
|
||||
return <OriginalSceneGridRowRenderer model={model} />;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
container: css({
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
|
||||
import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
|
||||
import { getMultiVariableValues } from '../../utils/utils';
|
||||
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
|
||||
|
||||
interface RowRepeaterBehaviorState extends SceneObjectState {
|
||||
variableName: string;
|
||||
|
@ -172,9 +171,6 @@ export class RowRepeaterBehavior extends SceneObjectBase<RowRepeaterBehaviorStat
|
|||
}
|
||||
|
||||
updateLayout(layout, this._clonedRows, maxYOfRows, rowToRepeat.state.key!);
|
||||
|
||||
// Used from dashboard url sync
|
||||
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
|
||||
}
|
||||
|
||||
public removeBehavior() {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useIsConditionallyHidden } from '../../conditional-rendering/useIsCondi
|
|||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { useDashboardState, useInterpolatedTitle } from '../../utils/utils';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
import { useSoloPanelContext } from '../SoloPanelContext';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
|
||||
|
@ -28,6 +29,7 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
|||
const clearStyles = useStyles2(clearButtonStyles);
|
||||
const isTopLevel = model.parent?.parent instanceof DashboardScene;
|
||||
const pointerDistance = usePointerDistance();
|
||||
const soloPanelContext = useSoloPanelContext();
|
||||
|
||||
const myIndex = rows.findIndex((row) => row === model);
|
||||
|
||||
|
@ -45,6 +47,10 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (soloPanelContext) {
|
||||
return <layout.Component model={layout} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable key={key!} draggableId={key!} index={myIndex} isDragDisabled={!isDraggable}>
|
||||
{(dragProvided, dragSnapshot) => (
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Spinner } from '@grafana/ui';
|
|||
import { DashboardStateChangedEvent } from '../../edit-pane/shared';
|
||||
import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
|
||||
import { dashboardLog, getMultiVariableValues } from '../../utils/utils';
|
||||
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
import { RowsLayoutManager } from './RowsLayoutManager';
|
||||
|
@ -118,7 +117,6 @@ export function performRowRepeats(variable: MultiValueVariable, row: RowItem, co
|
|||
}
|
||||
|
||||
row.setState({ repeatedRows: clonedRows });
|
||||
row.publishEvent(new DashboardRepeatsProcessedEvent({ source: row }), true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Button, useStyles2 } from '@grafana/ui';
|
|||
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { useDashboardState } from '../../utils/utils';
|
||||
import { useSoloPanelContext } from '../SoloPanelContext';
|
||||
import { useClipboardState } from '../layouts-shared/useClipboardState';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
|
@ -20,6 +21,11 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps<RowsLayo
|
|||
const { isEditing } = useDashboardState(model);
|
||||
const styles = useStyles2(getStyles);
|
||||
const { hasCopiedRow } = useClipboardState();
|
||||
const soloPanelContext = useSoloPanelContext();
|
||||
|
||||
if (soloPanelContext) {
|
||||
return rows.map((row) => <RowWrapper row={row} manager={model} key={row.state.key!} />);
|
||||
}
|
||||
|
||||
const isClone = isRepeatCloneOrChildOf(model);
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import { Spinner, Tooltip, useStyles2 } from '@grafana/ui';
|
|||
import { DashboardStateChangedEvent } from '../../edit-pane/shared';
|
||||
import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
|
||||
import { dashboardLog, getMultiVariableValues } from '../../utils/utils';
|
||||
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
import { TabsLayoutManager } from './TabsLayoutManager';
|
||||
|
@ -99,7 +98,6 @@ export function performTabRepeats(variable: MultiValueVariable, tab: TabItem, co
|
|||
const clonedTabs = createTabRepeats({ values, texts, variable, tab });
|
||||
|
||||
tab.setState({ repeatedTabs: clonedTabs });
|
||||
tab.publishEvent(new DashboardRepeatsProcessedEvent({ source: tab }), true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Button, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
|||
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { useSoloPanelContext } from '../SoloPanelContext';
|
||||
import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles';
|
||||
import { useClipboardState } from '../layouts-shared/useClipboardState';
|
||||
|
||||
|
@ -26,6 +27,11 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
|
|||
const { isEditing } = dashboard.useState();
|
||||
const { hasCopiedTab } = useClipboardState();
|
||||
const [_, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden(currentTab);
|
||||
const soloPanelContext = useSoloPanelContext();
|
||||
|
||||
if (soloPanelContext) {
|
||||
return <layout.Component model={layout} />;
|
||||
}
|
||||
|
||||
const isClone = isRepeatCloneOrChildOf(model);
|
||||
|
||||
|
|
|
@ -10,10 +10,10 @@ import { StarButton } from './actions/StarButton';
|
|||
import { getDynamicActions, renderActionElements } from './utils';
|
||||
|
||||
export const LeftActions = ({ dashboard }: { dashboard: DashboardScene }) => {
|
||||
const { editview, editPanel, isEditing, uid, meta, viewPanelScene } = dashboard.useState();
|
||||
const { editview, editPanel, isEditing, uid, meta, viewPanel } = dashboard.useState();
|
||||
|
||||
const hasEditView = Boolean(editview);
|
||||
const isViewingPanel = Boolean(viewPanelScene);
|
||||
const isViewingPanel = Boolean(viewPanel);
|
||||
const isEditingDashboard = Boolean(isEditing);
|
||||
const isEditingPanel = Boolean(editPanel);
|
||||
const hasUid = Boolean(uid);
|
||||
|
|
|
@ -27,7 +27,7 @@ import { ToolbarActionProps } from './types';
|
|||
import { getDynamicActions, renderActionElements } from './utils';
|
||||
|
||||
export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
|
||||
const { editPanel, editable, editview, isEditing, uid, meta, viewPanelScene } = dashboard.useState();
|
||||
const { editPanel, editable, editview, isEditing, uid, meta, viewPanel } = dashboard.useState();
|
||||
const { isPlaying } = playlistSrv.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
|
@ -37,7 +37,7 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
|
|||
const isEditingDashboard = Boolean(isEditing);
|
||||
const hasEditView = Boolean(editview);
|
||||
const isEditingPanel = Boolean(editPanel);
|
||||
const isViewingPanel = Boolean(viewPanelScene);
|
||||
const isViewingPanel = Boolean(viewPanel);
|
||||
const isEditingLibraryPanel = isEditingPanel && isLibraryPanel(editPanel!.state.panelRef.resolve());
|
||||
const isShowingDashboard = !hasEditView && !isViewingPanel && !isEditingPanel;
|
||||
const isEditingAndShowingDashboard = isEditingDashboard && isShowingDashboard;
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import { BusEventWithPayload } from '@grafana/data';
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
|
||||
export interface DashboardRepeatsProcessedEventPayload {
|
||||
source: SceneObject;
|
||||
}
|
||||
|
||||
export class DashboardRepeatsProcessedEvent extends BusEventWithPayload<DashboardRepeatsProcessedEventPayload> {
|
||||
public static type = 'dashboard-repeats-processed';
|
||||
}
|
|
@ -4,9 +4,9 @@ import { useEffect } from 'react';
|
|||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { UrlSyncContextProvider } from '@grafana/scenes';
|
||||
import { Alert, Box, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Box, useStyles2 } from '@grafana/ui';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
@ -15,8 +15,7 @@ import { DashboardRoutes } from 'app/types/dashboard';
|
|||
|
||||
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
|
||||
import { useSoloPanel } from './useSoloPanel';
|
||||
import { SoloPanelContextProvider, useDefineSoloPanelContext } from '../scene/SoloPanelContext';
|
||||
|
||||
export interface Props extends GrafanaRouteComponentProps<DashboardPageRouteParams, { panelId: string }> {}
|
||||
|
||||
|
@ -61,30 +60,26 @@ export function SoloPanelPage({ queryParams }: Props) {
|
|||
export default SoloPanelPage;
|
||||
|
||||
export function SoloPanelRenderer({ dashboard, panelId }: { dashboard: DashboardScene; panelId: string }) {
|
||||
const [panel, error] = useSoloPanel(dashboard, panelId);
|
||||
const { controls } = dashboard.useState();
|
||||
const { controls, body } = dashboard.useState();
|
||||
const refreshPicker = controls?.useState()?.refreshPicker;
|
||||
const styles = useStyles2(getStyles);
|
||||
const soloPanelContext = useDefineSoloPanelContext(panelId)!;
|
||||
|
||||
useEffect(() => {
|
||||
return refreshPicker?.activate();
|
||||
}, [refreshPicker]);
|
||||
const dashDeactivate = dashboard.activate();
|
||||
const refreshDeactivate = refreshPicker?.activate();
|
||||
|
||||
if (error) {
|
||||
return <Alert title={error} />;
|
||||
}
|
||||
|
||||
if (!panel) {
|
||||
return (
|
||||
<span>
|
||||
<Trans i18nKey="dashboard-scene.solo-panel-page.loading">Loading</Trans> <Spinner />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
dashDeactivate();
|
||||
refreshDeactivate?.();
|
||||
};
|
||||
}, [dashboard, refreshPicker]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<panel.Component model={panel} />
|
||||
<SoloPanelContextProvider value={soloPanelContext} dashboard={dashboard} singleMatch={true}>
|
||||
<body.Component model={body} />
|
||||
</SoloPanelContextProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { findVizPanelByKey } from '../utils/utils';
|
||||
|
||||
import { useSoloPanel } from './useSoloPanel';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: () => ({
|
||||
get: async (ref: DataSourceRef) => {
|
||||
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
||||
return {
|
||||
id: 1,
|
||||
uid: '-- Grafana --',
|
||||
name: 'grafana',
|
||||
type: 'grafana',
|
||||
meta: {
|
||||
id: 'grafana',
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useSoloPanel', () => {
|
||||
it('should return undefined panel and error when panel is not found', () => {
|
||||
const { dashboard } = setup();
|
||||
const { result } = renderHook(() => useSoloPanel(dashboard, 'foo-key'));
|
||||
|
||||
expect(result.current[0]).toBeUndefined();
|
||||
expect(result.current[1]).toBe('Panel not found');
|
||||
});
|
||||
|
||||
it('should return the panel when panel is found', () => {
|
||||
const { dashboard } = setup();
|
||||
|
||||
const { result } = renderHook(() => useSoloPanel(dashboard, 'panel-1'));
|
||||
const panel = findVizPanelByKey(dashboard, 'panel-1');
|
||||
|
||||
expect(result.current[0]).toEqual(panel);
|
||||
expect(result.current[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the cloned panel when panel is found', () => {
|
||||
const { dashboard } = setup();
|
||||
const { result } = renderHook(() => useSoloPanel(dashboard, 'A$panel-1'));
|
||||
const panel = findVizPanelByKey(dashboard, 'panel-1');
|
||||
|
||||
expect(result.current[0]).not.toBe(panel);
|
||||
expect(result.current[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return error when panelId correspond to a non VizPanel', () => {
|
||||
const { dashboard } = setup();
|
||||
const { result } = renderHook(() => useSoloPanel(dashboard, 'panel-2'));
|
||||
|
||||
expect(result.current[0]).toBeUndefined();
|
||||
expect(result.current[1]).toBe('Panel not found');
|
||||
});
|
||||
});
|
||||
|
||||
const setup = () => {
|
||||
const dashboard = transformSaveModelToScene({ dashboard: TEST_DASHBOARD, meta: {} });
|
||||
|
||||
return { dashboard };
|
||||
};
|
||||
|
||||
const TEST_DASHBOARD = {
|
||||
title: 'Scenes/PanelEdit/Queries: Edit',
|
||||
annotations: {
|
||||
list: [],
|
||||
},
|
||||
editable: true,
|
||||
fiscalYearStartMonth: 0,
|
||||
graphTooltip: 0,
|
||||
id: 2378,
|
||||
links: [],
|
||||
liveNow: false,
|
||||
panels: [
|
||||
{
|
||||
type: 'timeseries',
|
||||
datasource: 'prometheus',
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: {},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
gridPos: {
|
||||
h: 9,
|
||||
w: 24,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
id: 1,
|
||||
options: {
|
||||
colorMode: 'background',
|
||||
graphMode: 'area',
|
||||
justifyMode: 'auto',
|
||||
orientation: 'auto',
|
||||
reduceOptions: {
|
||||
calcs: ['lastNotNull', 'last', 'first', 'min', 'max', 'mean', 'sum', 'count'],
|
||||
fields: '',
|
||||
values: false,
|
||||
},
|
||||
text: {},
|
||||
},
|
||||
pluginVersion: '8.0.3',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'row',
|
||||
},
|
||||
],
|
||||
refresh: '',
|
||||
schemaVersion: 39,
|
||||
tags: [],
|
||||
templating: {
|
||||
list: [],
|
||||
},
|
||||
time: {
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
},
|
||||
timepicker: {},
|
||||
timezone: '',
|
||||
uid: 'ffbe00e2-803c-4d49-adb7-41aad336234f',
|
||||
version: 6,
|
||||
weekStart: '',
|
||||
};
|
|
@ -1,70 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { VizPanel, UrlSyncManager } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { DashboardRepeatsProcessedEvent } from '../scene/types/DashboardRepeatsProcessedEvent';
|
||||
import { containsPathIdSeparator, findVizPanelByPathId } from '../utils/pathId';
|
||||
|
||||
export function useSoloPanel(dashboard: DashboardScene, pathId: string): [VizPanel | undefined, string | undefined] {
|
||||
const [panel, setPanel] = useState<VizPanel>();
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const urlSyncManager = new UrlSyncManager();
|
||||
urlSyncManager.initSync(dashboard);
|
||||
|
||||
const cleanUp = dashboard.activate();
|
||||
|
||||
let panel: VizPanel | null = null;
|
||||
try {
|
||||
panel = findVizPanelByPathId(dashboard, pathId);
|
||||
} catch (e) {
|
||||
// do nothing, just the panel is not found or not a VizPanel
|
||||
}
|
||||
|
||||
if (panel) {
|
||||
activateParents(panel);
|
||||
setPanel(panel);
|
||||
} else if (containsPathIdSeparator(pathId)) {
|
||||
findRepeatClone(dashboard, pathId).then((panel) => {
|
||||
if (panel) {
|
||||
setPanel(panel);
|
||||
} else {
|
||||
setError('Panel not found');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setError('Panel not found');
|
||||
}
|
||||
|
||||
return cleanUp;
|
||||
}, [dashboard, pathId]);
|
||||
|
||||
return [panel, error];
|
||||
}
|
||||
|
||||
function activateParents(panel: VizPanel) {
|
||||
let parent = panel.parent;
|
||||
|
||||
while (parent && !parent.isActive) {
|
||||
parent.activate();
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
|
||||
function findRepeatClone(dashboard: DashboardScene, pathId: string): Promise<VizPanel | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
dashboard.subscribeToEvent(DashboardRepeatsProcessedEvent, () => {
|
||||
const panel = findVizPanelByPathId(dashboard, pathId);
|
||||
if (panel) {
|
||||
resolve(panel);
|
||||
} else {
|
||||
// If rows are repeated they could add new panel repeaters that needs to be activated
|
||||
dashboard.state.body.activateRepeaters?.();
|
||||
}
|
||||
});
|
||||
|
||||
dashboard.state.body.activateRepeaters?.();
|
||||
});
|
||||
}
|
|
@ -5223,6 +5223,9 @@
|
|||
"save-json-to-file": "Save JSON to file",
|
||||
"see-docs": "See <2>documentation</2> for more information about provisioning."
|
||||
},
|
||||
"search-panel": {
|
||||
"no-match": "No panels matching"
|
||||
},
|
||||
"share-public-dashboard-loader": {
|
||||
"loading-configuration": "Loading configuration"
|
||||
},
|
||||
|
@ -5495,6 +5498,9 @@
|
|||
"view-json-modal": {
|
||||
"title-json": "JSON"
|
||||
},
|
||||
"view-panel": {
|
||||
"not-found": "Panel not found"
|
||||
},
|
||||
"visualization-button": {
|
||||
"aria-label-change-visualization": "Change visualization",
|
||||
"aria-label-close": "Close options pane",
|
||||
|
@ -6135,9 +6141,6 @@
|
|||
"share-button": {
|
||||
"aria-label-sharedropdownmenu": "Toggle share menu"
|
||||
},
|
||||
"solo-panel-page": {
|
||||
"loading": "Loading"
|
||||
},
|
||||
"support-snapshot-service": {
|
||||
"description": {
|
||||
"dashboard-troubleshoot-visualization-issues": "Dashboard JSON used to help troubleshoot visualization issues"
|
||||
|
@ -10712,10 +10715,6 @@
|
|||
"could-anything-matching-query": "Could not find anything matching your query"
|
||||
}
|
||||
},
|
||||
"panel-search": {
|
||||
"no-matches": "No matches found",
|
||||
"unsupported-layout": "Unsupported layout"
|
||||
},
|
||||
"panel-type-filter": {
|
||||
"clear-button": "Clear types",
|
||||
"select-aria-label": "Panel type filter",
|
||||
|
|
Loading…
Reference in New Issue