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 8d3b8f91a75..2136356bad7 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -1,5 +1,6 @@ import { sceneGraph, SceneObject, SceneObjectBase, SceneObjectState, VariableDependencyConfig } from '@grafana/scenes'; import { t } from 'app/core/internationalization'; +import kbn from 'app/core/utils/kbn'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering'; @@ -72,6 +73,10 @@ export class RowItem return this.state.layout; } + public getSlug(): string { + return kbn.slugifyForUrl(sceneGraph.interpolate(this, this.state.title ?? 'Row')); + } + public switchLayout(layout: DashboardLayoutManager) { this.setState({ layout: this._layoutRestorer.getLayout(layout, this.state.layout) }); } diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx index df09763b00b..6bef7c0bf8d 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx @@ -1,5 +1,6 @@ import { SceneObjectState, SceneObjectBase, sceneGraph, VariableDependencyConfig, SceneObject } from '@grafana/scenes'; import { t } from 'app/core/internationalization'; +import kbn from 'app/core/utils/kbn'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { getDefaultVizPanel } from '../../utils/utils'; @@ -53,6 +54,10 @@ export class TabItem return this.state.layout; } + public getSlug(): string { + return kbn.slugifyForUrl(sceneGraph.interpolate(this, this.state.title ?? 'Tab')); + } + public switchLayout(layout: DashboardLayoutManager) { this.setState({ layout: this._layoutRestorer.getLayout(layout, this.state.layout) }); } diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx index e3b7d66afdd..f0b5c1080ed 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx @@ -13,10 +13,12 @@ export function TabItemRenderer({ model }: SceneComponentProps) { const { tabs, currentTabIndex } = parentLayout.useState(); const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text'); const { isSelected, onSelect, isSelectable } = useElementSelection(key); + const mySlug = model.getSlug(); + const urlKey = parentLayout.getUrlKey(); const myIndex = tabs.findIndex((tab) => tab === model); const isActive = myIndex === currentTabIndex; const location = useLocation(); - const href = textUtil.sanitize(locationUtil.getUrlForPartial(location, { tab: myIndex })); + const href = textUtil.sanitize(locationUtil.getUrlForPartial(location, { [urlKey]: mySlug })); return ( { + describe('url sync', () => { + it('when on top level', () => { + const tabsLayoutManager = new TabsLayoutManager({ + tabs: [new TabItem({ title: 'Performance' })], + }); + + const urlState = tabsLayoutManager.getUrlState(); + expect(urlState).toEqual({ dtab: 'performance' }); + }); + + it('when nested under row and parent tab', () => { + const innerMostTabs = new TabsLayoutManager({ + tabs: [new TabItem({ title: 'Performance' })], + }); + + new RowsLayoutManager({ + rows: [ + new RowItem({ + title: 'Overview', + layout: new TabsLayoutManager({ + tabs: [ + new TabItem({ + title: 'Frontend', + layout: innerMostTabs, + }), + ], + }), + }), + ], + }); + + const urlState = innerMostTabs.getUrlState(); + expect(urlState).toEqual({ + ['overview-frontend-dtab']: 'performance', + }); + }); + }); +}); 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 71af05bfce7..f4369290323 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -12,6 +12,7 @@ import { ObjectRemovedFromCanvasEvent, ObjectsReorderedOnCanvasEvent, } from '../../edit-pane/shared'; +import { RowItem } from '../layout-rows/RowItem'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -44,7 +45,7 @@ export class TabsLayoutManager extends SceneObjectBase i public readonly descriptor = TabsLayoutManager.descriptor; - protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] }); + protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: () => [this.getUrlKey()] }); public constructor(state: Partial) { super({ @@ -65,19 +66,23 @@ export class TabsLayoutManager extends SceneObjectBase i } public getUrlState() { - return { tab: this.state.currentTabIndex.toString() }; + const key = this.getUrlKey(); + return { [key]: this.getCurrentTab().getSlug() }; } public updateFromUrl(values: SceneObjectUrlValues) { - if (!values.tab) { + const key = this.getUrlKey(); + const urlValue = values[key]; + + if (!urlValue) { return; } - if (typeof values.tab === 'string') { - const tabIndex = parseInt(values.tab, 10); - if (this.state.tabs[tabIndex]) { - this.setState({ currentTabIndex: tabIndex }); - } else { - this.setState({ currentTabIndex: 0 }); + + if (typeof values[key] === 'string') { + // find tab with matching slug + const matchIndex = this.state.tabs.findIndex((tab) => tab.getSlug() === urlValue); + if (matchIndex !== -1) { + this.setState({ currentTabIndex: matchIndex }); } } } @@ -212,4 +217,24 @@ export class TabsLayoutManager extends SceneObjectBase i return new TabsLayoutManager({ tabs }); } + + getUrlKey(): string { + let parent = this.parent; + // Panel edit uses tab key already so we are using dtab here to not conflict + let key = 'dtab'; + + while (parent) { + if (parent instanceof TabItem) { + key = `${parent.getSlug()}-${key}`; + } + + if (parent instanceof RowItem) { + key = `${parent.getSlug()}-${key}`; + } + + parent = parent.parent; + } + + return key; + } }