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/edit-pane/DashboardEditPane.tsx

287 lines
7.8 KiB

import { css, cx } from '@emotion/css';
import { Resizable } from 're-resizable';
import { useEffect, useRef } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneObjectState, SceneObjectBase, SceneObject, sceneGraph, useSceneObjectState } from '@grafana/scenes';
import {
ElementSelectionContextItem,
ElementSelectionContextState,
Tab,
TabsBar,
ToolbarButton,
useStyles2,
} from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { isInCloneChain } from '../utils/clone';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardAddPane } from './DashboardAddPane';
import { DashboardOutline } from './DashboardOutline';
import { ElementEditPane } from './ElementEditPane';
import { ElementSelection } from './ElementSelection';
import { NewObjectAddedToCanvasEvent } from './shared';
import { useEditableElement } from './useEditableElement';
export interface DashboardEditPaneState extends SceneObjectState {
selection?: ElementSelection;
selectionContext: ElementSelectionContextState;
tab?: EditPaneTab;
}
export type EditPaneTab = 'add' | 'configure' | 'outline';
export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
public constructor() {
super({
selectionContext: {
enabled: false,
selected: [],
onSelect: (item, multi) => this.selectElement(item, multi),
},
});
this.addActivationHandler(this.onActivate.bind(this));
}
private onActivate() {
const dashboard = getDashboardSceneFor(this);
this._subs.add(
dashboard.subscribeToEvent(NewObjectAddedToCanvasEvent, ({ payload }) => {
this.newObjectAddedToCanvas(payload);
})
);
}
public enableSelection() {
// Enable element selection
this.setState({ selectionContext: { ...this.state.selectionContext, enabled: true } });
}
public disableSelection() {
this.setState({
selectionContext: { ...this.state.selectionContext, selected: [], enabled: false },
selection: undefined,
});
}
private selectElement(element: ElementSelectionContextItem, multi?: boolean) {
// We should not select clones
if (isInCloneChain(element.id)) {
if (multi) {
return;
}
this.clearSelection();
return;
}
const obj = sceneGraph.findByKey(this, element.id);
if (obj) {
this.selectObject(obj, element.id, multi);
}
}
public selectObject(obj: SceneObject, id: string, multi?: boolean) {
const prevItem = this.state.selection?.getFirstObject();
if (prevItem === obj && !multi) {
this.clearSelection();
return;
}
if (multi && this.state.selection?.hasValue(id)) {
this.removeMultiSelectedObject(id);
return;
}
const elementSelection = this.state.selection ?? new ElementSelection([[id, obj.getRef()]]);
const { selection, contextItems: selected } = elementSelection.getStateWithValue(id, obj, !!multi);
this.setState({
selection: new ElementSelection(selection),
selectionContext: {
...this.state.selectionContext,
selected,
},
});
}
private removeMultiSelectedObject(id: string) {
if (!this.state.selection) {
return;
}
const { entries, contextItems: selected } = this.state.selection.getStateWithoutValueAt(id);
if (entries.length === 0) {
this.clearSelection();
return;
}
this.setState({
selection: new ElementSelection([...entries]),
selectionContext: {
...this.state.selectionContext,
selected,
},
});
}
public clearSelection() {
if (!this.state.selection) {
return;
}
this.setState({
selection: undefined,
selectionContext: {
...this.state.selectionContext,
selected: [],
},
});
}
public onChangeTab = (tab: EditPaneTab) => {
this.setState({ tab });
};
private newObjectAddedToCanvas(obj: SceneObject) {
this.selectObject(obj, obj.state.key!, false);
if (this.state.tab !== 'configure') {
this.onChangeTab('configure');
}
}
}
export interface Props {
editPane: DashboardEditPane;
isCollapsed: boolean;
openOverlay?: boolean;
onToggleCollapse: () => void;
}
/**
* Making the EditPane rendering completely standalone (not using editPane.Component) in order to pass custom react props
*/
export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleCollapse, openOverlay }: Props) {
// Activate the edit pane
useEffect(() => {
editPane.enableSelection();
return () => {
editPane.disableSelection();
};
}, [editPane]);
useEffect(() => {
if (isCollapsed) {
editPane.clearSelection();
}
}, [editPane, isCollapsed]);
const { selection, tab = 'configure' } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
const styles = useStyles2(getStyles);
const paneRef = useRef<HTMLDivElement>(null);
const editableElement = useEditableElement(selection, editPane);
const selectedObject = selection?.getFirstObject();
if (!editableElement) {
return null;
}
if (isCollapsed) {
return (
<>
<div className={styles.expandOptionsWrapper}>
<ToolbarButton
tooltip={t('dashboard.edit-pane.open', 'Open options pane')}
icon="arrow-to-right"
onClick={onToggleCollapse}
variant="canvas"
className={styles.rotate180}
aria-label={t('dashboard.edit-pane.open', 'Open options pane')}
/>
</div>
{openOverlay && (
<Resizable className={cx(styles.fixed, styles.container)} defaultSize={{ height: '100%', width: '20vw' }}>
<ElementEditPane element={editableElement} key={selectedObject?.state.key} />
</Resizable>
)}
</>
);
}
return (
<div className={styles.wrapper} ref={paneRef}>
<TabsBar className={styles.tabsbar}>
<Tab
active={tab === 'add'}
label={t('dashboard.editpane.add', 'Add')}
onChangeTab={() => editPane.onChangeTab('add')}
/>
<Tab
active={tab === 'configure'}
label={t('dashboard.editpane.configure', 'Configure')}
onChangeTab={() => editPane.onChangeTab('configure')}
/>
<Tab
active={tab === 'outline'}
label={t('dashboard.editpane.outline', 'Outline')}
onChangeTab={() => editPane.onChangeTab('outline')}
/>
</TabsBar>
<div className={styles.tabContent}>
{tab === 'add' && <DashboardAddPane editPane={editPane} />}
{tab === 'configure' && <ElementEditPane element={editableElement} key={selectedObject?.state.key} />}
{tab === 'outline' && <DashboardOutline editPane={editPane} />}
</div>
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
wrapper: css({
display: 'flex',
flexDirection: 'column',
flex: '1 1 0',
}),
tabContent: css({
display: 'flex',
flex: '1 1 0',
flexDirection: 'column',
minHeight: 0,
overflow: 'auto',
}),
rotate180: css({
rotate: '180deg',
}),
tabsbar: css({
padding: theme.spacing(0, 1),
margin: theme.spacing(0.5, 0),
}),
expandOptionsWrapper: css({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(2, 1),
}),
// @ts-expect-error csstype doesn't allow !important. see https://github.com/frenic/csstype/issues/114
fixed: css({
position: 'absolute !important',
}),
container: css({
right: 0,
background: theme.colors.background.primary,
borderLeft: `1px solid ${theme.colors.border.weak}`,
boxShadow: theme.shadows.z3,
zIndex: theme.zIndex.navbarFixed,
overflowX: 'hidden',
overflowY: 'scroll',
}),
};
}