TabsLayout: Implements url sync and removes double scene object reference (#101115)
* TabsLayout: Implementts url sync and removes double scene object reference * Do not allow removing last tab * Update public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx Co-authored-by: Bogdan Matei <bogdan.matei@grafana.com> * Update public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx Co-authored-by: Bogdan Matei <bogdan.matei@grafana.com> * Update --------- Co-authored-by: Bogdan Matei <bogdan.matei@grafana.com>
This commit is contained in:
co-authored by
Bogdan Matei
parent
ba352af663
commit
436dc86a09
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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<TabItem>) {
|
||||
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 (
|
||||
<Tab
|
||||
className={!isClone && isSelected ? 'dashboard-selected-element' : undefined}
|
||||
label={titleInterpolated}
|
||||
active={model === currentTab}
|
||||
active={isActive}
|
||||
href={href}
|
||||
onPointerDown={(evt) => {
|
||||
evt.stopPropagation();
|
||||
|
||||
if (isEditing) {
|
||||
if (isClone) {
|
||||
dashboard.state.editPane.clearSelection();
|
||||
} else {
|
||||
onSelect?.(evt);
|
||||
}
|
||||
if (isEditing && isActive && !isClone) {
|
||||
evt.stopPropagation();
|
||||
onSelect?.(evt);
|
||||
}
|
||||
|
||||
parentLayout.changeTab(model);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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<TabsLayoutManagerState> implements DashboardLayoutManager {
|
||||
@@ -26,14 +32,42 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> 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<TabsLayoutManagerState>) {
|
||||
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<TabsLayoutManagerState> 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<TabsLayoutManagerState> 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] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import { TabsLayoutManager } from './TabsLayoutManager';
|
||||
|
||||
export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLayoutManager>) {
|
||||
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<TabsLay
|
||||
<tab.Component model={tab} key={tab.state.key!} />
|
||||
))}
|
||||
</TabsBar>
|
||||
<TabContent className={styles.tabContentContainer}>{layout && <layout.Component model={layout} />}</TabContent>
|
||||
<TabContent className={styles.tabContentContainer}>
|
||||
{currentTab && <layout.Component model={layout} />}
|
||||
</TabContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
+1
-8
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user