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 73ce7af6b19..4f4a2b45766 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx @@ -70,10 +70,6 @@ export class TabItem return new TabItems(items.filter((item) => item instanceof TabItem)); } - public onChangeTab() { - this.getParentLayout().changeTab(this); - } - public onChangeTitle(title: string) { this.setState({ title }); } 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 782fa9eeb3b..37c653ab354 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx @@ -1,5 +1,7 @@ import { useMemo } from 'react'; +import { useLocation } from 'react-router'; +import { locationUtil } from '@grafana/data'; import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; import { Tab, useElementSelection } from '@grafana/ui'; @@ -12,29 +14,27 @@ export function TabItemRenderer({ model }: SceneComponentProps) { const { title, key } = model.useState(); const isClone = useMemo(() => isClonedKey(key!), [key]); const parentLayout = model.getParentLayout(); - const { currentTab } = parentLayout.useState(); + const { tabs, currentTabIndex } = parentLayout.useState(); const dashboard = getDashboardSceneFor(model); const { isEditing } = dashboard.useState(); const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text'); const { isSelected, onSelect } = useElementSelection(key); + const myIndex = tabs.findIndex((tab) => tab === model); + const isActive = myIndex === currentTabIndex; + const location = useLocation(); + const href = locationUtil.getUrlForPartial(location, { tab: myIndex }); return ( { - evt.stopPropagation(); - - if (isEditing) { - if (isClone) { - dashboard.state.editPane.clearSelection(); - } else { - onSelect?.(evt); - } + if (isEditing && isActive && !isClone) { + evt.stopPropagation(); + onSelect?.(evt); } - - parentLayout.changeTab(model); }} /> ); 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 daa86286dae..3e2c338b26f 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -1,4 +1,10 @@ -import { SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { + SceneObjectBase, + SceneObjectState, + SceneObjectUrlSyncConfig, + SceneObjectUrlValues, + VizPanel, +} from '@grafana/scenes'; import { t } from 'app/core/internationalization'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; @@ -9,7 +15,7 @@ import { TabsLayoutManagerRenderer } from './TabsLayoutManagerRenderer'; interface TabsLayoutManagerState extends SceneObjectState { tabs: TabItem[]; - currentTab: TabItem; + currentTabIndex: number; } export class TabsLayoutManager extends SceneObjectBase implements DashboardLayoutManager { @@ -26,14 +32,42 @@ export class TabsLayoutManager extends SceneObjectBase i }, id: 'tabs-layout', createFromLayout: TabsLayoutManager.createFromLayout, - kind: 'TabsLayout', }; public readonly descriptor = TabsLayoutManager.descriptor; + protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] }); + + public constructor(state: Partial) { + super({ + ...state, + tabs: state.tabs ?? [new TabItem()], + currentTabIndex: state.currentTabIndex ?? 0, + }); + } + + public getUrlState() { + return { tab: this.state.currentTabIndex.toString() }; + } + + public updateFromUrl(values: SceneObjectUrlValues) { + if (!values.tab) { + return; + } + if (typeof values.tab === 'string') { + this.setState({ currentTabIndex: parseInt(values.tab, 10) }); + } + } + + public getCurrentTab(): TabItem { + return this.state.tabs.length > this.state.currentTabIndex + ? this.state.tabs[this.state.currentTabIndex] + : this.state.tabs[0]; + } + public addPanel(vizPanel: VizPanel) { - this.state.currentTab.getLayout().addPanel(vizPanel); + this.getCurrentTab().getLayout().addPanel(vizPanel); } public getVizPanels(): VizPanel[] { @@ -62,12 +96,12 @@ export class TabsLayoutManager extends SceneObjectBase i } public addNewRow() { - this.state.currentTab.getLayout().addNewRow(); + this.getCurrentTab().getLayout().addNewRow(); } public addNewTab() { const currentTab = new TabItem(); - this.setState({ tabs: [...this.state.tabs, currentTab], currentTab }); + this.setState({ tabs: [...this.state.tabs, currentTab], currentTabIndex: this.state.tabs.length }); } public editModeChanged(isEditing: boolean) { @@ -78,32 +112,33 @@ export class TabsLayoutManager extends SceneObjectBase i this.state.tabs.forEach((tab) => tab.getLayout().activateRepeaters?.()); } - public removeTab(tab: TabItem) { - if (this.state.currentTab === tab) { - const currentTabIndex = this.state.tabs.indexOf(tab); - const nextTabIndex = currentTabIndex === 0 ? 1 : currentTabIndex - 1; - const nextTab = this.state.tabs[nextTabIndex]; - this.setState({ tabs: this.state.tabs.filter((t) => t !== tab), currentTab: nextTab }); + public removeTab(tabToRemove: TabItem) { + // Do not allow removing last tab (for now) + if (this.state.tabs.length === 1) { return; } - const filteredTab = this.state.tabs.filter((tab) => tab !== this.state.currentTab); + const currentTab = this.getCurrentTab(); + + if (currentTab === tabToRemove) { + const nextTabIndex = this.state.currentTabIndex > 0 ? this.state.currentTabIndex - 1 : 0; + this.setState({ tabs: this.state.tabs.filter((t) => t !== tabToRemove), currentTabIndex: nextTabIndex }); + return; + } + + const filteredTab = this.state.tabs.filter((tab) => tab !== tabToRemove); const tabs = filteredTab.length === 0 ? [new TabItem()] : filteredTab; - this.setState({ tabs, currentTab: tabs[tabs.length - 1] }); - } - - public changeTab(tab: TabItem) { - this.setState({ currentTab: tab }); + this.setState({ tabs, currentTabIndex: 0 }); } public static createEmpty(): TabsLayoutManager { const tab = new TabItem(); - return new TabsLayoutManager({ tabs: [tab], currentTab: tab }); + return new TabsLayoutManager({ tabs: [tab] }); } public static createFromLayout(layout: DashboardLayoutManager): TabsLayoutManager { const tab = new TabItem({ layout: layout.clone() }); - return new TabsLayoutManager({ tabs: [tab], currentTab: tab }); + return new TabsLayoutManager({ tabs: [tab] }); } } 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 68d4840ae19..443ae6e1f1c 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,8 @@ import { TabsLayoutManager } from './TabsLayoutManager'; export function TabsLayoutManagerRenderer({ model }: SceneComponentProps) { const styles = useStyles2(getStyles); - const { tabs, currentTab } = model.useState(); + const { tabs, currentTabIndex } = model.useState(); + const currentTab = tabs[currentTabIndex]; const { layout } = currentTab.useState(); return ( @@ -18,7 +19,9 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps ))} - {layout && } + + {currentTab && } + ); } diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts index d6eb10803a1..d5093fb4663 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts @@ -44,6 +44,6 @@ export class TabsLayoutSerializer implements LayoutManagerSerializer { layout: layoutSerializerRegistry.get(layout.kind).serializer.deserialize(layout, elements, preload), }); }); - return new TabsLayoutManager({ tabs, currentTab: tabs[0] }); + return new TabsLayoutManager({ tabs }); } } diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index 4ff7c331b78..050565bc711 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -549,14 +549,7 @@ describe('dynamic layouts', () => { }), ]; - const scene = setupDashboardScene( - getMinimalSceneState( - new TabsLayoutManager({ - currentTab: tabs[0], - tabs, - }) - ) - ); + const scene = setupDashboardScene(getMinimalSceneState(new TabsLayoutManager({ tabs }))); const result = transformSceneToSaveModelSchemaV2(scene); expect(result.layout.kind).toBe('TabsLayout'); const tabsLayout = result.layout.spec as TabsLayoutSpec;