Tabs: Support for nested url sync (#102670)

This commit is contained in:
Torkel Ödegaard
2025-03-24 13:50:37 +01:00
committed by GitHub
parent 6f2a9abc03
commit c20de2b753
5 changed files with 92 additions and 10 deletions
@@ -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) });
}
@@ -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) });
}
@@ -13,10 +13,12 @@ export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
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 (
<Tab
@@ -0,0 +1,45 @@
import { RowItem } from '../layout-rows/RowItem';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { TabItem } from './TabItem';
import { TabsLayoutManager } from './TabsLayoutManager';
describe('TabsLayoutManager', () => {
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',
});
});
});
});
@@ -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<TabsLayoutManagerState> 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<TabsLayoutManagerState>) {
super({
@@ -65,19 +66,23 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> 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<TabsLayoutManagerState> 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;
}
}