mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
New Editor: add display modes to fix ratio with actual display (#22032)
* add mode picker * ratio cleanup * add siebar toggle * use secondary button * use cog icon * cleanup * full to fill * Portaling for Select * Centering container for panel edit * Panel editor tabs Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
88922ea4ee
commit
e9c6c040da
@ -1,4 +1,4 @@
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css, cx } from 'emotion';
|
||||
@ -28,13 +28,13 @@ const getTabsBarStyles = stylesFactory((theme: GrafanaTheme, hideBorder = false)
|
||||
};
|
||||
});
|
||||
|
||||
export const TabsBar: FC<Props> = ({ children, className, hideBorder }) => {
|
||||
export const TabsBar = React.forwardRef<HTMLDivElement, Props>(({ children, className, hideBorder }, ref) => {
|
||||
const theme = useTheme();
|
||||
const tabsStyles = getTabsBarStyles(theme, hideBorder);
|
||||
|
||||
return (
|
||||
<div className={cx(tabsStyles.tabsWrapper, className)}>
|
||||
<div className={cx(tabsStyles.tabsWrapper, className)} ref={ref}>
|
||||
<ul className={tabsStyles.tabs}>{children}</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,28 +1,31 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { GrafanaTheme, FieldConfigSource, PanelData, LoadingState, DefaultTimeRange, PanelEvents } from '@grafana/data';
|
||||
import React, { PureComponent, CSSProperties } from 'react';
|
||||
import {
|
||||
stylesFactory,
|
||||
Forms,
|
||||
FieldConfigEditor,
|
||||
CustomScrollbar,
|
||||
selectThemeVariant,
|
||||
TabContent,
|
||||
Tab,
|
||||
TabsBar,
|
||||
} from '@grafana/ui';
|
||||
GrafanaTheme,
|
||||
FieldConfigSource,
|
||||
PanelData,
|
||||
LoadingState,
|
||||
DefaultTimeRange,
|
||||
PanelEvents,
|
||||
SelectableValue,
|
||||
} from '@grafana/data';
|
||||
import { stylesFactory, Forms, FieldConfigEditor, CustomScrollbar, selectThemeVariant } from '@grafana/ui';
|
||||
import { css, cx } from 'emotion';
|
||||
import config from 'app/core/config';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { DashboardPanel } from '../../dashgrid/DashboardPanel';
|
||||
import { QueriesTab } from '../../panel_editor/QueriesTab';
|
||||
|
||||
import SplitPane from 'react-split-pane';
|
||||
import { StoreState } from '../../../../types/store';
|
||||
import { connect } from 'react-redux';
|
||||
import { updateLocation } from '../../../../core/reducers/location';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
import { PanelTitle } from './PanelTitle';
|
||||
import { DisplayMode, displayModes } from './types';
|
||||
import { PanelEditorTabs } from './PanelEditorTabs';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const handleColor = selectThemeVariant(
|
||||
@ -54,7 +57,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
bottom: 0;
|
||||
background: ${theme.colors.pageBg};
|
||||
`,
|
||||
fill: css`
|
||||
panelWrapper: css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`,
|
||||
@ -89,21 +92,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
centeringContainer: css`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
enum EditorTab {
|
||||
Query = 'query',
|
||||
Alerts = 'alerts',
|
||||
Transform = 'xform',
|
||||
}
|
||||
|
||||
const allTabs = [
|
||||
{ tab: EditorTab.Query, label: 'Query', show: (panel: PanelModel) => true },
|
||||
{ tab: EditorTab.Alerts, label: 'Alerts', show: (panel: PanelModel) => true },
|
||||
{ tab: EditorTab.Transform, label: 'Transform', show: (panel: PanelModel) => true },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
dashboard: DashboardModel;
|
||||
sourcePanel: PanelModel;
|
||||
@ -114,7 +110,8 @@ interface State {
|
||||
pluginLoadedCounter: number;
|
||||
panel: PanelModel;
|
||||
data: PanelData;
|
||||
tab: EditorTab;
|
||||
mode: DisplayMode;
|
||||
showPanelOptions: boolean;
|
||||
}
|
||||
|
||||
export class PanelEditor extends PureComponent<Props, State> {
|
||||
@ -128,8 +125,9 @@ export class PanelEditor extends PureComponent<Props, State> {
|
||||
const panel = props.sourcePanel.getEditClone();
|
||||
this.state = {
|
||||
panel,
|
||||
tab: EditorTab.Query,
|
||||
pluginLoadedCounter: 0,
|
||||
mode: DisplayMode.Fill,
|
||||
showPanelOptions: true,
|
||||
data: {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
@ -260,40 +258,64 @@ export class PanelEditor extends PureComponent<Props, State> {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
renderBottomOptions() {
|
||||
onDiplayModeChange = (mode: SelectableValue<DisplayMode>) => {
|
||||
this.setState({
|
||||
mode: mode.value!,
|
||||
});
|
||||
};
|
||||
|
||||
onTogglePanelOptions = () => {
|
||||
this.setState({
|
||||
showPanelOptions: !this.state.showPanelOptions,
|
||||
});
|
||||
};
|
||||
|
||||
renderHorizontalSplit(styles: any) {
|
||||
const { dashboard } = this.props;
|
||||
const { panel, tab } = this.state;
|
||||
const { panel, mode } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TabsBar>
|
||||
{allTabs.map(t => {
|
||||
if (t.show(panel)) {
|
||||
<SplitPane
|
||||
split="horizontal"
|
||||
minSize={50}
|
||||
primary="second"
|
||||
defaultSize="40%"
|
||||
resizerClassName={styles.resizerH}
|
||||
onDragStarted={() => (document.body.style.cursor = 'row-resize')}
|
||||
onDragFinished={this.onDragFinished}
|
||||
>
|
||||
<div className={styles.panelWrapper}>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
if (width < 3 || height < 3) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tab
|
||||
label={t.label}
|
||||
active={tab === t.tab}
|
||||
onChangeTab={() => {
|
||||
this.setState({ tab: t.tab });
|
||||
}}
|
||||
/>
|
||||
<div className={styles.centeringContainer} style={{ width, height }}>
|
||||
<div style={calculatePanelSize(mode, width, height, panel)}>
|
||||
<DashboardPanel
|
||||
dashboard={dashboard}
|
||||
panel={panel}
|
||||
isEditing={false}
|
||||
isInEditMode
|
||||
isFullscreen={false}
|
||||
isInView={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</TabsBar>
|
||||
<TabContent>
|
||||
{tab === EditorTab.Query && <QueriesTab panel={panel} dashboard={dashboard} />}
|
||||
{tab === EditorTab.Alerts && <div>TODO: Show Alerts</div>}
|
||||
{tab === EditorTab.Transform && <div>TODO: Show Transform</div>}
|
||||
</TabContent>
|
||||
</div>
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<div className={styles.noScrollPaneContent}>
|
||||
<PanelEditorTabs panel={panel} dashboard={dashboard} />
|
||||
</div>
|
||||
</SplitPane>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard } = this.props;
|
||||
const { panel } = this.state;
|
||||
const { panel, mode, showPanelOptions } = this.state;
|
||||
const styles = getStyles(config.theme);
|
||||
|
||||
if (!panel) {
|
||||
@ -309,58 +331,70 @@ export class PanelEditor extends PureComponent<Props, State> {
|
||||
</button>
|
||||
<PanelTitle value={panel.title} onChange={this.onPanelTitleChange} />
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.toolbarLeft}>
|
||||
<Forms.Select
|
||||
value={displayModes.find(v => v.value === mode)}
|
||||
options={displayModes}
|
||||
onChange={this.onDiplayModeChange}
|
||||
/>
|
||||
<Forms.Button icon="fa fa-cog" variant="secondary" onClick={this.onTogglePanelOptions} />
|
||||
<Forms.Button variant="destructive" onClick={this.onDiscard}>
|
||||
Discard
|
||||
</Forms.Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.panes}>
|
||||
<SplitPane
|
||||
split="vertical"
|
||||
primary="second"
|
||||
minSize={50}
|
||||
defaultSize={350}
|
||||
resizerClassName={styles.resizerV}
|
||||
onDragStarted={() => (document.body.style.cursor = 'col-resize')}
|
||||
onDragFinished={this.onDragFinished}
|
||||
>
|
||||
{showPanelOptions ? (
|
||||
<SplitPane
|
||||
split="horizontal"
|
||||
split="vertical"
|
||||
minSize={100}
|
||||
primary="second"
|
||||
defaultSize="40%"
|
||||
resizerClassName={styles.resizerH}
|
||||
onDragStarted={() => (document.body.style.cursor = 'row-resize')}
|
||||
defaultSize={350}
|
||||
resizerClassName={styles.resizerV}
|
||||
onDragStarted={() => (document.body.style.cursor = 'col-resize')}
|
||||
onDragFinished={this.onDragFinished}
|
||||
>
|
||||
<div className={styles.fill}>
|
||||
<DashboardPanel
|
||||
dashboard={dashboard}
|
||||
panel={panel}
|
||||
isEditing={false}
|
||||
isInEditMode
|
||||
isFullscreen={false}
|
||||
isInView={true}
|
||||
/>
|
||||
{this.renderHorizontalSplit(styles)}
|
||||
<div className={styles.noScrollPaneContent}>
|
||||
<CustomScrollbar>
|
||||
<div style={{ padding: '10px' }}>
|
||||
{this.renderFieldOptions()}
|
||||
{this.renderVisSettings()}
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
<div className={styles.noScrollPaneContent}>{this.renderBottomOptions()}</div>
|
||||
</SplitPane>
|
||||
<div className={styles.noScrollPaneContent}>
|
||||
<CustomScrollbar>
|
||||
<div style={{ padding: '10px' }}>
|
||||
{this.renderFieldOptions()}
|
||||
{this.renderVisSettings()}
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
</SplitPane>
|
||||
) : (
|
||||
this.renderHorizontalSplit(styles)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function calculatePanelSize(mode: DisplayMode, width: number, height: number, panel: PanelModel): CSSProperties {
|
||||
if (mode === DisplayMode.Fill) {
|
||||
return { width, height };
|
||||
}
|
||||
const colWidth = (window.innerWidth - GRID_CELL_VMARGIN * 4) / GRID_COLUMN_COUNT;
|
||||
const pWidth = colWidth * panel.gridPos.w;
|
||||
const pHeight = GRID_CELL_HEIGHT * panel.gridPos.h;
|
||||
const scale = Math.min(width / pWidth, height / pHeight);
|
||||
|
||||
if (mode === DisplayMode.Exact && pWidth <= width && pHeight <= height) {
|
||||
return {
|
||||
width: pWidth,
|
||||
height: pHeight,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: pWidth * scale,
|
||||
height: pHeight * scale,
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
location: state.location,
|
||||
});
|
||||
|
@ -0,0 +1,70 @@
|
||||
import React, { useState } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import useMeasure from 'react-use/lib/useMeasure';
|
||||
import { TabsBar, Tab, stylesFactory, TabContent } from '@grafana/ui';
|
||||
import { EditorTab, allTabs } from './types';
|
||||
import { DashboardModel } from '../../state';
|
||||
import { QueriesTab } from '../../panel_editor/QueriesTab';
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
|
||||
interface PanelEditorTabsProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
const getPanelEditorTabsStyles = stylesFactory(() => {
|
||||
return {
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`,
|
||||
content: css`
|
||||
flex-grow: 1;
|
||||
`,
|
||||
};
|
||||
});
|
||||
export const PanelEditorTabs: React.FC<PanelEditorTabsProps> = ({ panel, dashboard }) => {
|
||||
const [activeTab, setActiveTab] = useState(EditorTab.Query);
|
||||
const [tabsBarRef, tabsBarMeasurements] = useMeasure();
|
||||
const styles = getPanelEditorTabsStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div>
|
||||
<TabsBar ref={tabsBarRef}>
|
||||
{allTabs.map(t => {
|
||||
if (t.show(panel)) {
|
||||
return (
|
||||
<Tab
|
||||
label={t.label}
|
||||
active={activeTab === t.tab}
|
||||
onChangeTab={() => {
|
||||
setActiveTab(t.tab);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</TabsBar>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<TabContent style={{ height: `calc(100% - ${tabsBarMeasurements.height}px)` }}>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
return (
|
||||
<div style={{ width, height }}>
|
||||
{activeTab === EditorTab.Query && <QueriesTab panel={panel} dashboard={dashboard} />}
|
||||
{activeTab === EditorTab.Alerts && <div>TODO: Show Alerts</div>}
|
||||
{activeTab === EditorTab.Transform && <div>TODO: Show Transform</div>}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</TabContent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
|
||||
export enum DisplayMode {
|
||||
Fill = 0,
|
||||
Fit = 1,
|
||||
Exact = 2,
|
||||
}
|
||||
|
||||
export const displayModes = [
|
||||
{ value: DisplayMode.Fill, label: 'Fill', description: 'Use all avaliable space' },
|
||||
{ value: DisplayMode.Fit, label: 'Fit', description: 'Fit in the space keeping ratio' },
|
||||
{ value: DisplayMode.Exact, label: 'Exact', description: 'Same size as the dashboard' },
|
||||
];
|
||||
|
||||
export enum EditorTab {
|
||||
Query = 'query',
|
||||
Alerts = 'alerts',
|
||||
Transform = 'xform',
|
||||
}
|
||||
|
||||
export const allTabs = [
|
||||
{ tab: EditorTab.Query, label: 'Query', show: (panel: PanelModel) => true },
|
||||
{ tab: EditorTab.Alerts, label: 'Alerts', show: (panel: PanelModel) => true },
|
||||
{ tab: EditorTab.Transform, label: 'Transform', show: (panel: PanelModel) => true },
|
||||
];
|
Loading…
Reference in New Issue
Block a user