TabsLayoutManager: Tab url sync rethink (#110274)

* Url sync for tabs rehink

* Update

* Thinks are working pretty well

* wip: solve tab not found

* fix lint and tests

* adjust wait for repeats in getCurrentTab

* set currentTabSlug in useEffect

* set currentTabSlug on tab title change

---------

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Sergej-Vlasov 2025-09-02 17:09:55 +03:00 committed by GitHub
parent d33f0e0941
commit 2a7fcd7d5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 130 additions and 114 deletions

View File

@ -175,6 +175,8 @@ export class TabItem
public onChangeTitle(title: string) { public onChangeTitle(title: string) {
this.setState({ title }); this.setState({ title });
const currentTabSlug = this.getSlug();
this.getParentLayout().setState({ currentTabSlug });
} }
public onChangeName(name: string): void { public onChangeName(name: string): void {
@ -202,13 +204,14 @@ export class TabItem
public draggedPanelInside(panel: VizPanel) { public draggedPanelInside(panel: VizPanel) {
panel.clearParent(); panel.clearParent();
this.getLayout().addPanel(panel); this.getLayout().addPanel(panel);
this.setIsDropTarget(false); this.setIsDropTarget(false);
const parentLayout = this.getParentLayout(); const parentLayout = this.getParentLayout();
const tabIndex = parentLayout.state.tabs.findIndex((tab) => tab === this);
if (tabIndex !== parentLayout.state.currentTabIndex) { if (parentLayout.state.currentTabSlug !== this.getSlug()) {
parentLayout.setState({ currentTabIndex: tabIndex }); parentLayout.setState({ currentTabSlug: this.getSlug() });
} }
} }

View File

@ -2,34 +2,36 @@ import { css, cx } from '@emotion/css';
import { Draggable } from '@hello-pangea/dnd'; import { Draggable } from '@hello-pangea/dnd';
import { useLocation } from 'react-router'; import { useLocation } from 'react-router';
import { locationUtil, textUtil } from '@grafana/data'; import { GrafanaTheme2, locationUtil, textUtil } from '@grafana/data';
import { t } from '@grafana/i18n'; import { t } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { Box, Icon, Tab, Tooltip, useElementSelection, usePointerDistance, useStyles2 } from '@grafana/ui'; import { Box, Icon, Tab, TabContent, Tooltip, useElementSelection, usePointerDistance, useStyles2 } from '@grafana/ui';
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden'; import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { isRepeatCloneOrChildOf } from '../../utils/clone';
import { useDashboardState } from '../../utils/utils'; import { useDashboardState } from '../../utils/utils';
import { useSoloPanelContext } from '../SoloPanelContext';
import { TabItem } from './TabItem'; import { TabItem } from './TabItem';
export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) { export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
const { title, key, isDropTarget } = model.useState(); const { title, key, isDropTarget, layout } = model.useState();
const parentLayout = model.getParentLayout(); const parentLayout = model.getParentLayout();
const { currentTabIndex } = parentLayout.useState(); const { currentTabSlug } = parentLayout.useState();
const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text'); const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text');
const { isSelected, onSelect, isSelectable } = useElementSelection(key); const { isSelected, onSelect, isSelectable } = useElementSelection(key);
const { isEditing } = useDashboardState(model); const { isEditing } = useDashboardState(model);
const mySlug = model.getSlug(); const mySlug = model.getSlug();
const urlKey = parentLayout.getUrlKey(); const urlKey = parentLayout.getUrlKey();
const myIndex = parentLayout.getTabs().findIndex((tab) => tab === model); const isActive = mySlug === currentTabSlug;
const isActive = myIndex === currentTabIndex; const myIndex = parentLayout.state.tabs.findIndex((tab) => tab === model);
const location = useLocation(); const location = useLocation();
const href = textUtil.sanitize(locationUtil.getUrlForPartial(location, { [urlKey]: mySlug })); const href = textUtil.sanitize(locationUtil.getUrlForPartial(location, { [urlKey]: mySlug }));
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const pointerDistance = usePointerDistance(); const pointerDistance = usePointerDistance();
const [isConditionallyHidden] = useIsConditionallyHidden(model); const [isConditionallyHidden] = useIsConditionallyHidden(model);
const isClone = isRepeatCloneOrChildOf(model); const isClone = isRepeatCloneOrChildOf(model);
const soloPanelContext = useSoloPanelContext();
const isDraggable = !isClone && isEditing; const isDraggable = !isClone && isEditing;
@ -37,6 +39,10 @@ export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
return null; return null;
} }
if (soloPanelContext) {
return <layout.Component model={layout} />;
}
let titleCollisionProps = {}; let titleCollisionProps = {};
if (!model.hasUniqueTitle()) { if (!model.hasUniqueTitle()) {
@ -107,7 +113,25 @@ function IsHiddenSuffix() {
); );
} }
const getStyles = () => ({ interface TabItemLayoutRendererProps {
tab: TabItem;
isEditing?: boolean;
}
export function TabItemLayoutRenderer({ tab, isEditing }: TabItemLayoutRendererProps) {
const { layout } = tab.useState();
const styles = useStyles2(getStyles);
const [_, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden(tab);
return (
<TabContent className={cx(styles.tabContentContainer, isEditing && conditionalRenderingClass)}>
<layout.Component model={layout} />
{isEditing && conditionalRenderingOverlay}
</TabContent>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
dragging: css({ dragging: css({
cursor: 'move', cursor: 'move',
}), }),
@ -118,4 +142,16 @@ const getStyles = () => ({
opacity: 1, opacity: 1,
}), }),
}), }),
tabContentContainer: css({
backgroundColor: 'transparent',
position: 'relative',
display: 'flex',
flexDirection: 'column',
flex: 1,
// Without this min height, the custom grid (SceneGridLayout) wont render
// Should be bigger than paddingTop value
// consist of paddingTop + 0.125 = 9px
minHeight: theme.spacing(1 + 0.125),
paddingTop: theme.spacing(1),
}),
}); });

