mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
chore: high level restructure of canvas panel files (#69004)
This commit is contained in:
142
public/app/plugins/panel/canvas/editor/inline/InlineEdit.tsx
Normal file
142
public/app/plugins/panel/canvas/editor/inline/InlineEdit.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { SyntheticEvent, useEffect, useRef, useState } from 'react';
|
||||
import Draggable, { DraggableEventHandler } from 'react-draggable';
|
||||
import { Resizable, ResizeCallbackData } from 'react-resizable';
|
||||
|
||||
import { Dimensions2D, GrafanaTheme2 } from '@grafana/data';
|
||||
import { IconButton, Portal, useStyles2 } from '@grafana/ui';
|
||||
import store from 'app/core/store';
|
||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
|
||||
import { InlineEditBody } from './InlineEditBody';
|
||||
|
||||
type Props = {
|
||||
onClose?: () => void;
|
||||
id: number;
|
||||
scene: Scene;
|
||||
};
|
||||
|
||||
const OFFSET_X = 10;
|
||||
const OFFSET_Y = 32;
|
||||
|
||||
export function InlineEdit({ onClose, id, scene }: Props) {
|
||||
const root = scene.root.div?.getBoundingClientRect();
|
||||
const windowHeight = window.innerHeight;
|
||||
const windowWidth = window.innerWidth;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const styles = useStyles2(getStyles);
|
||||
const inlineEditKey = 'inlineEditPanel' + id.toString();
|
||||
|
||||
const defaultMeasurements = { width: 400, height: 400 };
|
||||
const widthOffset = root?.width ?? defaultMeasurements.width + OFFSET_X * 2;
|
||||
const defaultX = root?.x ?? 0 + widthOffset - defaultMeasurements.width - OFFSET_X;
|
||||
const defaultY = root?.y ?? 0 + OFFSET_Y;
|
||||
|
||||
const savedPlacement = store.getObject(inlineEditKey, {
|
||||
x: defaultX,
|
||||
y: defaultY,
|
||||
w: defaultMeasurements.width,
|
||||
h: defaultMeasurements.height,
|
||||
});
|
||||
const [measurements, setMeasurements] = useState<Dimensions2D>({ width: savedPlacement.w, height: savedPlacement.h });
|
||||
const [placement, setPlacement] = useState({ x: savedPlacement.x, y: savedPlacement.y });
|
||||
|
||||
// Checks that placement is within browser window
|
||||
useEffect(() => {
|
||||
const minX = windowWidth - measurements.width - OFFSET_X;
|
||||
const minY = windowHeight - measurements.height - OFFSET_Y;
|
||||
if (minX < placement.x && minX > 0) {
|
||||
setPlacement({ ...placement, x: minX });
|
||||
}
|
||||
if (minY < placement.y && minY > 0) {
|
||||
setPlacement({ ...placement, y: minY });
|
||||
}
|
||||
}, [windowHeight, windowWidth, placement, measurements]);
|
||||
|
||||
const onDragStop: DraggableEventHandler = (event, dragElement) => {
|
||||
let x = dragElement.x < 0 ? 0 : dragElement.x;
|
||||
let y = dragElement.y < 0 ? 0 : dragElement.y;
|
||||
|
||||
setPlacement({ x: x, y: y });
|
||||
saveToStore(x, y, measurements.width, measurements.height);
|
||||
};
|
||||
|
||||
const onResizeStop = (event: SyntheticEvent<Element, Event>, data: ResizeCallbackData) => {
|
||||
const { size } = data;
|
||||
setMeasurements({ width: size.width, height: size.height });
|
||||
saveToStore(placement.x, placement.y, size.width, size.height);
|
||||
};
|
||||
|
||||
const saveToStore = (x: number, y: number, width: number, height: number) => {
|
||||
store.setObject(inlineEditKey, { x: x, y: y, w: width, h: height });
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<div className={styles.draggableWrapper}>
|
||||
<Draggable handle="strong" onStop={onDragStop} position={{ x: placement.x, y: placement.y }}>
|
||||
<Resizable height={measurements.height} width={measurements.width} onResize={onResizeStop}>
|
||||
<div
|
||||
className={styles.inlineEditorContainer}
|
||||
style={{ height: `${measurements.height}px`, width: `${measurements.width}px` }}
|
||||
ref={ref}
|
||||
>
|
||||
<strong className={styles.inlineEditorHeader}>
|
||||
<div className={styles.placeholder} />
|
||||
<div>Canvas Inline Editor</div>
|
||||
<IconButton name="times" size="xl" className={styles.inlineEditorClose} onClick={onClose} />
|
||||
</strong>
|
||||
<div className={styles.inlineEditorContentWrapper}>
|
||||
<div className={styles.inlineEditorContent}>
|
||||
<InlineEditBody />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Resizable>
|
||||
</Draggable>
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
inlineEditorContainer: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: ${theme.components.panel.background};
|
||||
border: 1px solid ${theme.colors.border.strong};
|
||||
box-shadow: 5px 5px 20px -5px #000000;
|
||||
z-index: 1000;
|
||||
opacity: 1;
|
||||
min-width: 400px;
|
||||
`,
|
||||
draggableWrapper: css`
|
||||
width: 0;
|
||||
height: 0;
|
||||
`,
|
||||
inlineEditorHeader: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: ${theme.colors.background.canvas};
|
||||
border: 1px solid ${theme.colors.border.weak};
|
||||
height: 40px;
|
||||
cursor: move;
|
||||
`,
|
||||
inlineEditorContent: css`
|
||||
white-space: pre-wrap;
|
||||
padding: 10px;
|
||||
`,
|
||||
inlineEditorClose: css`
|
||||
margin-left: auto;
|
||||
`,
|
||||
placeholder: css`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
visibility: hidden;
|
||||
margin-right: auto;
|
||||
`,
|
||||
inlineEditorContentWrapper: css`
|
||||
overflow: scroll;
|
||||
`,
|
||||
});
|
||||
158
public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx
Normal file
158
public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { get as lodashGet } from 'lodash';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
|
||||
import { DataFrame, GrafanaTheme2, PanelOptionsEditorBuilder, StandardEditorContext } from '@grafana/data';
|
||||
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||
import { NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||
import { useStyles2 } from '@grafana/ui/src';
|
||||
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
||||
import { FrameState } from 'app/features/canvas/runtime/frame';
|
||||
import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { fillOptionsPaneItems } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
|
||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||
|
||||
import { activePanelSubject, InstanceState } from '../../CanvasPanel';
|
||||
import { addStandardCanvasEditorOptions } from '../../module';
|
||||
import { InlineEditTabs } from '../../types';
|
||||
import { getElementTypes, onAddItem } from '../../utils';
|
||||
import { getElementEditor } from '../element/elementEditor';
|
||||
import { getLayerEditor } from '../layer/layerEditor';
|
||||
|
||||
import { TabsEditor } from './TabsEditor';
|
||||
|
||||
export function InlineEditBody() {
|
||||
const activePanel = useObservable(activePanelSubject);
|
||||
const instanceState = activePanel?.panel.context?.instanceState;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<string>(InlineEditTabs.SelectedElement);
|
||||
|
||||
const pane = useMemo(() => {
|
||||
const p = activePanel?.panel;
|
||||
const state: InstanceState = instanceState;
|
||||
if (!state || !p) {
|
||||
return new OptionsPaneCategoryDescriptor({ id: 'root', title: 'root' });
|
||||
}
|
||||
|
||||
const supplier = (builder: PanelOptionsEditorBuilder<any>, context: StandardEditorContext<any>) => {
|
||||
if (activeTab === InlineEditTabs.ElementManagement) {
|
||||
builder.addNestedOptions(getLayerEditor(instanceState));
|
||||
}
|
||||
|
||||
const selection = state.selected;
|
||||
if (selection?.length === 1 && activeTab === InlineEditTabs.SelectedElement) {
|
||||
const element = selection[0];
|
||||
if (element && !(element instanceof FrameState)) {
|
||||
builder.addNestedOptions(
|
||||
getElementEditor({
|
||||
category: [`Selected element (${element.options.name})`],
|
||||
element,
|
||||
scene: state.scene,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
addStandardCanvasEditorOptions(builder);
|
||||
};
|
||||
|
||||
return getOptionsPaneCategoryDescriptor(
|
||||
{
|
||||
options: p.props.options,
|
||||
onChange: p.props.onOptionsChange,
|
||||
data: p.props.data?.series,
|
||||
},
|
||||
supplier
|
||||
);
|
||||
}, [instanceState, activePanel, activeTab]);
|
||||
|
||||
const topLevelItemsContainerStyle = {
|
||||
marginLeft: 15,
|
||||
marginTop: 10,
|
||||
};
|
||||
|
||||
const onTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
const typeOptions = getElementTypes(instanceState?.scene.shouldShowAdvancedTypes).options;
|
||||
const rootLayer: FrameState | undefined = instanceState?.layer;
|
||||
|
||||
const noElementSelected =
|
||||
instanceState && activeTab === InlineEditTabs.SelectedElement && instanceState.selected.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={topLevelItemsContainerStyle}>{pane.items.map((item) => item.render())}</div>
|
||||
<div style={topLevelItemsContainerStyle}>
|
||||
<AddLayerButton onChange={(sel) => onAddItem(sel, rootLayer)} options={typeOptions} label={'Add item'} />
|
||||
</div>
|
||||
<div style={topLevelItemsContainerStyle}>
|
||||
<TabsEditor onTabChange={onTabChange} />
|
||||
{pane.categories.map((p) => renderOptionsPaneCategoryDescriptor(p))}
|
||||
{noElementSelected && <div className={styles.selectElement}>Please select an element</div>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Recursively render options
|
||||
function renderOptionsPaneCategoryDescriptor(pane: OptionsPaneCategoryDescriptor) {
|
||||
return (
|
||||
<OptionsPaneCategory {...pane.props} key={pane.props.id}>
|
||||
<div>{pane.items.map((v) => v.render())}</div>
|
||||
{pane.categories.map((c) => renderOptionsPaneCategoryDescriptor(c))}
|
||||
</OptionsPaneCategory>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditorProps<T> {
|
||||
onChange: (v: T) => void;
|
||||
options: T;
|
||||
data?: DataFrame[];
|
||||
}
|
||||
|
||||
function getOptionsPaneCategoryDescriptor<T = any>(
|
||||
props: EditorProps<T>,
|
||||
supplier: PanelOptionsSupplier<T>
|
||||
): OptionsPaneCategoryDescriptor {
|
||||
const context: StandardEditorContext<unknown, unknown> = {
|
||||
data: props.data ?? [],
|
||||
options: props.options,
|
||||
};
|
||||
|
||||
const root = new OptionsPaneCategoryDescriptor({ id: 'root', title: 'root' });
|
||||
const getOptionsPaneCategory = (categoryNames?: string[]): OptionsPaneCategoryDescriptor => {
|
||||
if (categoryNames?.length) {
|
||||
const key = categoryNames[0];
|
||||
let sub = root.categories.find((v) => v.props.id === key);
|
||||
if (!sub) {
|
||||
sub = new OptionsPaneCategoryDescriptor({ id: key, title: key });
|
||||
root.categories.push(sub);
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
return root;
|
||||
};
|
||||
|
||||
const access: NestedValueAccess = {
|
||||
getValue: (path) => lodashGet(props.options, path),
|
||||
onChange: (path, value) => {
|
||||
props.onChange(setOptionImmutably(props.options as any, path, value));
|
||||
},
|
||||
};
|
||||
|
||||
// Use the panel options loader
|
||||
fillOptionsPaneItems(supplier, access, getOptionsPaneCategory, context);
|
||||
return root;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
selectElement: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
padding: ${theme.spacing(2)};
|
||||
`,
|
||||
});
|
||||
38
public/app/plugins/panel/canvas/editor/inline/TabsEditor.tsx
Normal file
38
public/app/plugins/panel/canvas/editor/inline/TabsEditor.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Tab, TabsBar } from '@grafana/ui/src';
|
||||
|
||||
import { InlineEditTabs } from '../../types';
|
||||
|
||||
type Props = {
|
||||
onTabChange: (v: string) => void;
|
||||
};
|
||||
|
||||
export const TabsEditor = ({ onTabChange }: Props) => {
|
||||
const [activeTab, setActiveTab] = useState<string>(InlineEditTabs.SelectedElement);
|
||||
|
||||
const tabs = [
|
||||
{ label: 'Selected Element', value: InlineEditTabs.SelectedElement },
|
||||
{ label: 'Element Management', value: InlineEditTabs.ElementManagement },
|
||||
];
|
||||
|
||||
const onCurrentTabChange = (value: string) => {
|
||||
onTabChange(value);
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabsBar>
|
||||
{tabs.map((t, index) => (
|
||||
<Tab
|
||||
key={`${t.value}-${index}`}
|
||||
label={t.label}
|
||||
active={t.value === activeTab}
|
||||
onChangeTab={() => onCurrentTabChange(t.value!)}
|
||||
/>
|
||||
))}
|
||||
</TabsBar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user