diff --git a/e2e-playwright/various-suite/solo-route.spec.ts b/e2e-playwright/various-suite/solo-route.spec.ts index 63dd4e6439d..7c09bce8da3 100644 --- a/e2e-playwright/various-suite/solo-route.spec.ts +++ b/e2e-playwright/various-suite/solo-route.spec.ts @@ -35,12 +35,12 @@ test.describe( test('Can view solo repeated panel in scenes', async ({ page, selectors }) => { // open Panel Tests - Graph NG const soloPanelUrl = selectors.pages.SoloPanel.url( - 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true' + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=A$panel-2&__feature.dashboardSceneSolo=true' ); await page.goto(soloPanelUrl); // Check that the panel title exists - const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server=B')); + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server=A')); await expect(panelTitle).toBeVisible(); // Check that uplot-main-div does not exist @@ -51,7 +51,7 @@ test.describe( test('Can view solo in repeated row and panel in scenes', async ({ page, selectors }) => { // open Panel Tests - Graph NG const soloPanelUrl = selectors.pages.SoloPanel.url( - 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true' + 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=B$2$panel-2&__feature.dashboardSceneSolo=true' ); await page.goto(soloPanelUrl); diff --git a/e2e/old-arch/various-suite/solo-route.spec.ts b/e2e/old-arch/various-suite/solo-route.spec.ts index 2abd2717d5b..df026f66033 100644 --- a/e2e/old-arch/various-suite/solo-route.spec.ts +++ b/e2e/old-arch/various-suite/solo-route.spec.ts @@ -25,17 +25,17 @@ describe('Solo Route', () => { it('Can view solo repeated panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( - 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true' + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=A$panel-2&__feature.dashboardSceneSolo=true' ); - e2e.components.Panels.Panel.title('server=B').should('exist'); + e2e.components.Panels.Panel.title('server=A').should('exist'); cy.contains('uplot-main-div').should('not.exist'); }); it('Can view solo in repeated row and panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( - 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true' + 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=B$2$panel-2&__feature.dashboardSceneSolo=true' ); e2e.components.Panels.Panel.title('server = B, pod = Rob').should('exist'); diff --git a/e2e/various-suite/solo-route.spec.ts b/e2e/various-suite/solo-route.spec.ts index 2abd2717d5b..df026f66033 100644 --- a/e2e/various-suite/solo-route.spec.ts +++ b/e2e/various-suite/solo-route.spec.ts @@ -25,17 +25,17 @@ describe('Solo Route', () => { it('Can view solo repeated panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( - 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true' + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=A$panel-2&__feature.dashboardSceneSolo=true' ); - e2e.components.Panels.Panel.title('server=B').should('exist'); + e2e.components.Panels.Panel.title('server=A').should('exist'); cy.contains('uplot-main-div').should('not.exist'); }); it('Can view solo in repeated row and panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( - 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true' + 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=B$2$panel-2&__feature.dashboardSceneSolo=true' ); e2e.components.Panels.Panel.title('server = B, pod = Rob').should('exist'); diff --git a/package.json b/package.json index 9babb485971..ede30bbfe43 100644 --- a/package.json +++ b/package.json @@ -290,8 +290,8 @@ "@grafana/plugin-ui": "0.10.9", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "6.29.7", - "@grafana/scenes-react": "6.29.7", + "@grafana/scenes": "^6.30.0", + "@grafana/scenes-react": "^6.30.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index b6bfc95b006..53ebf12af8b 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -99,7 +99,7 @@ export const getShareUrlParams = ( const urlParamsUpdate: UrlQueryMap = {}; if (panel) { - urlParamsUpdate.viewPanel = panel.state.key; + urlParamsUpdate.viewPanel = panel.getPathId(); } if (opts.useAbsoluteTimeRange) { diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index bdcee40b3dd..70dee7c9c67 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -6,8 +6,8 @@ import { } from '@grafana/ui'; import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem'; -import { containsCloneKey, getLastKeyFromClone, isInCloneChain } from '../utils/clone'; -import { findEditPanel, getDashboardSceneFor } from '../utils/utils'; +import { isRepeatCloneOrChildOf } from '../utils/clone'; +import { getDashboardSceneFor } from '../utils/utils'; import { ElementSelection } from './ElementSelection'; import { @@ -183,24 +183,13 @@ export class DashboardEditPane extends SceneObjectBase { } private selectElement(element: ElementSelectionContextItem, options: ElementSelectionOnSelectOptions) { - // We should not select clones - if (isInCloneChain(element.id)) { - if (options.multi) { + let obj = sceneGraph.findByKey(this, element.id); + if (obj) { + // Do not select repeat clones or their children + if (isRepeatCloneOrChildOf(obj)) { return; } - this.clearSelection(); - return; - } - - let obj = sceneGraph.findByKey(this, element.id); - if (obj) { - if (obj instanceof VizPanel && containsCloneKey(getLastKeyFromClone(element.id))) { - const sourceVizPanel = findEditPanel(this, element.id); - if (sourceVizPanel) { - obj = sourceVizPanel; - } - } this.selectObject(obj, element.id, options); } } diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx index bda1336db94..218a8709dee 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -7,7 +7,7 @@ import { Trans, t } from '@grafana/i18n'; import { SceneObject } from '@grafana/scenes'; import { Box, Icon, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui'; -import { isInCloneChain } from '../utils/clone'; +import { isRepeatCloneOrChildOf } from '../utils/clone'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardEditPane } from './DashboardEditPane'; @@ -39,7 +39,7 @@ function DashboardOutlineNode({ sceneObject, editPane, depth }: DashboardOutline const { key } = sceneObject.useState(); const [isCollapsed, setIsCollapsed] = useState(depth > 0); const { isSelected, onSelect } = useElementSelection(key); - const isCloned = useMemo(() => isInCloneChain(key!), [key]); + const isCloned = useMemo(() => isRepeatCloneOrChildOf(sceneObject), [sceneObject]); const editableElement = useMemo(() => getEditableElementFor(sceneObject)!, [sceneObject]); const noTitleText = t('dashboard.outline.tree-item.no-title', ''); diff --git a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx index a35663eeaac..69fd3472c0b 100644 --- a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.tsx @@ -5,7 +5,7 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constan import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { - findOriginalVizPanelByKey, + findVizPanelByKey, getDashboardSceneFor, getLibraryPanelBehavior, getQueryRunnerFor, @@ -54,7 +54,7 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase { it('Should hash the key of the cloned panels and set it as panelId', () => { const queryRunner = sceneGraph.findObject(scene, (o) => o.state.key === 'data-query-runner2')!; - const expectedPanelId = djb2Hash(getCloneKey('panel-2', 1)); - expect(scene.enrichDataRequest(queryRunner).panelId).toEqual(expectedPanelId); + expect(scene.enrichDataRequest(queryRunner).panelId).toEqual(3670868617); }); }); @@ -974,6 +973,10 @@ function buildTestScene(overrides?: Partial) { body: new VizPanel({ title: 'Panel B', key: getCloneKey('panel-2', 1), + repeatSourceKey: 'panel-2', + $variables: new SceneVariableSet({ + variables: [new LocalValueVariable({ name: 'a', value: 'A' })], + }), pluginId: 'table', $data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }), }), diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 0dc7501cbc1..4d85840b1d4 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -61,7 +61,7 @@ import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { DashboardEditView } from '../settings/utils'; import { historySrv } from '../settings/version-history/HistorySrv'; import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; -import { isInCloneChain } from '../utils/clone'; +import { isRepeatCloneOrChildOf } from '../utils/clone'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl } from '../utils/getDashboardUrl'; @@ -660,9 +660,9 @@ export class DashboardScene extends SceneObjectBase impleme let panelId = 0; if (panel && panel.state.key) { - if (isInCloneChain(panel.state.key)) { + if (isRepeatCloneOrChildOf(panel)) { // We check if any of the panel ancestors are clones because we can't use the original panel ID in this case - panelId = djb2Hash(panel?.state.key); + panelId = djb2Hash(panel.getPathId()); } else { // Otherwise, it's the absolute original panel, and we can use the key directly // getPanelIdForVizPanel extracts the panel ID from the key so we don't need to do it manually diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts index 4bad8552455..d89a729ca17 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts @@ -1,10 +1,8 @@ import { AppEvents } from '@grafana/data'; -import { SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { LocalValueVariable, SceneQueryRunner, SceneVariableSet, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { KioskMode } from 'app/types/dashboard'; -import { getCloneKey } from '../utils/clone'; - import { DashboardScene } from './DashboardScene'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; @@ -72,7 +70,7 @@ describe('DashboardSceneUrlSync', () => { let errorNotice = 0; appEvents.on(AppEvents.alertError, (evt) => errorNotice++); - scene.urlSync?.updateFromUrl({ viewPanel: getCloneKey('panel-1', 1) }); + scene.urlSync?.updateFromUrl({ viewPanel: 'A$panel-1' }); expect(scene.state.viewPanelScene).toBeUndefined(); // Verify no error notice was shown @@ -87,8 +85,17 @@ describe('DashboardSceneUrlSync', () => { 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: getCloneKey('panel-1', 1), + key: 'panel-1', pluginId: 'table', }), }), @@ -97,7 +104,7 @@ describe('DashboardSceneUrlSync', () => { // Verify it subscribes to DashboardRepeatsProcessedEvent scene.publishEvent(new DashboardRepeatsProcessedEvent({ source: scene })); - expect(scene.state.viewPanelScene?.getUrlKey()).toBe(getCloneKey('panel-1', 1)); + expect(scene.state.viewPanelScene?.getUrlKey()).toBe('A$panel-1'); }); }); diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index b145635bb39..fcc52e46b81 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -11,8 +11,8 @@ import { buildPanelEditScene } from '../panel-edit/PanelEditor'; import { createDashboardEditViewFor } from '../settings/utils'; import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; import { ShareModal } from '../sharing/ShareModal'; -import { containsCloneKey } from '../utils/clone'; -import { findEditPanel, findVizPanelByKey, getLibraryPanelBehavior } from '../utils/utils'; +import { containsPathIdSeparator, findVizPanelByPathId } from '../utils/pathId'; +import { findEditPanel, getLibraryPanelBehavior } from '../utils/utils'; import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; @@ -74,13 +74,13 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { // Handle view panel state if (typeof values.viewPanel === 'string') { - const panel = findVizPanelByKey(this._scene, values.viewPanel); + 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 (containsCloneKey(values.viewPanel)) { + if (containsPathIdSeparator(values.viewPanel)) { this._handleViewRepeatClone(values.viewPanel); return; } @@ -162,7 +162,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { private _handleViewRepeatClone(viewPanel: string) { if (!this._viewEventSub) { this._viewEventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { - const panel = findVizPanelByKey(this._scene, viewPanel); + const panel = findVizPanelByPathId(this._scene, viewPanel); if (panel) { this._viewEventSub?.unsubscribe(); this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: panel.getRef() }) }); diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx index 8d841b7d1dc..02490e3d5c7 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx @@ -76,7 +76,7 @@ describe('panelMenuBehavior', () => { expect(menu.state.items?.length).toBe(6); // verify view panel url keeps url params and adds viewPanel= - expect(menu.state.items?.[0].href).toBe('/d/dash-1?from=now-5m&to=now&viewPanel=panel-12'); + expect(menu.state.items?.[0].href).toBe('/d/dash-1?from=now-5m&to=now&viewPanel=a$panel-12'); // verify edit url keeps url time range expect(menu.state.items?.[1].href).toBe('/d/dash-1?from=now-5m&to=now&editPanel=12'); // verify share diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index d02e94516dd..30774d910fb 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -12,7 +12,7 @@ import { } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config, locationService } from '@grafana/runtime'; -import { LocalValueVariable, sceneGraph, SceneGridRow, VizPanel, VizPanelMenu } from '@grafana/scenes'; +import { LocalValueVariable, sceneGraph, VizPanel, VizPanelMenu } from '@grafana/scenes'; import { DataQuery, OptionsWithLegend } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { createErrorNotification } from 'app/core/copy/appNotification'; @@ -35,7 +35,7 @@ import { ShowConfirmModalEvent } from 'app/types/events'; import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer'; import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; import { ShareModal } from '../sharing/ShareModal'; -import { isInCloneChain } from '../utils/clone'; +import { isRepeatCloneOrChildOf } from '../utils/clone'; import { DashboardInteractions } from '../utils/interactions'; import { getEditPanelUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils'; @@ -74,7 +74,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) { const dashboard = getDashboardSceneFor(panel); const { isEmbedded } = dashboard.state.meta; const exploreMenuItem = await getExploreMenuItem(panel); - const isReadOnlyRepeat = isInCloneChain(panel.state.key!); + const isReadOnlyRepeat = isRepeatCloneOrChildOf(panel); // For embedded dashboards we only have explore action for now if (isEmbedded) { @@ -504,21 +504,6 @@ function createExtensionContext(panel: VizPanel, dashboard: DashboardScene): Plu }); } - // 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, diff --git a/public/app/features/dashboard-scene/scene/ViewPanelScene.tsx b/public/app/features/dashboard-scene/scene/ViewPanelScene.tsx index 46486395aef..fcd47bb7034 100644 --- a/public/app/features/dashboard-scene/scene/ViewPanelScene.tsx +++ b/public/app/features/dashboard-scene/scene/ViewPanelScene.tsx @@ -19,7 +19,7 @@ export class ViewPanelScene extends SceneObjectBase { } public getUrlKey() { - return this.state.panelRef.resolve().state.key; + return this.state.panelRef.resolve().getPathId(); } public static Component = ({ model }: SceneComponentProps) => { diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index a4f2ef2e2fd..bf3872b2603 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -12,6 +12,7 @@ import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer'; 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 { getPanelIdForVizPanel } from '../utils/utils'; @@ -21,20 +22,24 @@ import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutMana export function setupKeyboardShortcuts(scene: DashboardScene) { const keybindings = new KeybindingSet(); - let vizPanelKey: string | null = null; + let vizPanelPathId: string | null = null; const canEdit = scene.canEditDashboard(); const panelAttentionSubscription = appEvents.subscribe(SetPanelAttentionEvent, (event) => { if (typeof event.payload.panelId === 'string') { - vizPanelKey = event.payload.panelId; + vizPanelPathId = event.payload.panelId; } }); function withFocusedPanel(scene: DashboardScene, fn: (vizPanel: VizPanel) => void) { return () => { - const vizPanel = sceneGraph.findObject(scene, (o) => o.state.key === vizPanelKey); - if (vizPanel && vizPanel instanceof VizPanel) { + if (vizPanelPathId == null) { + return; + } + + const vizPanel = findVizPanelByPathId(scene, vizPanelPathId); + if (vizPanel) { fn(vizPanel); return; } diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx index 99bcff261e6..6f6d211777b 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx @@ -116,7 +116,10 @@ export class AutoGridItem extends SceneObjectBase implements const isSource = index === 0; const clone = isSource ? panelToRepeat - : panelToRepeat.clone({ key: getCloneKey(panelToRepeat.state.key!, index) }); + : panelToRepeat.clone({ + key: getCloneKey(panelToRepeat.state.key!, index), + repeatSourceKey: panelToRepeat.state.key, + }); clone.setState({ $variables: getLocalVariableValueSet(variable, variableValues[index], variableTexts[index]) }); diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayout.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayout.tsx index b96550cfa22..7ba318f2f4b 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayout.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayout.tsx @@ -2,6 +2,7 @@ import { createRef, CSSProperties, PointerEvent as ReactPointerEvent } from 'rea import { SceneLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { getLayoutOrchestratorFor } from '../../utils/utils'; import { AutoGridItem } from './AutoGridItem'; @@ -92,6 +93,10 @@ export class AutoGridLayout extends SceneObjectBase impleme } public isDraggable(): boolean { + if (isRepeatCloneOrChildOf(this)) { + return false; + } + return this.state.isDraggable ?? false; } diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.test.ts b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.test.ts index 76dbfdbf291..cb4499c30bb 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.test.ts +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.test.ts @@ -1,6 +1,6 @@ import { SceneQueryRunner, VizPanel } from '@grafana/scenes'; -import { findVizPanelByKey } from '../../utils/utils'; +import { DashboardEditActionEvent } from '../../edit-pane/shared'; import { DashboardScene } from '../DashboardScene'; import { AutoGridItem } from './AutoGridItem'; @@ -8,33 +8,38 @@ import { AutoGridLayout } from './AutoGridLayout'; import { AutoGridLayoutManager } from './AutoGridLayoutManager'; describe('AutoGridLayoutManager', () => { - it('Should clone the layout', () => { - const { manager } = setup(); - const clone = manager.cloneLayout('foo', true) as AutoGridLayoutManager; + it('can remove panel', () => { + const { manager, panel1 } = setup(); - expect(clone).not.toBe(manager); - expect(clone.state.layout).not.toBe(manager.state.layout); - expect(clone.state.layout.state.children).not.toBe(manager.state.layout.state.children); - expect(clone.state.layout.state.children.length).toBe(manager.state.layout.state.children.length); + manager.subscribeToEvent(DashboardEditActionEvent, (event) => { + event.payload.perform(); + }); - const panelA = findVizPanelByKey(clone, 'foo/grid-item-1/panel-1'); - expect(panelA?.state.title).toBe('Panel A'); + manager.removePanel(panel1); - const panelB = findVizPanelByKey(clone, 'foo/grid-item-2/panel-2'); - expect(panelB?.state.title).toBe('Panel B'); + expect(manager.state.layout.state.children.length).toBe(1); }); }); function setup() { + const panel1 = new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }); + + const panel2 = new VizPanel({ + title: 'Panel A', + key: 'panel-1', + pluginId: 'table', + $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), + }); + const gridItems = [ new AutoGridItem({ key: 'grid-item-1', - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), + body: panel1, }), new AutoGridItem({ key: 'grid-item-2', @@ -50,5 +55,5 @@ function setup() { new DashboardScene({ body: manager }); - return { manager }; + return { manager, panel1, panel2 }; } diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx index 69ee13494ac..397c0ce6bb8 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx @@ -7,13 +7,11 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { dashboardEditActions, NewObjectAddedToCanvasEvent } from '../../edit-pane/shared'; import { serializeAutoGridLayout } from '../../serialization/layoutSerializers/AutoGridLayoutSerializer'; -import { joinCloneKeys } from '../../utils/clone'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; import { forceRenderChildren, getDashboardSceneFor, getGridItemKeyForPanelId, - getPanelIdForVizPanel, getVizPanelKeyForPanelId, } from '../../utils/utils'; import { DashboardGridItem } from '../layout-default/DashboardGridItem'; @@ -90,8 +88,13 @@ export class AutoGridLayoutManager } public getOutlineChildren(): SceneObject[] { - const outlineChildren = this.state.layout.state.children.map((gridItem) => gridItem.state.body); - return outlineChildren; + const children: SceneObject[] = []; + + for (const child of this.state.layout.state.children) { + children.push(child.state.body, ...(child.state.repeatedPanels || [])); + } + + return children; } public addPanel(vizPanel: VizPanel) { @@ -245,26 +248,7 @@ export class AutoGridLayoutManager } public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { - return this.clone({ - layout: this.state.layout.clone({ - isDraggable: isSource && this.state.layout.state.isDraggable, - children: this.state.layout.state.children.map((gridItem) => { - if (gridItem instanceof AutoGridItem) { - // Get the original panel ID from the gridItem's key - const panelId = getPanelIdForVizPanel(gridItem.state.body); - const gridItemKey = joinCloneKeys(ancestorKey, getGridItemKeyForPanelId(panelId)); - - return gridItem.clone({ - key: gridItemKey, - body: gridItem.state.body.clone({ - key: joinCloneKeys(gridItemKey, getVizPanelKeyForPanelId(panelId)), - }), - }); - } - throw new Error('Unexpected child type'); - }), - }), - }); + return this.clone({}); } public getOptions(): OptionsPaneItemDescriptor[] { diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutRenderer.tsx index c70a3ef5200..8208df99d9d 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutRenderer.tsx @@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; -import { useHasClonedParents } from '../../utils/clone'; +import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { useDashboardState } from '../../utils/utils'; import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions'; import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles'; @@ -14,7 +14,6 @@ import { AutoGridLayoutManager } from './AutoGridLayoutManager'; export function AutoGridLayoutRenderer({ model }: SceneComponentProps) { const { children, isHidden } = model.useState(); - const hasClonedParents = useHasClonedParents(model); const styles = useStyles2(getStyles, model.state); const { layoutOrchestrator, isEditing } = useDashboardState(model); const layoutManager = sceneGraph.getAncestor(model, AutoGridLayoutManager); @@ -24,7 +23,7 @@ export function AutoGridLayoutRenderer({ model }: SceneComponentProps { expect(panel1.state.$variables?.state.variables[0].getValueText?.()).toBe('A'); expect(panel2.state.$variables?.state.variables[0].getValue()).toBe('2'); - expect(panel1.state.key).toBe('panel-1'); - expect(isInCloneChain(panel2.state.key!)).toBe(true); + expect(panel1.state.repeatSourceKey).toBe(undefined); + expect(panel2.state.repeatSourceKey).toBe(repeater.state.body.state.key); }); it('Should wait for variable to load', async () => { diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx index f228575170b..bea25d00605 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx @@ -173,7 +173,10 @@ export class DashboardGridItem const isSource = index === 0; const clone = isSource ? panelToRepeat - : panelToRepeat.clone({ key: getCloneKey(panelToRepeat.state.key!, index) }); + : panelToRepeat.clone({ + key: getCloneKey(panelToRepeat.state.key!, index), + repeatSourceKey: panelToRepeat.state.key, + }); clone.setState({ $variables: getLocalVariableValueSet(variable, variableValues[index], variableTexts[index]) }); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx index c2e3bb77691..fdb9dd9fc38 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.test.tsx @@ -210,22 +210,6 @@ describe('DefaultGridLayoutManager', () => { expect(gridRow.state.children.length).toBe(3); }); - - it('Should clone the layout correctly', () => { - const { manager } = setup(); - const clone = manager.cloneLayout('foo', true) as DefaultGridLayoutManager; - const panelA = findVizPanelByKey(clone, 'foo/grid-item-0/panel-0'); - expect(panelA?.state.title).toBe('Panel A'); - - const panelB = findVizPanelByKey(clone, 'foo/grid-item-1/panel-1'); - expect(panelB?.state.title).toBe('Panel B'); - - const panelC = findVizPanelByKey(clone, 'foo/panel-2/grid-item-3/panel-3'); - expect(panelC?.state.title).toBe('Panel C'); - - const panelD = findVizPanelByKey(clone, 'foo/panel-2/grid-item-4/panel-4'); - expect(panelD?.state.title).toBe('Panel D'); - }); }); }); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index 76fffddd0a5..97c04cbdd86 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -29,7 +29,7 @@ import { ObjectsReorderedOnCanvasEvent, } from '../../edit-pane/shared'; import { serializeDefaultGridLayout } from '../../serialization/layoutSerializers/DefaultGridLayoutSerializer'; -import { isClonedKey, joinCloneKeys, useHasClonedParents } from '../../utils/clone'; +import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; import { forceRenderChildren, @@ -433,11 +433,7 @@ export class DefaultGridLayoutManager for (const child of this.state.grid.state.children) { // Flatten repeated grid items if (child instanceof DashboardGridItem) { - if (child.state.repeatedPanels) { - children.push(...child.state.repeatedPanels); - } else { - children.push(child.state.body); - } + children.push(child.state.body, ...(child.state.repeatedPanels || [])); } } @@ -445,75 +441,7 @@ export class DefaultGridLayoutManager } public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { - return this.clone({ - grid: this.state.grid.clone({ - isResizable: isSource && this.state.grid.state.isResizable, - isDraggable: isSource && this.state.grid.state.isDraggable, - children: this.state.grid.state.children.reduce<{ panelId: number; children: SceneGridItemLike[] }>( - (childrenAcc, child) => { - if (child instanceof DashboardGridItem) { - const gridItemKey = joinCloneKeys(ancestorKey, getGridItemKeyForPanelId(childrenAcc.panelId)); - - const gridItem = child.clone({ - key: gridItemKey, - body: child.state.body.clone({ - key: joinCloneKeys(gridItemKey, getVizPanelKeyForPanelId(childrenAcc.panelId++)), - }), - isDraggable: isSource && child.state.isDraggable, - isResizable: isSource && child.state.isResizable, - }); - - childrenAcc.children.push(gridItem); - - return childrenAcc; - } - - if (child instanceof SceneGridRow) { - const rowKey = joinCloneKeys(ancestorKey, getVizPanelKeyForPanelId(childrenAcc.panelId++)); - - const row = child.clone({ - key: rowKey, - children: child.state.children.reduce((rowAcc, rowChild) => { - if (isClonedKey(rowChild.state.key!)) { - return rowAcc; - } - - if (!(rowChild instanceof DashboardGridItem)) { - rowAcc.push(rowChild.clone()); - return rowAcc; - } - - const gridItemKey = joinCloneKeys(rowKey, getGridItemKeyForPanelId(childrenAcc.panelId)); - - const gridItem = rowChild.clone({ - key: gridItemKey, - isDraggable: isSource && rowChild.state.isDraggable, - isResizable: isSource && rowChild.state.isResizable, - body: rowChild.state.body.clone({ - key: joinCloneKeys(gridItemKey, getVizPanelKeyForPanelId(childrenAcc.panelId++)), - }), - }); - - rowAcc.push(gridItem); - return rowAcc; - }, []), - isDraggable: isSource && child.state.isDraggable, - isResizable: isSource && child.state.isResizable, - }); - - childrenAcc.children.push(row); - - return childrenAcc; - } - - childrenAcc.children.push(child.clone()); - - return childrenAcc; - }, - { panelId: 0, children: [] } - ).children, - }), - }); + return this.clone({}); } public removeRow(row: SceneGridRow, removePanels = false) { @@ -634,7 +562,7 @@ function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps { // Verify that first row still has repeat behavior const row1 = grid.state.children[1] as SceneGridRow; - expect(row1.state.key).toBe(getCloneKey('row-1', 0)); + expect(row1.state.key).toBe('row-1'); expect(row1.state.$behaviors?.[0]).toBeInstanceOf(RowRepeaterBehavior); expect(row1.state.$variables!.state.variables[0].getValue()).toBe('A1'); expect(row1.state.actions).toBeDefined(); const gridItemRow1 = row1.state.children[0] as SceneGridItem; - expect(gridItemRow1.state.key!).toBe(joinCloneKeys(row1.state.key!, 'grid-item-1')); - expect(gridItemRow1.state.body?.state.key).toBe('canvas-1'); + expect(gridItemRow1.state.key!).toBe('grid-item-1'); const row2 = grid.state.children[2] as SceneGridRow; expect(row2.state.key).toBe(getCloneKey('row-1', 1)); @@ -70,51 +69,14 @@ describe('RowRepeaterBehavior', () => { expect(row2.state.actions).toBeUndefined(); const gridItemRow2 = row2.state.children[0] as SceneGridItem; - expect(gridItemRow2.state.key!).toBe(joinCloneKeys(row2.state.key!, 'grid-item-1')); - expect(gridItemRow2.state.body?.state.key).toBe(joinCloneKeys(gridItemRow2.state.key!, 'canvas-1')); + expect(gridItemRow2.state.key!).toBe(row2.state.key! + 'grid-item-1'); }); it('Repeated rows should be read only', () => { const row1 = grid.state.children[1] as SceneGridRow; const row2 = grid.state.children[2] as SceneGridRow; - expect(isInCloneChain(row1.state.key!)).toBe(false); - expect(isInCloneChain(row2.state.key!)).toBe(true); - }); - - it('Should update all rows when a panel is added to a clone', async () => { - const originalRow = grid.state.children[1] as SceneGridRow; - const clone1 = grid.state.children[2] as SceneGridRow; - const clone2 = grid.state.children[3] as SceneGridRow; - - expect(originalRow.state.children.length).toBe(1); - expect(clone1.state.children.length).toBe(1); - expect(clone2.state.children.length).toBe(1); - - clone1.setState({ - children: [ - ...clone1.state.children, - new SceneGridItem({ - x: 0, - y: 16, - width: 24, - height: 5, - key: 'grid-item-4', - body: new SceneCanvasText({ - text: 'new panel', - }), - }), - ], - }); - - grid.forceRender(); - - // repeater has run so there are new clone row objects - const newClone1 = grid.state.children[2] as SceneGridRow; - const newClone2 = grid.state.children[3] as SceneGridRow; - - expect(originalRow.state.children.length).toBe(2); - expect(newClone1.state.children.length).toBe(2); - expect(newClone2.state.children.length).toBe(2); + expect(isRepeatCloneOrChildOf(row1)).toBe(false); + expect(isRepeatCloneOrChildOf(row2)).toBe(true); }); it('Should push row at the bottom down', () => { diff --git a/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts index a9cd9969c94..a813fddaace 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts +++ b/public/app/features/dashboard-scene/scene/layout-default/RowRepeaterBehavior.ts @@ -12,16 +12,7 @@ import { VariableValueSingle, } from '@grafana/scenes'; -import { - containsCloneKey, - getLastKeyFromClone, - isClonedKeyOf, - joinCloneKeys, - getCloneKey, - isClonedKey, - getOriginalKey, - getLocalVariableValueSet, -} from '../../utils/clone'; +import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone'; import { getMultiVariableValues } from '../../utils/utils'; import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; @@ -49,11 +40,10 @@ export class RowRepeaterBehavior extends SceneObjectBase !isClonedKey(child.state.key!)); const sub = layout.subscribeToState(() => { - const repeatedRows = layout.state.children.filter((child) => - isClonedKeyOf(child.state.key!, originalRow.state.key!) + const repeatedRows = layout.state.children.filter( + (child) => child instanceof SceneGridRow && child.state.repeatSourceKey === originalRow.state.key ); // go through cloned rows, search for panels that are not clones @@ -62,31 +52,12 @@ export class RowRepeaterBehavior extends SceneObjectBase !isClonedKey(child.state.key!)); - // if no differences in row children compared to original, then no new panel added to clone - if (rowNonClonedPanels.length === originalRowNonClonedPanels.length) { + if (row.state.children.length === originalRow.state.children.length) { continue; } - // if there are differences, find the new panel, move it to the original and perform repeat - const gridItem = rowNonClonedPanels.find((gridItem) => !containsCloneKey(gridItem.state.key!)); - - if (gridItem) { - const newGridItem = gridItem.clone(); - - row.setState({ children: row.state.children.filter((item) => item !== gridItem) }); - - // if we are moving a panel from the origin row to a clone row, we just return - // this means we are modifying the origin row, re-triggering the repeat and losing that panel - if (originalRow.state.children.find((item) => item.state.key === newGridItem.state.key)) { - return; - } - - originalRow.setState({ children: [...originalRow.state.children, newGridItem] }); - - this.performRepeat(true); - } + this.performRepeat(true); } }); @@ -163,15 +134,14 @@ export class RowRepeaterBehavior extends SceneObjectBase 0 ? sourceItem.clone() : sourceItem; const cloneItemY = sourceItemY + (rowContentHeight + 1) * rowIndex; - const cloneItem = - rowIndex > 0 - ? sourceItem.clone({ - isDraggable: false, - isResizable: false, - }) - : sourceItem; - - cloneItem.setState({ - key: cloneItemKey, - y: cloneItemY, - }); + // Update grid item keys on clone rows (not needed on source row) + // Needed to not have duplicate grid items keys in the same grid if (rowIndex > 0) { - ensureUniqueKeys(cloneItem, cloneItemKey); + cloneItem.setState({ y: cloneItemY, key: rowClone.state.key + sourceItem.state.key! }); } children.push(cloneItem); @@ -252,9 +211,7 @@ function getRowContentHeight(panels: SceneGridItemLike[]): number { function updateLayout(layout: SceneGridLayout, rows: SceneGridRow[], maxYOfRows: number, rowKey: string) { const allChildren = getLayoutChildrenFilterOutRepeatClones(layout, rowKey); - const index = allChildren.findIndex( - (child) => child instanceof SceneGridRow && getOriginalKey(child.state.key!) === getOriginalKey(rowKey) - ); + const index = allChildren.findIndex((child) => child instanceof SceneGridRow && child.state.key === rowKey); if (index === -1) { throw new Error('RowRepeaterBehavior: Parent row not found in layout children'); @@ -284,14 +241,6 @@ function updateLayout(layout: SceneGridLayout, rows: SceneGridRow[], maxYOfRows: function getLayoutChildrenFilterOutRepeatClones(layout: SceneGridLayout, rowKey: string) { return layout.state.children.filter( - (child) => !(child instanceof SceneGridRow) || !isClonedKeyOf(getLastKeyFromClone(child.state.key!), rowKey) + (child) => !(child instanceof SceneGridRow) || child.state.repeatSourceKey !== rowKey ); } - -function ensureUniqueKeys(item: SceneGridItemLike, ancestors: string) { - item.forEachChild((child) => { - const key = joinCloneKeys(ancestors, child.state.key!); - child.setState({ key }); - ensureUniqueKeys(child, key); - }); -} diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx index 7bbc2ffebe3..72e34058ab8 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -45,6 +45,8 @@ export interface RowItemState extends SceneObjectState { conditionalRendering?: ConditionalRendering; repeatByVariable?: string; repeatedRows?: RowItem[]; + /** Marks object as a repeated object and a key pointer to source object */ + repeatSourceKey?: string; } export class RowItem diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx index affa68d54ea..1913ce0c2b4 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx @@ -9,7 +9,7 @@ import { SceneComponentProps } from '@grafana/scenes'; import { clearButtonStyles, Icon, Tooltip, useElementSelection, usePointerDistance, useStyles2 } from '@grafana/ui'; import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden'; -import { useIsClone } from '../../utils/clone'; +import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { useDashboardState, useInterpolatedTitle } from '../../utils/utils'; import { DashboardScene } from '../DashboardScene'; @@ -17,7 +17,7 @@ import { RowItem } from './RowItem'; export function RowItemRenderer({ model }: SceneComponentProps) { const { layout, collapse: isCollapsed, fillScreen, hideHeader: isHeaderHidden, isDropTarget, key } = model.useState(); - const isClone = useIsClone(model); + const isClone = isRepeatCloneOrChildOf(model); const { isEditing } = useDashboardState(model); const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden(model); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.test.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.test.tsx index c6ac4f4066b..c4c7496cda4 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.test.tsx @@ -38,7 +38,7 @@ describe('RowItemRepeater', () => { expect(screen.queryByText('Row C')).toBeInTheDocument(); }); - expect(rowToRepeat.state.key).toBe('row-1-clone-0'); + expect(rowToRepeat.state.key).toBe('row-1'); expect(rowToRepeat.state.repeatedRows!.length).toBe(2); expect(rowToRepeat.state.repeatedRows![0].state.key).toBe('row-1-clone-1'); }); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx index ea606dcaadf..39681c4fa4c 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemRepeater.tsx @@ -97,12 +97,17 @@ export function performRowRepeats(variable: MultiValueVariable, row: RowItem, co const rowCloneKey = getCloneKey(row.state.key!, rowIndex); const rowClone = isSourceRow ? row - : row.clone({ repeatByVariable: undefined, repeatedRows: undefined, layout: undefined }); + : row.clone({ + key: rowCloneKey, + repeatSourceKey: row.state.key, + repeatByVariable: undefined, + repeatedRows: undefined, + layout: undefined, + }); const layout = isSourceRow ? row.getLayout() : row.getLayout().cloneLayout(rowCloneKey, false); rowClone.setState({ - key: rowCloneKey, $variables: getLocalVariableValueSet(variable, variableValues[rowIndex], variableTexts[rowIndex]), layout, }); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index 1cdf72a480c..d021eac7889 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -12,7 +12,6 @@ import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboa import { dashboardEditActions, ObjectsReorderedOnCanvasEvent } from '../../edit-pane/shared'; import { serializeRowsLayout } from '../../serialization/layoutSerializers/RowsLayoutSerializer'; -import { isClonedKey, joinCloneKeys } from '../../utils/clone'; import { getDashboardSceneFor } from '../../utils/utils'; import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; @@ -67,16 +66,7 @@ export class RowsLayoutManager extends SceneObjectBase i } public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { - return this.clone({ - rows: this.state.rows.map((row) => { - const key = joinCloneKeys(ancestorKey, row.state.key!); - - return row.clone({ - key, - layout: row.state.layout.cloneLayout(key, isSource), - }); - }), - }); + return this.clone({}); } public duplicate(): DashboardLayoutManager { @@ -188,7 +178,7 @@ export class RowsLayoutManager extends SceneObjectBase i if (layout instanceof TabsLayoutManager) { for (const tab of layout.state.tabs) { - if (isClonedKey(tab.state.key!)) { + if (tab.state.repeatSourceKey) { continue; } @@ -221,21 +211,24 @@ export class RowsLayoutManager extends SceneObjectBase i } if (child instanceof SceneGridRow) { - if (!isClonedKey(child.state.key!)) { - const behaviour = child.state.$behaviors?.find((b) => b instanceof RowRepeaterBehavior); - - config.push({ - title: child.state.title, - isCollapsed: !!child.state.isCollapsed, - isDraggable: child.state.isDraggable, - isResizable: child.state.isResizable, - children: child.state.children, - repeat: behaviour?.state.variableName, - }); - - // Since we encountered a row item, any subsequent panels should be added to a new row - children = undefined; + // Skip repeated row clones + if (child.state.repeatSourceKey) { + return; } + + const behaviour = child.state.$behaviors?.find((b) => b instanceof RowRepeaterBehavior); + + config.push({ + title: child.state.title, + isCollapsed: !!child.state.isCollapsed, + isDraggable: child.state.isDraggable, + isResizable: child.state.isResizable, + children: child.state.children, + repeat: behaviour?.state.variableName, + }); + + // Since we encountered a row item, any subsequent panels should be added to a new row + children = undefined; } else { if (!children) { children = []; diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx index 9c74a5b37a7..523b44c6422 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx @@ -7,7 +7,7 @@ import { Trans } from '@grafana/i18n'; import { MultiValueVariable, SceneComponentProps, sceneGraph, useSceneObjectState } from '@grafana/scenes'; import { Button, useStyles2 } from '@grafana/ui'; -import { isInCloneChain } from '../../utils/clone'; +import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { useDashboardState } from '../../utils/utils'; import { useClipboardState } from '../layouts-shared/useClipboardState'; @@ -21,7 +21,7 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps) { const styles = useStyles2(getStyles); const pointerDistance = usePointerDistance(); const [isConditionallyHidden] = useIsConditionallyHidden(model); - const isClone = useIsClone(model); + const isClone = isRepeatCloneOrChildOf(model); const isDraggable = !isClone && isEditing; diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.test.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.test.tsx index 6533dab789d..d8a7cf80988 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.test.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.test.tsx @@ -38,7 +38,7 @@ describe('TabItemRepeater', () => { expect(screen.queryByText('Tab C')).toBeInTheDocument(); }); - expect(tabToRepeat.state.key).toBe('tab-1-clone-0'); + expect(tabToRepeat.state.key).toBe('tab-1'); expect(tabToRepeat.state.repeatedTabs!.length).toBe(2); expect(tabToRepeat.state.repeatedTabs![0].state.key).toBe('tab-1-clone-1'); }); diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.tsx index e8ce39b892c..16ac618b600 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeater.tsx @@ -152,12 +152,17 @@ export function createTabRepeats({ const tabCloneKey = getCloneKey(tab.state.key!, tabIndex); const tabClone = isSourceTab ? tab - : tab.clone({ repeatByVariable: undefined, repeatedTabs: undefined, layout: undefined }); + : tab.clone({ + key: tabCloneKey, + repeatSourceKey: tab.state.key, + repeatByVariable: undefined, + repeatedTabs: undefined, + layout: undefined, + }); const layout = isSourceTab ? tab.getLayout() : tab.getLayout().cloneLayout(tabCloneKey, false); tabClone.setState({ - key: tabCloneKey, $variables: getLocalVariableValueSet(variable, variableValues[tabIndex], variableTexts[tabIndex]), layout, }); diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index 4ebf8583330..ec14a3c2d3a 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -12,15 +12,6 @@ import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboa import { dashboardEditActions, ObjectsReorderedOnCanvasEvent } from '../../edit-pane/shared'; import { serializeTabsLayout } from '../../serialization/layoutSerializers/TabsLayoutSerializer'; -import { - containsCloneKey, - getCloneKey, - getLastKeyFromClone, - getOriginalKey, - isClonedKey, - isClonedKeyOf, - joinCloneKeys, -} from '../../utils/clone'; import { getDashboardSceneFor } from '../../utils/utils'; import { RowItem } from '../layout-rows/RowItem'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; @@ -138,16 +129,7 @@ export class TabsLayoutManager extends SceneObjectBase i } public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { - return this.clone({ - tabs: this.state.tabs.map((tab) => { - const key = joinCloneKeys(ancestorKey, tab.state.key!); - - return tab.clone({ - key, - layout: tab.state.layout.cloneLayout(key, isSource), - }); - }), - }); + return this.clone(); } public getOutlineChildren() { @@ -257,19 +239,19 @@ export class TabsLayoutManager extends SceneObjectBase i let destinationTab = allTabs[toIndex]; let selectionIndex = toIndex; - if (containsCloneKey(getLastKeyFromClone(destinationTab.state.key!))) { - if (isClonedKeyOf(destinationTab.state.key!, objectToMove.state.key!)) { + if (destinationTab.state.repeatSourceKey) { + if (destinationTab.state.repeatSourceKey === objectToMove.state.repeatSourceKey) { // moving tab between its clones return; } - const originalTabKey = getCloneKey(getOriginalKey(destinationTab.state.key!), 0); - const originalTabIndex = allTabs.findIndex((tab) => tab.state.key === originalTabKey); - if (originalTabIndex !== -1) { - destinationTab = allTabs[originalTabIndex]; + const sourceTabIndx = allTabs.findIndex((tab) => tab.state.key === destinationTab.state.repeatSourceKey); + + if (sourceTabIndx !== -1) { + destinationTab = allTabs[sourceTabIndx]; const isMovingLeft = toIndex < fromIndex; - selectionIndex = originalTabIndex + (isMovingLeft ? 0 : destinationTab.state.repeatedTabs?.length || 0); + selectionIndex = sourceTabIndx + (isMovingLeft ? 0 : destinationTab.state.repeatedTabs?.length || 0); } } @@ -319,7 +301,7 @@ export class TabsLayoutManager extends SceneObjectBase i if (layout instanceof RowsLayoutManager) { for (const row of layout.state.rows) { - if (isClonedKey(row.state.key!)) { + if (row.state.repeatSourceKey) { continue; } diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx index 1e4c81c546a..a223350371b 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx @@ -8,7 +8,7 @@ import { MultiValueVariable, SceneComponentProps, sceneGraph, useSceneObjectStat import { Button, TabContent, TabsBar, useStyles2 } from '@grafana/ui'; import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden'; -import { isInCloneChain } from '../../utils/clone'; +import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { getDashboardSceneFor } from '../../utils/utils'; import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles'; import { useClipboardState } from '../layouts-shared/useClipboardState'; @@ -27,7 +27,7 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts index 284d0a4fec2..81e901c0a32 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts @@ -2,7 +2,6 @@ import { Spec as DashboardV2Spec, RowsLayoutRowKind } from '@grafana/schema/dist import { RowItem } from '../../scene/layout-rows/RowItem'; import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager'; -import { isClonedKey } from '../../utils/clone'; import { layoutDeserializerRegistry } from './layoutSerializerRegistry'; import { getConditionalRendering } from './utils'; @@ -11,7 +10,7 @@ export function serializeRowsLayout(layoutManager: RowsLayoutManager): Dashboard return { kind: 'RowsLayout', spec: { - rows: layoutManager.state.rows.filter((row) => !isClonedKey(row.state.key!)).map(serializeRow), + rows: layoutManager.state.rows.filter((row) => !row.state.repeatSourceKey).map(serializeRow), }, }; } diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts index 883f80c5415..52d378d98c3 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts @@ -2,7 +2,6 @@ import { Spec as DashboardV2Spec, TabsLayoutTabKind } from '@grafana/schema/dist import { TabItem } from '../../scene/layout-tabs/TabItem'; import { TabsLayoutManager } from '../../scene/layout-tabs/TabsLayoutManager'; -import { isClonedKey } from '../../utils/clone'; import { layoutDeserializerRegistry } from './layoutSerializerRegistry'; import { getConditionalRendering } from './utils'; @@ -11,7 +10,7 @@ export function serializeTabsLayout(layoutManager: TabsLayoutManager): Dashboard return { kind: 'TabsLayout', spec: { - tabs: layoutManager.state.tabs.filter((tab) => !isClonedKey(tab.state.key!)).map(serializeTab), + tabs: layoutManager.state.tabs.filter((tab) => !tab.state.repeatSourceKey).map(serializeTab), }, }; } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index f5b9a700bd8..a18a5918c7e 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -36,7 +36,6 @@ import { PanelTimeRange } from '../scene/PanelTimeRange'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior'; -import { isClonedKey } from '../utils/clone'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { djb2Hash } from '../utils/djb2Hash'; import { @@ -74,9 +73,10 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa if (child instanceof SceneGridRow) { // Skip repeat clones or when generating a snapshot - if (isClonedKey(child.state.key!) && !isSnapshot) { + if (child.state.repeatSourceKey && !isSnapshot) { continue; } + gridRowToSaveModel(child, panels, isSnapshot); } } diff --git a/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx index c7bb7284d0f..74b7952ab02 100644 --- a/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx @@ -6,7 +6,7 @@ import { dateTime } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test'; import { selectors } from '@grafana/e2e-selectors'; import { config, locationService, setPluginImportUtils } from '@grafana/runtime'; -import { SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { LocalValueVariable, SceneTimeRange, SceneVariableSet, VizPanel } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; @@ -46,7 +46,7 @@ describe('ShareLinkTab', () => { buildAndRenderScenario({}); expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue( - 'http://dashboards.grafana.com/grafana/d/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12' + 'http://dashboards.grafana.com/grafana/d/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=A$panel-12' ); }); }); @@ -57,7 +57,7 @@ describe('ShareLinkTab', () => { await act(() => tab.onToggleLockedTime()); expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue( - 'http://dashboards.grafana.com/grafana/d/dash-1?from=now-6h&to=now&viewPanel=panel-12' + 'http://dashboards.grafana.com/grafana/d/dash-1?from=now-6h&to=now&viewPanel=A$panel-12' ); }); }); @@ -67,7 +67,7 @@ describe('ShareLinkTab', () => { await act(() => tab.onThemeChange('light')); expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue( - 'http://dashboards.grafana.com/grafana/d/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12&theme=light' + 'http://dashboards.grafana.com/grafana/d/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=A$panel-12&theme=light' ); }); @@ -88,7 +88,7 @@ describe('ShareLinkTab', () => { await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage }) ).toHaveAttribute( 'href', - 'http://dashboards.grafana.com/grafana/render/d-solo/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&panelId=panel-12&__feature.dashboardSceneSolo=true&width=1000&height=500&tz=Pacific%2FEaster' + 'http://dashboards.grafana.com/grafana/render/d-solo/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&panelId=A$panel-12&__feature.dashboardSceneSolo=true&width=1000&height=500&tz=Pacific%2FEaster' ); }); }); @@ -102,6 +102,15 @@ function buildAndRenderScenario(options: ScenarioOptions) { title: 'Panel A', pluginId: 'table', key: 'panel-12', + $variables: new SceneVariableSet({ + variables: [ + new LocalValueVariable({ + name: 'server', + value: 'A', + text: 'A', + }), + ], + }), }); const tab = new ShareLinkTab({ panelRef: panel.getRef() }); const scene = new DashboardScene({ diff --git a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx index 0800e9fe0ad..9d2daa5812b 100644 --- a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx @@ -76,7 +76,7 @@ export class ShareLinkTab extends SceneObjectBase implements let imageQueryParams = urlParamsUpdate; if (panel) { delete imageQueryParams.viewPanel; - imageQueryParams.panelId = panel.state.key; + imageQueryParams.panelId = panel.getPathId(); // force solo route to use scenes imageQueryParams['__feature.dashboardSceneSolo'] = true; } @@ -84,7 +84,7 @@ export class ShareLinkTab extends SceneObjectBase implements const imageUrl = getDashboardUrl({ uid: dashboard.state.uid, currentQueryParams: window.location.search, - updateQuery: { ...urlParamsUpdate, ...queryOptions, panelId: panel?.state.key }, + updateQuery: { ...urlParamsUpdate, ...queryOptions, panelId: panel?.getPathId() }, absolute: true, soloRoute: true, render: true, diff --git a/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx b/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx index e63cda018a1..5d37b375dbe 100644 --- a/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx +++ b/public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx @@ -43,7 +43,7 @@ function SharePanelEmbedTabRenderer({ model }: SceneComponentProps { it('should return the cloned panel when panel is found', () => { const { dashboard } = setup(); - const { result } = renderHook(() => useSoloPanel(dashboard, 'panel-1-clone-1')); + const { result } = renderHook(() => useSoloPanel(dashboard, 'A$panel-1')); const panel = findVizPanelByKey(dashboard, 'panel-1'); expect(result.current[0]).not.toBe(panel); diff --git a/public/app/features/dashboard-scene/solo/useSoloPanel.ts b/public/app/features/dashboard-scene/solo/useSoloPanel.ts index acc7409bdc3..37777439b4c 100644 --- a/public/app/features/dashboard-scene/solo/useSoloPanel.ts +++ b/public/app/features/dashboard-scene/solo/useSoloPanel.ts @@ -4,10 +4,9 @@ import { VizPanel, UrlSyncManager } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { DashboardRepeatsProcessedEvent } from '../scene/types/DashboardRepeatsProcessedEvent'; -import { containsCloneKey } from '../utils/clone'; -import { findVizPanelByKey } from '../utils/utils'; +import { containsPathIdSeparator, findVizPanelByPathId } from '../utils/pathId'; -export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPanel | undefined, string | undefined] { +export function useSoloPanel(dashboard: DashboardScene, pathId: string): [VizPanel | undefined, string | undefined] { const [panel, setPanel] = useState(); const [error, setError] = useState(); @@ -19,7 +18,7 @@ export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPa let panel: VizPanel | null = null; try { - panel = findVizPanelByKey(dashboard, panelId); + panel = findVizPanelByPathId(dashboard, pathId); } catch (e) { // do nothing, just the panel is not found or not a VizPanel } @@ -27,8 +26,8 @@ export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPa if (panel) { activateParents(panel); setPanel(panel); - } else if (containsCloneKey(panelId)) { - findRepeatClone(dashboard, panelId).then((panel) => { + } else if (containsPathIdSeparator(pathId)) { + findRepeatClone(dashboard, pathId).then((panel) => { if (panel) { setPanel(panel); } else { @@ -40,7 +39,7 @@ export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPa } return cleanUp; - }, [dashboard, panelId]); + }, [dashboard, pathId]); return [panel, error]; } @@ -54,10 +53,10 @@ function activateParents(panel: VizPanel) { } } -function findRepeatClone(dashboard: DashboardScene, panelId: string): Promise { +function findRepeatClone(dashboard: DashboardScene, pathId: string): Promise { return new Promise((resolve) => { dashboard.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { - const panel = findVizPanelByKey(dashboard, panelId); + const panel = findVizPanelByPathId(dashboard, pathId); if (panel) { resolve(panel); } else { diff --git a/public/app/features/dashboard-scene/utils/clone.test.ts b/public/app/features/dashboard-scene/utils/clone.test.ts index 97b0e377e6a..6772c72e9a2 100644 --- a/public/app/features/dashboard-scene/utils/clone.test.ts +++ b/public/app/features/dashboard-scene/utils/clone.test.ts @@ -1,134 +1,10 @@ -import { - getCloneKey, - getOriginalKey, - isInCloneChain, - isClonedKey, - joinCloneKeys, - containsCloneKey, - getLastKeyFromClone, - isClonedKeyOf, -} from './clone'; +import { getCloneKey } from './clone'; describe('clone', () => { describe('getCloneKey', () => { it('should return the clone key', () => { - expect(getCloneKey('panel', 1)).toBe('panel-clone-1'); - expect(getCloneKey('panel-clone-2', 1)).toBe('panel-clone-1'); - }); - - it('should not alter ancestors', () => { - expect(getCloneKey('row-clone-1/panel', 2)).toBe('row-clone-1/panel-clone-2'); - expect(getCloneKey('tab-clone-0/row-clone-1/panel', 2)).toBe('tab-clone-0/row-clone-1/panel-clone-2'); - expect(getCloneKey('row-clone-1/panel-clone-3', 2)).toBe('row-clone-1/panel-clone-2'); - expect(getCloneKey('tab-clone-0/row-clone-1/panel-clone-3', 2)).toBe('tab-clone-0/row-clone-1/panel-clone-2'); - }); - }); - - describe('getOriginalKey', () => { - it('should return the original key', () => { - expect(getOriginalKey('panel')).toBe('panel'); - expect(getOriginalKey('panel-clone-1')).toBe('panel'); - expect(getOriginalKey('row-clone-1/panel-clone-2')).toBe('panel'); - expect(getOriginalKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe('panel'); - expect(getOriginalKey('panel-2-clone-3')).toBe('panel-2'); - expect(getOriginalKey('panel-2')).toBe('panel-2'); - }); - }); - - describe('isClonedKey', () => { - it('should return true for cloned keys', () => { - expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe(true); - expect(isClonedKey('row-clone-0/panel-clone-1')).toBe(true); - expect(isClonedKey('panel-clone-1')).toBe(true); - }); - - it('should return false for non-cloned keys', () => { - expect(isClonedKey('panel-clone-0')).toBe(false); - expect(isClonedKey('tab-clone-1/row-clone-2/panel-clone-0')).toBe(false); - expect(isClonedKey('row-clone-1/panel-clone-0')).toBe(false); - expect(isClonedKey('panel')).toBe(false); - expect(isClonedKey('tab-clone-1/row-clone-2/panel')).toBe(false); - expect(isClonedKey('row-clone-1/panel')).toBe(false); - }); - - it('should properly handle indexes containing 0', () => { - expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-0')).toBe(false); - expect(isClonedKey('row-clone-0/panel-clone-0')).toBe(false); - expect(isClonedKey('panel-clone-0')).toBe(false); - - expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-101')).toBe(true); - expect(isClonedKey('row-clone-0/panel-clone-101')).toBe(true); - expect(isClonedKey('panel-clone-1010')).toBe(true); - - expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-10')).toBe(true); - expect(isClonedKey('row-clone-0/panel-clone-100')).toBe(true); - expect(isClonedKey('panel-clone-1000')).toBe(true); - }); - }); - - describe('isClonedKeyOf', () => { - it('should return true for cloned keys', () => { - expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel-clone-2')).toBe(true); - expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel')).toBe(true); - expect(isClonedKeyOf('panel-clone-2', 'panel-clone-2')).toBe(true); - expect(isClonedKeyOf('panel-clone-2', 'panel')).toBe(true); - }); - - it('should return false for non-cloned keys', () => { - expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel2-clone-2')).toBe(false); - expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel2')).toBe(false); - expect(isClonedKeyOf('panel-clone-2', 'panel2-clone-2')).toBe(false); - expect(isClonedKeyOf('panel-clone-2', 'panel2')).toBe(false); - }); - }); - - describe('isInCloneChain', () => { - it('should return true for keys with cloned ancestors', () => { - expect(isInCloneChain('tab-clone-1/row-clone-0/panel-clone-0')).toBe(true); - expect(isInCloneChain('row-clone-0/row-clone-1/panel-clone-0')).toBe(true); - expect(isInCloneChain('row-clone-0/row-clone-0/panel-clone-1')).toBe(true); - expect(isInCloneChain('panel-clone-1')).toBe(true); - }); - - it('should return false for keys without cloned ancestors', () => { - expect(isInCloneChain('panel-clone-0')).toBe(false); - expect(isInCloneChain('row-clone-0/panel-clone-0')).toBe(false); - expect(isInCloneChain('tab-clone-0/row-clone-0/panel-clone-0')).toBe(false); - expect(isInCloneChain('panel')).toBe(false); - expect(isInCloneChain('tab-clone-0/row-clone-0/panel')).toBe(false); - expect(isInCloneChain('tab-clone-0/row/panel')).toBe(false); - expect(isInCloneChain('tab-clone-0/row/panel-0')).toBe(false); - expect(isInCloneChain('tab/row-clone-0/panel-0')).toBe(false); - expect(isInCloneChain('row-clone-0/panel')).toBe(false); - }); - }); - - describe('getLastKeyFromClone', () => { - it('should return the last key', () => { - expect(getLastKeyFromClone('tab-clone-1/row-clone-2/panel-clone-3')).toBe('panel-clone-3'); - expect(getLastKeyFromClone('row-clone-1/panel-clone-2')).toBe('panel-clone-2'); - expect(getLastKeyFromClone('row-clone-1/panel')).toBe('panel'); - expect(getLastKeyFromClone('panel')).toBe('panel'); - }); - }); - - describe('joinCloneKeys', () => { - it('should join keys with a separator', () => { - expect(joinCloneKeys('row', 'panel-clone-1')).toBe('row/panel-clone-1'); - }); - }); - - describe('containsCloneKey', () => { - it('should return true for keys with clone key', () => { - expect(containsCloneKey('row-clone-0/panel-clone-1')).toBe(true); - expect(containsCloneKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe(true); - expect(containsCloneKey('panel-clone-1')).toBe(true); - }); - - it('should return false for keys without clone key', () => { - expect(containsCloneKey('panel')).toBe(false); - expect(containsCloneKey('tab-0/row-1/panel-2')).toBe(false); - expect(containsCloneKey('row-1/panel-2')).toBe(false); + expect(getCloneKey('panel-1', 1)).toBe('panel-1-clone-1'); + expect(getCloneKey('panel-22', 1)).toBe('panel-22-clone-1'); }); }); }); diff --git a/public/app/features/dashboard-scene/utils/clone.ts b/public/app/features/dashboard-scene/utils/clone.ts index 2dc9fac27c3..9923357bf5d 100644 --- a/public/app/features/dashboard-scene/utils/clone.ts +++ b/public/app/features/dashboard-scene/utils/clone.ts @@ -7,13 +7,7 @@ import { VariableValueSingle, } from '@grafana/scenes'; -import { DashboardScene } from '../scene/DashboardScene'; - const CLONE_KEY = '-clone-'; -const CLONE_SEPARATOR = '/'; - -const CLONED_KEY_REGEX = new RegExp(`${CLONE_KEY}[1-9][0-9]*$`); -const ORIGINAL_REGEX = new RegExp(`${CLONE_KEY}\\d+$`); /** * Create or alter the last key for a key @@ -21,91 +15,21 @@ const ORIGINAL_REGEX = new RegExp(`${CLONE_KEY}\\d+$`); * @param index */ export function getCloneKey(key: string, index: number): string { - const parts = key.split(CLONE_SEPARATOR).slice(0, -1); - const lastKey = getOriginalKey(getLastKeyFromClone(key)); - return [...parts, `${lastKey}${CLONE_KEY}${index}`].join(CLONE_SEPARATOR); + return `${key}${CLONE_KEY}${index}`; } -/** - * Get the original key from a clone key - * @param key - */ -export function getOriginalKey(key: string): string { - return getLastKeyFromClone(key).replace(ORIGINAL_REGEX, ''); -} +export function isRepeatCloneOrChildOf(scene: SceneObject): boolean { + let obj: SceneObject | undefined = scene; -/** - * Checks if the last key is a clone key - * @param key - */ -export function isClonedKey(key: string): boolean { - return CLONED_KEY_REGEX.test(getLastKeyFromClone(key)); -} + do { + if ('repeatSourceKey' in obj.state && obj.state.repeatSourceKey) { + return true; + } -/** - * Checks if key1 is a clone of key2 - * @param key1 - * @param key2 - */ -export function isClonedKeyOf(key1: string, key2: string): boolean { - return isClonedKey(key1) && getOriginalKey(key1) === getOriginalKey(key2); -} + obj = obj.parent; + } while (obj); -/** - * Checks if the key or any of its ancestors are cloned - * @param key - */ -export function isInCloneChain(key: string): boolean { - return key.split(CLONE_SEPARATOR).some(isClonedKey); -} - -/** - * Get the last key from a clone key - * @param key - */ -export function getLastKeyFromClone(key: string): string { - return key.split(CLONE_SEPARATOR).pop() ?? ''; -} - -/** - * Join clone keys - * @param keys - */ -export function joinCloneKeys(...keys: string[]): string { - return keys.filter(Boolean).join(CLONE_SEPARATOR); -} - -/** - * Checks if a key contains the '-clone-' string - * @param key - */ -export function containsCloneKey(key: string): boolean { - return key.includes(CLONE_KEY); -} - -/** - * Useful hook for checking of a scene is a clone - * @param scene - */ -export function useIsClone(scene: SceneObject): boolean { - const { key } = scene.useState(); - return isClonedKey(key!); -} - -/** - * Useful hook for checking if a scene is in a clone chain - * @param scene - */ -export function useHasClonedParents(scene: SceneObject): boolean { - if (isClonedKey(scene.state.key!)) { - return true; - } - - if (!scene.parent || scene.parent instanceof DashboardScene) { - return false; - } - - return useHasClonedParents(scene.parent); + return false; } export function getLocalVariableValueSet( diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index bd838432be5..39b2f5b5c66 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -4,7 +4,6 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene } from '../scene/DashboardScene'; import { VizPanelLinks } from '../scene/PanelLinks'; -import { isClonedKey } from './clone'; import { getDashboardSceneFor, getLayoutManagerFor, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from './utils'; function getTimePicker(scene: DashboardScene) { @@ -37,12 +36,11 @@ export function getNextPanelId(scene: SceneObject): number { let max = 0; sceneGraph - .findAllObjects(scene.getRoot(), (obj) => obj instanceof VizPanel || obj instanceof SceneGridRow) + .findAllObjects( + scene.getRoot(), + (obj) => (obj instanceof VizPanel || obj instanceof SceneGridRow) && !obj.state.repeatSourceKey + ) .forEach((panel) => { - if (isClonedKey(panel.state.key!)) { - return; - } - const panelId = getPanelIdForVizPanel(panel); if (panelId > max) { max = panelId; diff --git a/public/app/features/dashboard-scene/utils/pathId.test.ts b/public/app/features/dashboard-scene/utils/pathId.test.ts new file mode 100644 index 00000000000..5b65accf85e --- /dev/null +++ b/public/app/features/dashboard-scene/utils/pathId.test.ts @@ -0,0 +1,98 @@ +import { + LocalValueVariable, + SceneGridItem, + SceneGridLayout, + SceneGridRow, + SceneVariableSet, + VizPanel, +} from '@grafana/scenes'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; + +import { findVizPanelByPathId } from './pathId'; + +describe('findVizPanelByPathId', () => { + it('should find correct panel', () => { + const { scene, panel1, repeatedPanel } = buildTestScene(); + + expect(findVizPanelByPathId(scene, 'panel-1')).toBe(panel1); + expect(findVizPanelByPathId(scene, 'US$pod1$panel-2')).toBe(repeatedPanel); + }); + + it('should find correct pane with legacy number only', () => { + const { scene, panel1 } = buildTestScene(); + + expect(findVizPanelByPathId(scene, '1')).toBe(panel1); + // This should not find anything + expect(findVizPanelByPathId(scene, '1$panel-1')).toBe(null); + }); + + it('should include local and parent local variable value', () => { + const { repeatedPanel } = buildTestScene(); + + expect(repeatedPanel.getPathId()).toBe('US$pod1$panel-2'); + }); +}); + +function buildTestScene() { + const panel1 = new VizPanel({ + title: 'Panel 1', + pluginId: 'table', + key: 'panel-1', + }); + + const repeatedPanel = new VizPanel({ + title: 'Panel 2', + pluginId: 'table', + key: 'panel-2', + $variables: new SceneVariableSet({ + variables: [new LocalValueVariable({ name: 'pod', value: 'pod1', text: 'pod1' })], + }), + }); + + const grid = new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'grid-item-1', + x: 0, + y: 0, + width: 24, + height: 10, + body: panel1, + }), + new SceneGridRow({ + key: 'row-1', + x: 0, + y: 10, + width: 24, + height: 1, + $variables: new SceneVariableSet({ + variables: [new LocalValueVariable({ name: 'datacenter', value: 'US', text: 'US' })], + }), + children: [ + new SceneGridItem({ + key: 'grid-item-2', + x: 0, + y: 11, + width: 24, + height: 5, + body: repeatedPanel, + }), + ], + }), + ], + }); + + const scene = new DashboardScene({ + title: 'My dashboard', + uid: 'dash-1', + tags: ['database', 'panel'], + meta: { + canEdit: true, + }, + body: new DefaultGridLayoutManager({ grid }), + }); + + return { scene, panel1, repeatedPanel }; +} diff --git a/public/app/features/dashboard-scene/utils/pathId.ts b/public/app/features/dashboard-scene/utils/pathId.ts new file mode 100644 index 00000000000..a77ec004cfd --- /dev/null +++ b/public/app/features/dashboard-scene/utils/pathId.ts @@ -0,0 +1,32 @@ +import { SceneObject, VizPanel, sceneGraph, PATH_ID_SEPARATOR } from '@grafana/scenes'; + +import { getVizPanelKeyForPanelId } from './utils'; + +export function findVizPanelByPathId(scene: SceneObject, pathId: string): VizPanel | null { + // Check if pathId is just an old legacy panel id + if (/^\d+$/.test(pathId)) { + pathId = getVizPanelKeyForPanelId(parseInt(pathId, 10)); + } + + const panel = sceneGraph.findObject(scene, (obj) => { + if (!(obj instanceof VizPanel)) { + return false; + } + + return pathId === obj.getPathId(); + }); + + if (panel) { + if (panel instanceof VizPanel) { + return panel; + } else { + throw new Error(`Found panel with key ${pathId} but it was not a VizPanel`); + } + } + + return null; +} + +export function containsPathIdSeparator(key: string): boolean { + return key.includes(PATH_ID_SEPARATOR); +} diff --git a/public/app/features/dashboard-scene/utils/urlBuilders.ts b/public/app/features/dashboard-scene/utils/urlBuilders.ts index f79f2e5fa7c..0658b0ff178 100644 --- a/public/app/features/dashboard-scene/utils/urlBuilders.ts +++ b/public/app/features/dashboard-scene/utils/urlBuilders.ts @@ -8,7 +8,7 @@ import { getQueryRunnerFor } from './utils'; export function getViewPanelUrl(vizPanel: VizPanel) { return locationUtil.getUrlForPartial(locationService.getLocation(), { - viewPanel: vizPanel.state.key, + viewPanel: vizPanel.getPathId(), editPanel: undefined, }); } diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 9db459df2ce..76cd899adf6 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -29,8 +29,6 @@ import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; -import { containsCloneKey, getLastKeyFromClone, getOriginalKey, isInCloneChain } from './clone'; - export const NEW_PANEL_HEIGHT = 8; export const NEW_PANEL_WIDTH = 12; @@ -44,7 +42,7 @@ export function getVizPanelKeyForPanelId(panelId: number) { } export function getPanelIdForVizPanel(panel: SceneObject): number { - return parseInt(getOriginalKey(panel.state.key!).replace('panel-', ''), 10); + return parseInt(panel.state.key!.replace('panel-', ''), 10); } /** @@ -98,102 +96,16 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP return null; } - -export function findOriginalVizPanelByKey(scene: SceneObject, key: string | undefined): VizPanel | null { - if (!key) { - return null; - } - - let panel: VizPanel | null = findOriginalVizPanelInternal(scene, key); - - if (panel) { - return panel; - } - - // Also try to find by panel id - const id = parseInt(key, 10); - if (isNaN(id)) { - return null; - } - - const panelId = getVizPanelKeyForPanelId(id); - panel = findVizPanelInternal(scene, panelId); - - if (panel) { - return panel; - } - - panel = findOriginalVizPanelInternal(scene, panelId); - - return panel; -} - -function findOriginalVizPanelInternal(scene: SceneObject, key: string | undefined): VizPanel | null { - if (!key) { - return null; - } - - const panel = sceneGraph.findObject(scene, (obj) => { - const objKey = obj.state.key!; - - // Compare the original keys - if (objKey === key || (!isInCloneChain(objKey) && getOriginalKey(objKey) === getOriginalKey(key))) { - return true; - } - - if (!(obj instanceof VizPanel)) { - return false; - } - - return false; - }); - - if (panel) { - if (panel instanceof VizPanel) { - return panel; - } else { - throw new Error(`Found panel with key ${key} but it was not a VizPanel`); - } - } - - return null; -} - export function findEditPanel(scene: SceneObject, key: string | undefined): VizPanel | null { if (!key) { return null; } - // First we try to find the non-cloned panel - // This means it is either in not in a repeat chain or every item in the chain is not a clone - let panel: SceneObject | null = findOriginalVizPanelByKey(scene, key); + let panel: SceneObject | null = findVizPanelByKey(scene, key); if (!panel || !panel.state.key) { return null; } - // Get the actual panel key, without any of the ancestors - const panelKey = getLastKeyFromClone(panel.state.key); - - // If the panel contains clone in the key, this means it's a repeated panel, and we need to find the original panel - if (containsCloneKey(panelKey)) { - // Get the original key of the panel that we are looking for - const originalPanelKey = getOriginalKey(panelKey); - // Start the search from the parent to avoid unnecessary checks - // The parent usually is the grid item where the referenced panel is also located - panel = sceneGraph.findObject(panel.parent ?? scene, (sceneObject) => { - if (!sceneObject.state.key || isInCloneChain(sceneObject.state.key)) { - return false; - } - - const currentLastKey = getLastKeyFromClone(sceneObject.state.key); - if (containsCloneKey(currentLastKey)) { - return false; - } - - return getOriginalKey(currentLastKey) === originalPanelKey; - }); - } - if (!(panel instanceof VizPanel)) { return null; } diff --git a/public/app/plugins/datasource/dashboard/datasource.ts b/public/app/plugins/datasource/dashboard/datasource.ts index 524b044fed0..e525a2bb69c 100644 --- a/public/app/plugins/datasource/dashboard/datasource.ts +++ b/public/app/plugins/datasource/dashboard/datasource.ts @@ -24,7 +24,7 @@ import { config } from '@grafana/runtime'; import { SceneDataProvider, SceneDataTransformer, SceneObject } from '@grafana/scenes'; import { activateSceneObjectAndParentTree, - findOriginalVizPanelByKey, + findVizPanelByKey, getVizPanelKeyForPanelId, } from 'app/features/dashboard-scene/utils/utils'; @@ -63,7 +63,7 @@ export class DashboardDatasource extends DataSourceApi { return of({ data: [] }); } - let sourcePanel = this.findSourcePanel(scene, panelId); + let sourcePanel = findVizPanelByKey(scene, getVizPanelKeyForPanelId(panelId)); if (!sourcePanel) { return of({ data: [], error: { message: 'Could not find source panel' } }); @@ -295,11 +295,6 @@ export class DashboardDatasource extends DataSourceApi { }; } - private findSourcePanel(scene: SceneObject, panelId: number) { - // We're trying to find the original panel, not a cloned one, since `panelId` alone cannot resolve clones - return findOriginalVizPanelByKey(scene, getVizPanelKeyForPanelId(panelId)); - } - private emitFirstLoadedDataIfMixedDS( requestId: string ): (source: Observable) => Observable { diff --git a/yarn.lock b/yarn.lock index a27155c3ac9..ab8f5754642 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3586,11 +3586,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:6.29.7": - version: 6.29.7 - resolution: "@grafana/scenes-react@npm:6.29.7" +"@grafana/scenes-react@npm:^6.30.0": + version: 6.30.0 + resolution: "@grafana/scenes-react@npm:6.30.0" dependencies: - "@grafana/scenes": "npm:6.29.7" + "@grafana/scenes": "npm:6.30.0" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3602,13 +3602,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/d3db42b8face33a871cc2b983b00890b4a7b9cf685b06dd02df730036f9c13f1cdbc0dcb578453a0ce29381bdceb26d2a07ae4be77a8f753677dde1560eb491f + checksum: 10/107b930aaf88945cbc51601443190357d9733bd3e9063fa7d2fd7496ad772e25f7cbddd34fd6a2adeb5ae85d39d03cecf8d2ad3d74a351e61fedabb17dfce82a languageName: node linkType: hard -"@grafana/scenes@npm:6.29.7": - version: 6.29.7 - resolution: "@grafana/scenes@npm:6.29.7" +"@grafana/scenes@npm:6.30.0, @grafana/scenes@npm:^6.30.0": + version: 6.30.0 + resolution: "@grafana/scenes@npm:6.30.0" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3628,7 +3628,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/a2bbda33c78cd568d6333e033e1b587917ee4ca00d8f69e3707197d6712beba1d1b9244f05259bc4cce7416b9ec67d24eddaa1e6073695e2b4a7af1a37253174 + checksum: 10/b1036a1d8c531b3e197c3de276c4fe4a7092fcd27e1fd350ff6530cabb8eb5436a6184840ef6a4e6747510326e58c9a73d54b0c36f8ee7d20f8fd6eaccea8d75 languageName: node linkType: hard @@ -18313,8 +18313,8 @@ __metadata: "@grafana/plugin-ui": "npm:0.10.9" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:6.29.7" - "@grafana/scenes-react": "npm:6.29.7" + "@grafana/scenes": "npm:^6.30.0" + "@grafana/scenes-react": "npm:^6.30.0" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/test-utils": "workspace:*"