Dashboards: Add tabs layout (#100127)
This commit is contained in:
@@ -53,7 +53,7 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<div className={tabsStyles.item}>
|
||||
<div className={cx(tabsStyles.item, className)}>
|
||||
<a
|
||||
{...commonProps}
|
||||
href={href}
|
||||
@@ -68,7 +68,7 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={tabsStyles.item}>
|
||||
<div className={cx(tabsStyles.item, className)}>
|
||||
<button
|
||||
{...commonProps}
|
||||
type="button"
|
||||
|
||||
@@ -595,6 +595,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
this.state.body.addNewRow();
|
||||
}
|
||||
|
||||
public onCreateNewTab() {
|
||||
this.state.body.addNewTab();
|
||||
}
|
||||
|
||||
public onCreateNewPanel(): VizPanel {
|
||||
const vizPanel = getDefaultVizPanel();
|
||||
|
||||
|
||||
@@ -194,6 +194,25 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
leftActions.push({
|
||||
group: 'add-panel',
|
||||
condition: isEditingAndShowingDashboard,
|
||||
render: () => (
|
||||
<Button
|
||||
key="add-tab-button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon="plus"
|
||||
fill="text"
|
||||
onClick={() => {
|
||||
dashboard.onCreateNewTab();
|
||||
}}
|
||||
data-testid={selectors.components.PageToolbar.itemButton('add_tab')}
|
||||
>
|
||||
<Trans i18nKey="dashboard.toolbar.add-tab">Tab</Trans>
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
leftActions.push({
|
||||
group: 'add-panel',
|
||||
condition: isEditingAndShowingDashboard,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getGridItemKeyForPanelId,
|
||||
getDashboardSceneFor,
|
||||
} from '../../utils/utils';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
@@ -177,6 +178,22 @@ export class DefaultGridLayoutManager
|
||||
return panels;
|
||||
}
|
||||
|
||||
public hasVizPanels(): boolean {
|
||||
for (const child of this.state.grid.state.children) {
|
||||
if (child instanceof DashboardGridItem) {
|
||||
return true;
|
||||
} else if (child instanceof SceneGridRow) {
|
||||
for (const rowChild of child.state.children) {
|
||||
if (rowChild instanceof DashboardGridItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public addNewRow(): SceneGridRow {
|
||||
const id = dashboardSceneGraph.getNextPanelId(this);
|
||||
|
||||
@@ -205,6 +222,17 @@ export class DefaultGridLayoutManager
|
||||
return row;
|
||||
}
|
||||
|
||||
public addNewTab() {
|
||||
const shouldAddTab = this.hasVizPanels();
|
||||
const tabsLayout = TabsLayoutManager.createFromLayout(this);
|
||||
|
||||
if (shouldAddTab) {
|
||||
tabsLayout.addNewTab();
|
||||
}
|
||||
|
||||
getDashboardSceneFor(this).switchLayout(tabsLayout);
|
||||
}
|
||||
|
||||
public editModeChanged(isEditing: boolean) {
|
||||
const updateResizeAndDragging = () => {
|
||||
this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing });
|
||||
|
||||
+28
-1
@@ -5,6 +5,7 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
|
||||
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
|
||||
import { ResponsiveGridItem } from './ResponsiveGridItem';
|
||||
@@ -101,12 +102,38 @@ export class ResponsiveGridLayoutManager
|
||||
return panels;
|
||||
}
|
||||
|
||||
public hasVizPanels(): boolean {
|
||||
for (const child of this.state.layout.state.children) {
|
||||
if (child instanceof ResponsiveGridItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public addNewRow() {
|
||||
const shouldAddRow = this.hasVizPanels();
|
||||
const rowsLayout = RowsLayoutManager.createFromLayout(this);
|
||||
rowsLayout.addNewRow();
|
||||
|
||||
if (shouldAddRow) {
|
||||
rowsLayout.addNewRow();
|
||||
}
|
||||
|
||||
getDashboardSceneFor(this).switchLayout(rowsLayout);
|
||||
}
|
||||
|
||||
public addNewTab() {
|
||||
const shouldAddTab = this.hasVizPanels();
|
||||
const tabsLayout = TabsLayoutManager.createFromLayout(this);
|
||||
|
||||
if (shouldAddTab) {
|
||||
tabsLayout.addNewTab();
|
||||
}
|
||||
|
||||
getDashboardSceneFor(this).switchLayout(tabsLayout);
|
||||
}
|
||||
|
||||
public getOptions(): OptionsPaneItemDescriptor[] {
|
||||
return getEditOptions(this);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ import { t } from 'app/core/internationalization';
|
||||
|
||||
import { isClonedKey } from '../../utils/clone';
|
||||
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
|
||||
import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
@@ -62,10 +64,31 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
return panels;
|
||||
}
|
||||
|
||||
public hasVizPanels(): boolean {
|
||||
for (const row of this.state.rows) {
|
||||
if (row.getLayout().hasVizPanels()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public addNewRow() {
|
||||
this.setState({ rows: [...this.state.rows, new RowItem()] });
|
||||
}
|
||||
|
||||
public addNewTab() {
|
||||
const shouldAddTab = this.hasVizPanels();
|
||||
const tabsLayout = TabsLayoutManager.createFromLayout(this);
|
||||
|
||||
if (shouldAddTab) {
|
||||
tabsLayout.addNewTab();
|
||||
}
|
||||
|
||||
getDashboardSceneFor(this).switchLayout(tabsLayout);
|
||||
}
|
||||
|
||||
public editModeChanged(isEditing: boolean) {
|
||||
this.state.rows.forEach((row) => row.getLayout().editModeChanged?.(isEditing));
|
||||
}
|
||||
@@ -87,9 +110,8 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
}
|
||||
|
||||
public removeRow(row: RowItem) {
|
||||
this.setState({
|
||||
rows: this.state.rows.filter((r) => r !== row),
|
||||
});
|
||||
const rows = this.state.rows.filter((r) => r !== row);
|
||||
this.setState({ rows: rows.length === 0 ? [new RowItem()] : rows });
|
||||
}
|
||||
|
||||
public static createEmpty(): RowsLayoutManager {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { SceneObjectState, SceneObjectBase, sceneGraph, VariableDependencyConfig, SceneObject } from '@grafana/scenes';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { BulkActionElement } from '../types/BulkActionElement';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { EditableDashboardElement } from '../types/EditableDashboardElement';
|
||||
import { LayoutParent } from '../types/LayoutParent';
|
||||
|
||||
import { getEditOptions, renderActions } from './TabItemEditor';
|
||||
import { TabItemRenderer } from './TabItemRenderer';
|
||||
import { TabItems } from './TabItems';
|
||||
import { TabsLayoutManager } from './TabsLayoutManager';
|
||||
|
||||
export interface TabItemState extends SceneObjectState {
|
||||
layout: DashboardLayoutManager;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export class TabItem
|
||||
extends SceneObjectBase<TabItemState>
|
||||
implements LayoutParent, BulkActionElement, EditableDashboardElement
|
||||
{
|
||||
public static Component = TabItemRenderer;
|
||||
|
||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||
statePaths: ['title'],
|
||||
});
|
||||
|
||||
public readonly isEditableDashboardElement = true;
|
||||
public readonly typeName = 'Tab';
|
||||
|
||||
constructor(state?: Partial<TabItemState>) {
|
||||
super({
|
||||
...state,
|
||||
title: state?.title ?? t('dashboard.tabs-layout.tab.new', 'New tab'),
|
||||
layout: state?.layout ?? ResponsiveGridLayoutManager.createEmpty(),
|
||||
});
|
||||
}
|
||||
|
||||
public getLayout(): DashboardLayoutManager {
|
||||
return this.state.layout;
|
||||
}
|
||||
|
||||
public switchLayout(layout: DashboardLayoutManager) {
|
||||
this.setState({ layout });
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
return getEditOptions(this);
|
||||
}
|
||||
|
||||
public renderActions(): ReactNode {
|
||||
return renderActions(this);
|
||||
}
|
||||
|
||||
public getParentLayout(): TabsLayoutManager {
|
||||
return sceneGraph.getAncestor(this, TabsLayoutManager);
|
||||
}
|
||||
|
||||
public onDelete() {
|
||||
const layout = sceneGraph.getAncestor(this, TabsLayoutManager);
|
||||
layout.removeTab(this);
|
||||
}
|
||||
|
||||
public createMultiSelectedElement(items: SceneObject[]): TabItems {
|
||||
return new TabItems(items.filter((item) => item instanceof TabItem));
|
||||
}
|
||||
|
||||
public onChangeTab() {
|
||||
this.getParentLayout().changeTab(this);
|
||||
}
|
||||
|
||||
public onChangeTitle(title: string) {
|
||||
this.setState({ title });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
|
||||
import { Button, Input } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
|
||||
export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[] {
|
||||
const tabOptions = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.tabs-layout.tab-options.title', 'Tab options'),
|
||||
id: 'tab-options',
|
||||
isOpenDefault: true,
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'),
|
||||
render: () => <TabTitleInput tab={model} />,
|
||||
})
|
||||
);
|
||||
}, [model]);
|
||||
|
||||
const { layout } = model.useState();
|
||||
const layoutOptions = useLayoutCategory(layout);
|
||||
|
||||
return [tabOptions, layoutOptions];
|
||||
}
|
||||
|
||||
export function renderActions(tab: TabItem): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="secondary" icon="copy" />
|
||||
<Button size="sm" variant="destructive" fill="outline" onClick={() => tab.onDelete()} icon="trash-alt" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TabTitleInput({ tab }: { tab: TabItem }) {
|
||||
const { title } = tab.useState();
|
||||
|
||||
return <Input value={title} onChange={(e) => tab.onChangeTitle(e.currentTarget.value)} />;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { Tab, useElementSelection } from '@grafana/ui';
|
||||
|
||||
import { isClonedKey } from '../../utils/clone';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
|
||||
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 dashboard = getDashboardSceneFor(model);
|
||||
const { isEditing } = dashboard.useState();
|
||||
const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text');
|
||||
const { isSelected, onSelect } = useElementSelection(key);
|
||||
|
||||
return (
|
||||
<Tab
|
||||
className={!isClone && isSelected ? 'dashboard-selected-element' : undefined}
|
||||
label={titleInterpolated}
|
||||
active={model === currentTab}
|
||||
onPointerDown={(evt) => {
|
||||
evt.stopPropagation();
|
||||
|
||||
if (isEditing) {
|
||||
if (isClone) {
|
||||
dashboard.state.editPane.clearSelection();
|
||||
} else {
|
||||
onSelect?.(evt);
|
||||
}
|
||||
}
|
||||
|
||||
parentLayout.changeTab(model);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { MultiSelectedEditableDashboardElement } from '../types/MultiSelectedEditableDashboardElement';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
import { getEditOptions, renderActions } from './TabItemsEditor';
|
||||
|
||||
export class TabItems implements MultiSelectedEditableDashboardElement {
|
||||
public readonly isMultiSelectedEditableDashboardElement = true;
|
||||
public readonly typeName = 'Tabs';
|
||||
public readonly key: string;
|
||||
|
||||
public constructor(private _tabs: TabItem[]) {
|
||||
this.key = uuidv4();
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
return getEditOptions(this);
|
||||
}
|
||||
|
||||
public renderActions(): ReactNode {
|
||||
return renderActions(this);
|
||||
}
|
||||
|
||||
public getTabs(): TabItem[] {
|
||||
return this._tabs;
|
||||
}
|
||||
|
||||
public onDelete() {
|
||||
this._tabs.forEach((tab) => tab.onDelete());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Button, Stack, Text } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { TabItems } from './TabItems';
|
||||
|
||||
export function getEditOptions(_model: TabItems): OptionsPaneCategoryDescriptor[] {
|
||||
const tabOptions = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.edit-pane.tab.multi-select.options-header', 'Multi-selected Tab options'),
|
||||
id: 'ms-tab-options',
|
||||
isOpenDefault: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return [tabOptions];
|
||||
}
|
||||
|
||||
export function renderActions(model: TabItems) {
|
||||
const tabs = model.getTabs();
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<Text>
|
||||
<Trans i18nKey="dashboard.edit-pane.tab.multi-select.selection-number">No. of tabs selected: </Trans>
|
||||
{tabs.length}
|
||||
</Text>
|
||||
<Stack direction="row">
|
||||
<Button size="sm" variant="secondary" icon="copy" />
|
||||
<Button size="sm" variant="destructive" fill="outline" onClick={() => model.onDelete()} icon="trash-alt" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
import { TabsLayoutManagerRenderer } from './TabsLayoutManagerRenderer';
|
||||
|
||||
interface TabsLayoutManagerState extends SceneObjectState {
|
||||
tabs: TabItem[];
|
||||
currentTab: TabItem;
|
||||
}
|
||||
|
||||
export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> implements DashboardLayoutManager {
|
||||
public static Component = TabsLayoutManagerRenderer;
|
||||
|
||||
public readonly isDashboardLayoutManager = true;
|
||||
|
||||
public static readonly descriptor = {
|
||||
get name() {
|
||||
return t('dashboard.tabs-layout.name', 'Tabs');
|
||||
},
|
||||
get description() {
|
||||
return t('dashboard.tabs-layout.description', 'Tabs layout');
|
||||
},
|
||||
id: 'tabs-layout',
|
||||
createFromLayout: TabsLayoutManager.createFromLayout,
|
||||
};
|
||||
|
||||
public readonly descriptor = TabsLayoutManager.descriptor;
|
||||
|
||||
public addPanel(vizPanel: VizPanel) {
|
||||
this.state.currentTab.getLayout().addPanel(vizPanel);
|
||||
}
|
||||
|
||||
public getVizPanels(): VizPanel[] {
|
||||
const panels: VizPanel[] = [];
|
||||
|
||||
for (const tab of this.state.tabs) {
|
||||
const innerPanels = tab.getLayout().getVizPanels();
|
||||
panels.push(...innerPanels);
|
||||
}
|
||||
|
||||
return panels;
|
||||
}
|
||||
|
||||
public hasVizPanels(): boolean {
|
||||
for (const tab of this.state.tabs) {
|
||||
if (tab.getLayout().hasVizPanels()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public addNewRow() {
|
||||
this.state.currentTab.getLayout().addNewRow();
|
||||
}
|
||||
|
||||
public addNewTab() {
|
||||
const currentTab = new TabItem();
|
||||
this.setState({ tabs: [...this.state.tabs, currentTab], currentTab });
|
||||
}
|
||||
|
||||
public editModeChanged(isEditing: boolean) {
|
||||
this.state.tabs.forEach((tab) => tab.getLayout().editModeChanged?.(isEditing));
|
||||
}
|
||||
|
||||
public activateRepeaters() {
|
||||
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 });
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredTab = this.state.tabs.filter((tab) => tab !== this.state.currentTab);
|
||||
const tabs = filteredTab.length === 0 ? [new TabItem()] : filteredTab;
|
||||
|
||||
this.setState({ tabs, currentTab: tabs[tabs.length - 1] });
|
||||
}
|
||||
|
||||
public changeTab(tab: TabItem) {
|
||||
this.setState({ currentTab: tab });
|
||||
}
|
||||
|
||||
public static createEmpty(): TabsLayoutManager {
|
||||
const tab = new TabItem();
|
||||
return new TabsLayoutManager({ tabs: [tab], currentTab: tab });
|
||||
}
|
||||
|
||||
public static createFromLayout(layout: DashboardLayoutManager): TabsLayoutManager {
|
||||
const tab = new TabItem({ layout: layout.clone() });
|
||||
return new TabsLayoutManager({ tabs: [tab], currentTab: tab });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { TabsLayoutManager } from './TabsLayoutManager';
|
||||
|
||||
export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLayoutManager>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { tabs, currentTab } = model.useState();
|
||||
const { layout } = currentTab.useState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabsBar className={styles.tabsContainer}>
|
||||
{tabs.map((tab) => (
|
||||
<tab.Component model={tab} key={tab.state.key!} />
|
||||
))}
|
||||
</TabsBar>
|
||||
<TabContent className={styles.tabContentContainer}>{layout && <layout.Component model={layout} />}</TabContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
tabsContainer: css({
|
||||
flexShrink: 1,
|
||||
padding: '2px 2px 0 2px',
|
||||
marginBottom: theme.spacing(1),
|
||||
}),
|
||||
tabContentContainer: css({
|
||||
backgroundColor: 'transparent',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
scrollbarWidth: 'thin',
|
||||
padding: '2px 2px 0 2px',
|
||||
}),
|
||||
});
|
||||
@@ -3,8 +3,14 @@ import { Registry } from '@grafana/data';
|
||||
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
export const layoutRegistry: Registry<LayoutRegistryItem> = new Registry<LayoutRegistryItem>(() => {
|
||||
return [DefaultGridLayoutManager.descriptor, ResponsiveGridLayoutManager.descriptor, RowsLayoutManager.descriptor];
|
||||
return [
|
||||
DefaultGridLayoutManager.descriptor,
|
||||
ResponsiveGridLayoutManager.descriptor,
|
||||
RowsLayoutManager.descriptor,
|
||||
TabsLayoutManager.descriptor,
|
||||
];
|
||||
});
|
||||
|
||||
@@ -34,15 +34,25 @@ export interface DashboardLayoutManager<S = {}> extends SceneObject {
|
||||
duplicatePanel?(panel: VizPanel): void;
|
||||
|
||||
/**
|
||||
* getVizPanels
|
||||
* Gets all the viz panels in the layout
|
||||
*/
|
||||
getVizPanels(): VizPanel[];
|
||||
|
||||
/**
|
||||
* Check if the layout has viz panels
|
||||
*/
|
||||
hasVizPanels(): boolean;
|
||||
|
||||
/**
|
||||
* Add row
|
||||
*/
|
||||
addNewRow(): void;
|
||||
|
||||
/**
|
||||
* Add tab
|
||||
*/
|
||||
addNewTab(): void;
|
||||
|
||||
/**
|
||||
* Notify the layout manager that the edit mode has changed
|
||||
* @param isEditing
|
||||
|
||||
@@ -969,6 +969,12 @@
|
||||
"options-header": "Multi-selected Row options",
|
||||
"selection-number": "No. of rows selected: {{length}}"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"multi-select": {
|
||||
"options-header": "Multi-selected Tab options",
|
||||
"selection-number": "No. of tabs selected: "
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
@@ -1094,11 +1100,23 @@
|
||||
"title-option": "Title"
|
||||
}
|
||||
},
|
||||
"tabs-layout": {
|
||||
"description": "Tabs layout",
|
||||
"name": "Tabs",
|
||||
"tab": {
|
||||
"new": "New tab"
|
||||
},
|
||||
"tab-options": {
|
||||
"title": "Tab options",
|
||||
"title-option": "Title"
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"add": "Add",
|
||||
"add-panel": "Panel",
|
||||
"add-panel-lib": "Import",
|
||||
"add-row": "Row",
|
||||
"add-tab": "Tab",
|
||||
"alert-rules": "Alert rules",
|
||||
"back-to-dashboard": "Back to dashboard",
|
||||
"dashboard-settings": {
|
||||
|
||||
@@ -969,6 +969,12 @@
|
||||
"options-header": "Mūľŧį-şęľęčŧęđ Ŗőŵ őpŧįőʼnş",
|
||||
"selection-number": "Ńő. őƒ řőŵş şęľęčŧęđ: {{length}}"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"multi-select": {
|
||||
"options-header": "Mūľŧį-şęľęčŧęđ Ŧäþ őpŧįőʼnş",
|
||||
"selection-number": "Ńő. őƒ ŧäþş şęľęčŧęđ: "
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
@@ -1094,11 +1100,23 @@
|
||||
"title-option": "Ŧįŧľę"
|
||||
}
|
||||
},
|
||||
"tabs-layout": {
|
||||
"description": "Ŧäþş ľäyőūŧ",
|
||||
"name": "Ŧäþş",
|
||||
"tab": {
|
||||
"new": "Ńęŵ ŧäþ"
|
||||
},
|
||||
"tab-options": {
|
||||
"title": "Ŧäþ őpŧįőʼnş",
|
||||
"title-option": "Ŧįŧľę"
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"add": "Åđđ",
|
||||
"add-panel": "Päʼnęľ",
|
||||
"add-panel-lib": "Ĩmpőřŧ",
|
||||
"add-row": "Ŗőŵ",
|
||||
"add-tab": "Ŧäþ",
|
||||
"alert-rules": "Åľęřŧ řūľęş",
|
||||
"back-to-dashboard": "ßäčĸ ŧő đäşĥþőäřđ",
|
||||
"dashboard-settings": {
|
||||
|
||||
Reference in New Issue
Block a user