The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx

246 lines
7.7 KiB

import React from 'react';
import {
SceneObjectState,
SceneObjectBase,
sceneGraph,
VariableDependencyConfig,
SceneObject,
VizPanel,
} from '@grafana/scenes';
import { TabsLayoutTabKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { LS_TAB_COPY_KEY } from 'app/core/constants';
import { appEvents } from 'app/core/core';
import { t } from 'app/core/internationalization';
import store from 'app/core/store';
import kbn from 'app/core/utils/kbn';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { ShowConfirmModalEvent } from 'app/types/events';
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
import { serializeTab } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
import { getElements } from '../../serialization/layoutSerializers/utils';
import { getDashboardSceneFor, getDefaultVizPanel } from '../../utils/utils';
import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager';
import { LayoutRestorer } from '../layouts-shared/LayoutRestorer';
import { clearClipboard } from '../layouts-shared/paste';
import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView';
import { BulkActionElement } from '../types/BulkActionElement';
import { DashboardDropTarget } from '../types/DashboardDropTarget';
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
import { LayoutParent } from '../types/LayoutParent';
import { useEditOptions } from './TabItemEditor';
import { TabItemRenderer } from './TabItemRenderer';
import { TabItemRepeaterBehavior } from './TabItemRepeaterBehavior';
import { TabItems } from './TabItems';
import { TabsLayoutManager } from './TabsLayoutManager';
export interface TabItemState extends SceneObjectState {
layout: DashboardLayoutManager;
title?: string;
isDropTarget?: boolean;
conditionalRendering?: ConditionalRendering;
}
export class TabItem
extends SceneObjectBase<TabItemState>
implements LayoutParent, BulkActionElement, EditableDashboardElement, DashboardDropTarget
{
public static Component = TabItemRenderer;
protected _variableDependency = new VariableDependencyConfig(this, {
statePaths: ['title'],
});
public readonly isEditableDashboardElement = true;
public readonly isDashboardDropTarget = true;
private _layoutRestorer = new LayoutRestorer();
public containerRef = React.createRef<HTMLDivElement>();
constructor(state?: Partial<TabItemState>) {
super({
...state,
title: state?.title ?? t('dashboard.tabs-layout.tab.new', 'New tab'),
layout: state?.layout ?? AutoGridLayoutManager.createEmpty(),
conditionalRendering: state?.conditionalRendering ?? ConditionalRendering.createEmpty(),
});
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
const deactivate = this.state.conditionalRendering?.activate();
return () => {
if (deactivate) {
deactivate();
}
};
}
public getEditableElementInfo(): EditableDashboardElementInfo {
return {
typeName: t('dashboard.edit-pane.elements.tab', 'Tab'),
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
icon: 'layers',
isContainer: true,
};
}
public getLayout(): DashboardLayoutManager {
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) });
}
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
return useEditOptions(this, isNewElement);
}
public onDelete() {
const layout = this.getParentLayout();
layout.removeTab(this);
}
public onConfirmDelete() {
const layout = this.getParentLayout();
if (layout.shouldUngroup()) {
layout.removeTab(this);
return;
}
if (this.getLayout().getVizPanels().length === 0) {
this.onDelete();
return;
}
appEvents.publish(
new ShowConfirmModalEvent({
title: t('dashboard.tabs-layout.delete-tab-title', 'Delete tab?'),
text: t(
'dashboard.tabs-layout.delete-tab-text',
'Deleting this tab will also remove all panels. Are you sure you want to continue?'
),
yesText: t('dashboard.tabs-layout.delete-tab-yes', 'Delete'),
onConfirm: () => {
this.onDelete();
},
})
);
}
public serialize(): TabsLayoutTabKind {
return serializeTab(this);
}
public onCopy() {
const elements = getElements(this.getLayout(), getDashboardSceneFor(this));
clearClipboard();
store.set(LS_TAB_COPY_KEY, JSON.stringify({ elements, tab: this.serialize() }));
}
public createMultiSelectedElement(items: SceneObject[]): TabItems {
return new TabItems(items.filter((item) => item instanceof TabItem));
}
public onDuplicate(): void {
this.getParentLayout().duplicateTab(this);
}
public duplicate(): TabItem {
return this.clone({ key: undefined, layout: this.getLayout().duplicate() });
}
public onAddPanel(panel = getDefaultVizPanel()) {
this.getLayout().addPanel(panel);
}
public onAddTab() {
this.getParentLayout().addNewTab();
}
public onChangeTitle(title: string) {
this.setState({ title });
}
public onChangeName(name: string): void {
this.onChangeTitle(name);
}
public onChangeRepeat(repeat: string | undefined) {
let repeatBehavior = this._getRepeatBehavior();
if (repeat) {
// Remove repeat behavior if it exists to trigger repeat when adding new one
if (repeatBehavior) {
repeatBehavior.removeBehavior();
}
repeatBehavior = new TabItemRepeaterBehavior({ variableName: repeat });
this.setState({ $behaviors: [...(this.state.$behaviors ?? []), repeatBehavior] });
repeatBehavior.activate();
} else {
repeatBehavior?.removeBehavior();
}
}
public setIsDropTarget(isDropTarget: boolean) {
if (!!this.state.isDropTarget !== isDropTarget) {
this.setState({ isDropTarget });
}
}
public draggedPanelOutside(panel: VizPanel) {
this.getLayout().removePanel?.(panel);
this.setIsDropTarget(false);
}
public draggedPanelInside(panel: VizPanel) {
panel.clearParent();
this.getLayout().addPanel(panel);
this.setIsDropTarget(false);
const parentLayout = this.getParentLayout();
const tabIndex = parentLayout.state.tabs.findIndex((tab) => tab === this);
if (tabIndex !== parentLayout.state.currentTabIndex) {
parentLayout.setState({ currentTabIndex: tabIndex });
}
}
public getRepeatVariable(): string | undefined {
return this._getRepeatBehavior()?.state.variableName;
}
public getParentLayout(): TabsLayoutManager {
return sceneGraph.getAncestor(this, TabsLayoutManager);
}
public scrollIntoView(): void {
const tabsLayout = this.getParentLayout();
if (tabsLayout.getCurrentTab() !== this) {
tabsLayout.switchToTab(this);
}
scrollCanvasElementIntoView(this, this.containerRef);
}
public hasUniqueTitle(): boolean {
const parentLayout = this.getParentLayout();
const duplicateTitles = parentLayout.duplicateTitles();
return !duplicateTitles.has(this.state.title);
}
private _getRepeatBehavior(): TabItemRepeaterBehavior | undefined {
return this.state.$behaviors?.find((b) => b instanceof TabItemRepeaterBehavior);
}
}