View File

@ -98,6 +98,7 @@ export function performTabRepeats(variable: MultiValueVariable, tab: TabItem, co
const clonedTabs = createTabRepeats({ values, texts, variable, tab }); const clonedTabs = createTabRepeats({ values, texts, variable, tab });
tab.setState({ repeatedTabs: clonedTabs }); tab.setState({ repeatedTabs: clonedTabs });
tab.parent?.forceRender();
} }
/** /**

View File

@ -32,6 +32,9 @@ describe('TabsLayoutManager', () => {
tabs: [new TabItem({ title: 'Performance' })], tabs: [new TabItem({ title: 'Performance' })],
}); });
// currentTabSlug is set during rendering so forcing here
tabsLayoutManager.setState({ currentTabSlug: tabsLayoutManager.getCurrentTab()?.getSlug() });
const urlState = tabsLayoutManager.getUrlState(); const urlState = tabsLayoutManager.getUrlState();
expect(urlState).toEqual({ dtab: 'performance' }); expect(urlState).toEqual({ dtab: 'performance' });
}); });
@ -41,6 +44,9 @@ describe('TabsLayoutManager', () => {
tabs: [new TabItem({ title: 'Performance' })], tabs: [new TabItem({ title: 'Performance' })],
}); });
// currentTabSlug is set during rendering so forcing here
innerMostTabs.setState({ currentTabSlug: innerMostTabs.getCurrentTab()?.getSlug() });
new RowsLayoutManager({ new RowsLayoutManager({
rows: [ rows: [
new RowItem({ new RowItem({
@ -129,35 +135,19 @@ describe('TabsLayoutManager', () => {
lastUndo = undefined; lastUndo = undefined;
}); });
it('should remove a non-current tab without using removeElement', () => {
const manager = new TabsLayoutManager({ tabs: [] });
const tab1 = manager.addNewTab(new TabItem({ title: 'Tab 1' }));
const tab2 = manager.addNewTab(new TabItem({ title: 'Tab 2' }));
expect(manager.state.tabs).toHaveLength(2);
expect(manager.state.currentTabIndex).toBe(1); // tab2 is current
manager.removeTab(tab1);
expect(manager.state.tabs).toHaveLength(1);
expect(manager.state.tabs[0]).toBe(tab2);
expect(manager.state.currentTabIndex).toBe(0);
expect(dashboardEditActions.removeElement).not.toHaveBeenCalled();
});
it('should remove the current tab using removeElement', () => { it('should remove the current tab using removeElement', () => {
const manager = new TabsLayoutManager({ tabs: [] }); const manager = new TabsLayoutManager({ tabs: [] });
const tab1 = manager.addNewTab(new TabItem({ title: 'Tab 1' })); const tab1 = manager.addNewTab(new TabItem({ title: 'Tab 1' }));
const tab2 = manager.addNewTab(new TabItem({ title: 'Tab 2' })); const tab2 = manager.addNewTab(new TabItem({ title: 'Tab 2' }));
expect(manager.state.tabs).toHaveLength(2); expect(manager.state.tabs).toHaveLength(2);
expect(manager.state.currentTabIndex).toBe(1); // tab2 is current expect(manager.state.currentTabSlug).toBe(tab2.getSlug()); // tab2 is current
manager.removeTab(tab2); manager.removeTab(tab2);
expect(manager.state.tabs).toHaveLength(1); expect(manager.state.tabs).toHaveLength(1);
expect(manager.state.tabs[0]).toBe(tab1); expect(manager.state.tabs[0]).toBe(tab1);
expect(manager.state.currentTabIndex).toBe(0); expect(manager.state.currentTabSlug).toBe(tab1.getSlug());
expect(dashboardEditActions.removeElement).toHaveBeenCalled(); expect(dashboardEditActions.removeElement).toHaveBeenCalled();
}); });
@ -167,7 +157,7 @@ describe('TabsLayoutManager', () => {
const tab2 = manager.addNewTab(new TabItem({ title: 'Tab 2' })); const tab2 = manager.addNewTab(new TabItem({ title: 'Tab 2' }));
expect(manager.state.tabs).toHaveLength(2); expect(manager.state.tabs).toHaveLength(2);
expect(manager.state.currentTabIndex).toBe(1); // tab2 is current expect(manager.state.currentTabSlug).toBe(tab2.getSlug()); // tab2 is current
manager.removeTab(tab2); manager.removeTab(tab2);
@ -181,7 +171,7 @@ describe('TabsLayoutManager', () => {
expect(manager.state.tabs).toHaveLength(2); expect(manager.state.tabs).toHaveLength(2);
expect(manager.state.tabs).toContain(tab1); expect(manager.state.tabs).toContain(tab1);
expect(manager.state.tabs).toContain(tab2); expect(manager.state.tabs).toContain(tab2);
expect(manager.state.currentTabIndex).toBe(1); // tab2 should be current again expect(manager.state.currentTabSlug).toBe(tab2.getSlug()); // tab2 should be current again
}); });
}); });
@ -231,19 +221,18 @@ describe('TabsLayoutManager', () => {
const tab3 = manager.addNewTab(new TabItem({ title: 'Tab 3' })); const tab3 = manager.addNewTab(new TabItem({ title: 'Tab 3' }));
// Set tab2 as current // Set tab2 as current
manager.setState({ currentTabIndex: 1 }); manager.setState({ currentTabSlug: tab2.getSlug() });
expect(manager.state.currentTabIndex).toBe(1);
manager.moveTab(1, 0); manager.moveTab(1, 0);
expect(manager.state.tabs).toEqual([tab2, tab1, tab3]); expect(manager.state.tabs).toEqual([tab2, tab1, tab3]);
expect(manager.state.currentTabIndex).toBe(0); expect(manager.state.currentTabSlug).toBe(tab2.getSlug());
// Undo should restore the original state // Undo should restore the original state
lastUndo && lastUndo(); lastUndo && lastUndo();
expect(manager.state.tabs).toEqual([tab1, tab2, tab3]); expect(manager.state.tabs).toEqual([tab1, tab2, tab3]);
expect(manager.state.currentTabIndex).toBe(1); expect(manager.state.currentTabSlug).toBe(tab2.getSlug());
}); });
}); });
}); });

View File

@ -25,7 +25,7 @@ import { TabsLayoutManagerRenderer } from './TabsLayoutManagerRenderer';
interface TabsLayoutManagerState extends SceneObjectState { interface TabsLayoutManagerState extends SceneObjectState {
tabs: TabItem[]; tabs: TabItem[];
currentTabIndex: number; currentTabSlug?: string;
} }
export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> implements DashboardLayoutManager { export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> implements DashboardLayoutManager {
@ -58,7 +58,6 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
super({ super({
...state, ...state,
tabs: state.tabs ?? [new TabItem()], tabs: state.tabs ?? [new TabItem()],
currentTabIndex: state.currentTabIndex ?? 0,
}); });
} }
@ -74,7 +73,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
public getUrlState() { public getUrlState() {
const key = this.getUrlKey(); const key = this.getUrlKey();
return { [key]: this.getCurrentTab().getSlug() }; return { [key]: this.state.currentTabSlug };
} }
public updateFromUrl(values: SceneObjectUrlValues) { public updateFromUrl(values: SceneObjectUrlValues) {
@ -86,35 +85,49 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
} }
if (typeof values[key] === 'string') { if (typeof values[key] === 'string') {
// find tab with matching slug this.setState({ currentTabSlug: values[key] });
const matchIndex = this.getTabs().findIndex((tab) => tab.getSlug() === urlValue);
if (matchIndex !== -1) {
this.setState({ currentTabIndex: matchIndex });
}
} }
} }
public switchToTab(tab: TabItem) { public switchToTab(tab: TabItem) {
this.setState({ currentTabIndex: this.getTabs().indexOf(tab) }); this.setState({ currentTabSlug: tab.getSlug() });
} }
public getCurrentTab(): TabItem { public getCurrentTab(): TabItem | undefined {
return this.getTabs().length > this.state.currentTabIndex const tabs = this.getTabs();
? this.getTabs()[this.state.currentTabIndex] const selectedTab = tabs.find((tab) => tab.getSlug() === this.state.currentTabSlug);
: this.getTabs()[0]; if (selectedTab) {
return selectedTab;
}
// return undefined either if variable is loading or repeats were not processed yet
for (const tab of tabs) {
if (tab.state.repeatByVariable) {
const variable = sceneGraph.lookupVariable(tab.state.repeatByVariable, this);
if ((variable && variable.state.loading) || !tab.state.repeatedTabs) {
return;
}
}
}
// return first tab if no hits and variables finished loading
return tabs[0];
} }
public getTabs(): TabItem[] { public getTabs(): TabItem[] {
const tabsWithRepeats = this.state.tabs.reduce<TabItem[]>((acc, tab) => { return this.state.tabs.reduce<TabItem[]>((acc, tab) => {
acc.push(tab, ...(tab.state.repeatedTabs ?? [])); acc.push(tab, ...(tab.state.repeatedTabs ?? []));
return acc; return acc;
}, []); }, []);
return tabsWithRepeats;
} }
public addPanel(vizPanel: VizPanel) { public addPanel(vizPanel: VizPanel) {
this.getCurrentTab().getLayout().addPanel(vizPanel); const tab = this.getCurrentTab();
if (tab) {
tab.getLayout().addPanel(vizPanel);
}
} }
public getVizPanels(): VizPanel[] { public getVizPanels(): VizPanel[] {
@ -163,16 +176,12 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
dashboardEditActions.addElement({ dashboardEditActions.addElement({
addedObject: newTab, addedObject: newTab,
source: this, source: this,
perform: () => this.setState({ tabs: [...this.state.tabs, newTab], currentTabIndex: this.getTabs().length }), perform: () => this.setState({ tabs: [...this.state.tabs, newTab], currentTabSlug: newTab.getSlug() }),
undo: () => { undo: () => {
const indexOfNewTab = this.getTabs().findIndex((t) => t === newTab);
this.setState({ this.setState({
tabs: this.state.tabs.filter((t) => t !== newTab), tabs: this.state.tabs.filter((t) => t !== newTab),
// if the new tab was the current tab, set the current tab to the previous tab // if the new tab was the current tab, set the current tab to the previous tab
currentTabIndex: currentTabSlug: this.state.currentTabSlug === newTab.getSlug() ? undefined : this.state.currentTabSlug,
this.state.currentTabIndex === indexOfNewTab
? Math.max(0, this.state.currentTabIndex - 1)
: this.state.currentTabIndex,
}); });
}, },
}); });
@ -201,35 +210,29 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
return; return;
} }
const currentTab = this.getCurrentTab(); const tabIndex = this.state.tabs.findIndex((t) => t === tabToRemove);
if (currentTab === tabToRemove) { dashboardEditActions.removeElement({
const currentTabIndex = this.state.currentTabIndex; removedObject: tabToRemove,
const indexOfTabToRemove = this.state.tabs.findIndex((t) => t === tabToRemove); source: this,
const nextTabIndex = currentTabIndex > 0 ? currentTabIndex - 1 : 0; perform: () => {
const tabs = this.state.tabs;
const tabIndex = tabs.findIndex((t) => t === tabToRemove);
const newCurrentTabIndex = tabIndex > 0 ? tabIndex - 1 : 0;
dashboardEditActions.removeElement({ const newTabsState = tabs.filter((t) => t !== tabToRemove);
removedObject: tabToRemove,
source: this,
perform: () =>
this.setState({
tabs: this.state.tabs.filter((t) => t !== tabToRemove),
currentTabIndex: currentTabIndex === indexOfTabToRemove ? nextTabIndex : currentTabIndex,
}),
undo: () => {
const tabs = [...this.state.tabs];
tabs.splice(indexOfTabToRemove, 0, tabToRemove);
this.setState({ tabs, currentTabIndex });
},
});
return; this.setState({
} tabs: newTabsState,
currentTabSlug: newTabsState[newCurrentTabIndex]?.getSlug(),
const filteredTab = this.state.tabs.filter((tab) => tab !== tabToRemove); });
const tabs = filteredTab.length === 0 ? [new TabItem()] : filteredTab; },
undo: () => {
this.setState({ tabs, currentTabIndex: 0 }); const tabs = [...this.state.tabs];
tabs.splice(tabIndex, 0, tabToRemove);
this.setState({ tabs, currentTabSlug: tabToRemove.getSlug() });
},
});
} }
public moveTab(fromIndex: number, toIndex: number) { public moveTab(fromIndex: number, toIndex: number) {
@ -274,7 +277,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
const tabs = [...this.state.tabs]; const tabs = [...this.state.tabs];
const [removed] = tabs.splice(fromIndex, 1); const [removed] = tabs.splice(fromIndex, 1);
tabs.splice(toIndex, 0, removed); tabs.splice(toIndex, 0, removed);
this.setState({ tabs, currentTabIndex: selectedTabIndex }); this.setState({ tabs });
this.publishEvent(new ObjectsReorderedOnCanvasEvent(this), true); this.publishEvent(new ObjectsReorderedOnCanvasEvent(this), true);
} }
@ -288,7 +291,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
const editPane = getDashboardSceneFor(this).state.editPane; const editPane = getDashboardSceneFor(this).state.editPane;
editPane.selectObject(tab!, tabKey, { force: true, multi: false }); editPane.selectObject(tab!, tabKey, { force: true, multi: false });
this.setState({ currentTabIndex: tabIndex }); this.setState({ currentTabSlug: tab.getSlug() });
} }
public static createEmpty(): TabsLayoutManager { public static createEmpty(): TabsLayoutManager {

View File

@ -1,14 +1,13 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { DragDropContext, Droppable } from '@hello-pangea/dnd'; import { DragDropContext, Droppable } from '@hello-pangea/dnd';
import { useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { Trans } from '@grafana/i18n'; import { Trans } from '@grafana/i18n';
import { MultiValueVariable, SceneComponentProps, sceneGraph, useSceneObjectState } from '@grafana/scenes'; import { MultiValueVariable, SceneComponentProps, sceneGraph, useSceneObjectState } from '@grafana/scenes';
import { Button, TabContent, TabsBar, useStyles2 } from '@grafana/ui'; import { Button, TabsBar, useStyles2 } from '@grafana/ui';
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { isRepeatCloneOrChildOf } from '../../utils/clone';
import { getDashboardSceneFor } from '../../utils/utils'; import { getDashboardSceneFor } from '../../utils/utils';
import { useSoloPanelContext } from '../SoloPanelContext'; import { useSoloPanelContext } from '../SoloPanelContext';
@ -16,6 +15,7 @@ import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles';
import { useClipboardState } from '../layouts-shared/useClipboardState'; import { useClipboardState } from '../layouts-shared/useClipboardState';
import { TabItem } from './TabItem'; import { TabItem } from './TabItem';
import { TabItemLayoutRenderer } from './TabItemRenderer';
import { TabItemRepeater } from './TabItemRepeater'; import { TabItemRepeater } from './TabItemRepeater';
import { TabsLayoutManager } from './TabsLayoutManager'; import { TabsLayoutManager } from './TabsLayoutManager';
@ -23,16 +23,20 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { tabs, key } = model.useState(); const { tabs, key } = model.useState();
const currentTab = model.getCurrentTab(); const currentTab = model.getCurrentTab();
const { layout } = currentTab.useState();
const dashboard = getDashboardSceneFor(model); const dashboard = getDashboardSceneFor(model);
const { isEditing } = dashboard.useState(); const { isEditing } = dashboard.useState();
const { hasCopiedTab } = useClipboardState(); const { hasCopiedTab } = useClipboardState();
const [_, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden(currentTab);
const isNestedInTab = useMemo(() => model.parent instanceof TabItem, [model.parent]); const isNestedInTab = useMemo(() => model.parent instanceof TabItem, [model.parent]);
const soloPanelContext = useSoloPanelContext(); const soloPanelContext = useSoloPanelContext();
useEffect(() => {
if (currentTab && currentTab.getSlug() !== model.state.currentTabSlug) {
model.setState({ currentTabSlug: currentTab.getSlug() });
}
}, [currentTab, model]);
if (soloPanelContext) { if (soloPanelContext) {
return <layout.Component model={layout} />; return tabs.map((tab) => <TabWrapper tab={tab} manager={model} key={tab.state.key!} />);
} }
const isClone = isRepeatCloneOrChildOf(model); const isClone = isRepeatCloneOrChildOf(model);
@ -94,18 +98,7 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
</DragDropContext> </DragDropContext>
</TabsBar> </TabsBar>
{isEditing && ( {currentTab && <TabItemLayoutRenderer tab={currentTab} isEditing={isEditing} />}
<TabContent className={cx(styles.tabContentContainer, conditionalRenderingClass)}>
{currentTab && <layout.Component model={layout} />}
{conditionalRenderingOverlay}
</TabContent>
)}
{!isEditing && (
<TabContent className={styles.tabContentContainer}>
{currentTab && <layout.Component model={layout} />}
</TabContent>
)}
</div> </div>
); );
} }
@ -144,18 +137,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
paddingInline: theme.spacing(0.125), paddingInline: theme.spacing(0.125),
paddingTop: '1px', paddingTop: '1px',
}), }),
tabContentContainer: css({
backgroundColor: 'transparent',
position: 'relative',
display: 'flex',
flexDirection: 'column',
flex: 1,
// Without this min height, the custom grid (SceneGridLayout) wont render
// Should be bigger than paddingTop value
// consist of paddingTop + 0.125 = 9px
minHeight: theme.spacing(1 + 0.125),
paddingTop: theme.spacing(1),
}),
nestedTabsMargin: css({ nestedTabsMargin: css({
marginLeft: theme.spacing(2), marginLeft: theme.spacing(2),
}), }),

View File

@ -50,6 +50,9 @@ export function addNewRowTo(layout: DashboardLayoutManager): RowItem | SceneGrid
if (layout instanceof TabsLayoutManager) { if (layout instanceof TabsLayoutManager) {
const currentTab = layout.getCurrentTab(); const currentTab = layout.getCurrentTab();
if (!currentTab) {
throw new Error('Could find currently active tab');
}
return addNewRowTo(currentTab.state.layout); return addNewRowTo(currentTab.state.layout);
} }