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 { stylesFactory, useTheme } from '../../themes';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { css, cx } from 'emotion';
|
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 theme = useTheme();
|
||||||
const tabsStyles = getTabsBarStyles(theme, hideBorder);
|
const tabsStyles = getTabsBarStyles(theme, hideBorder);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(tabsStyles.tabsWrapper, className)}>
|
<div className={cx(tabsStyles.tabsWrapper, className)} ref={ref}>
|
||||||
<ul className={tabsStyles.tabs}>{children}</ul>
|
<ul className={tabsStyles.tabs}>{children}</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,28 +1,31 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent, CSSProperties } from 'react';
|
||||||
import { GrafanaTheme, FieldConfigSource, PanelData, LoadingState, DefaultTimeRange, PanelEvents } from '@grafana/data';
|
|
||||||
import {
|
import {
|
||||||
stylesFactory,
|
GrafanaTheme,
|
||||||
Forms,
|
FieldConfigSource,
|
||||||
FieldConfigEditor,
|
PanelData,
|
||||||
CustomScrollbar,
|
LoadingState,
|
||||||
selectThemeVariant,
|
DefaultTimeRange,
|
||||||
TabContent,
|
PanelEvents,
|
||||||
Tab,
|
SelectableValue,
|
||||||
TabsBar,
|
} from '@grafana/data';
|
||||||
} from '@grafana/ui';
|
import { stylesFactory, Forms, FieldConfigEditor, CustomScrollbar, selectThemeVariant } from '@grafana/ui';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import config from 'app/core/config';
|
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 { PanelModel } from '../../state/PanelModel';
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
import { DashboardModel } from '../../state/DashboardModel';
|
||||||
import { DashboardPanel } from '../../dashgrid/DashboardPanel';
|
import { DashboardPanel } from '../../dashgrid/DashboardPanel';
|
||||||
import { QueriesTab } from '../../panel_editor/QueriesTab';
|
|
||||||
import SplitPane from 'react-split-pane';
|
import SplitPane from 'react-split-pane';
|
||||||
import { StoreState } from '../../../../types/store';
|
import { StoreState } from '../../../../types/store';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { updateLocation } from '../../../../core/reducers/location';
|
import { updateLocation } from '../../../../core/reducers/location';
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
import { PanelTitle } from './PanelTitle';
|
import { PanelTitle } from './PanelTitle';
|
||||||
|
import { DisplayMode, displayModes } from './types';
|
||||||
|
import { PanelEditorTabs } from './PanelEditorTabs';
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
const handleColor = selectThemeVariant(
|
const handleColor = selectThemeVariant(
|
||||||
@ -54,7 +57,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: ${theme.colors.pageBg};
|
background: ${theme.colors.pageBg};
|
||||||
`,
|
`,
|
||||||
fill: css`
|
panelWrapper: css`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`,
|
`,
|
||||||
@ -89,21 +92,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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 {
|
interface Props {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
sourcePanel: PanelModel;
|
sourcePanel: PanelModel;
|
||||||
@ -114,7 +110,8 @@ interface State {
|
|||||||
pluginLoadedCounter: number;
|
pluginLoadedCounter: number;
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
data: PanelData;
|
data: PanelData;
|
||||||
tab: EditorTab;
|
mode: DisplayMode;
|
||||||
|
showPanelOptions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelEditor extends PureComponent<Props, State> {
|
export class PanelEditor extends PureComponent<Props, State> {
|
||||||
@ -128,8 +125,9 @@ export class PanelEditor extends PureComponent<Props, State> {
|
|||||||
const panel = props.sourcePanel.getEditClone();
|
const panel = props.sourcePanel.getEditClone();
|
||||||
this.state = {
|
this.state = {
|
||||||
panel,
|
panel,
|
||||||
tab: EditorTab.Query,
|
|
||||||
pluginLoadedCounter: 0,
|
pluginLoadedCounter: 0,
|
||||||
|
mode: DisplayMode.Fill,
|
||||||
|
showPanelOptions: true,
|
||||||
data: {
|
data: {
|
||||||
state: LoadingState.NotStarted,
|
state: LoadingState.NotStarted,
|
||||||
series: [],
|
series: [],
|
||||||
@ -260,40 +258,64 @@ export class PanelEditor extends PureComponent<Props, State> {
|
|||||||
this.forceUpdate();
|
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 { dashboard } = this.props;
|
||||||
const { panel, tab } = this.state;
|
const { panel, mode } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<SplitPane
|
||||||
<TabsBar>
|
split="horizontal"
|
||||||
{allTabs.map(t => {
|
minSize={50}
|
||||||
if (t.show(panel)) {
|
primary="second"
|
||||||
return (
|
defaultSize="40%"
|
||||||
<Tab
|
resizerClassName={styles.resizerH}
|
||||||
label={t.label}
|
onDragStarted={() => (document.body.style.cursor = 'row-resize')}
|
||||||
active={tab === t.tab}
|
onDragFinished={this.onDragFinished}
|
||||||
onChangeTab={() => {
|
>
|
||||||
this.setState({ tab: t.tab });
|
<div className={styles.panelWrapper}>
|
||||||
}}
|
<AutoSizer>
|
||||||
/>
|
{({ width, height }) => {
|
||||||
);
|
if (width < 3 || height < 3) {
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
})}
|
}
|
||||||
</TabsBar>
|
return (
|
||||||
<TabContent>
|
<div className={styles.centeringContainer} style={{ width, height }}>
|
||||||
{tab === EditorTab.Query && <QueriesTab panel={panel} dashboard={dashboard} />}
|
<div style={calculatePanelSize(mode, width, height, panel)}>
|
||||||
{tab === EditorTab.Alerts && <div>TODO: Show Alerts</div>}
|
<DashboardPanel
|
||||||
{tab === EditorTab.Transform && <div>TODO: Show Transform</div>}
|
dashboard={dashboard}
|
||||||
</TabContent>
|
panel={panel}
|
||||||
|
isEditing={false}
|
||||||
|
isInEditMode
|
||||||
|
isFullscreen={false}
|
||||||
|
isInView={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
|
<div className={styles.noScrollPaneContent}>
|
||||||
|
<PanelEditorTabs panel={panel} dashboard={dashboard} />
|
||||||
|
</div>
|
||||||
|
</SplitPane>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard } = this.props;
|
const { panel, mode, showPanelOptions } = this.state;
|
||||||
const { panel } = this.state;
|
|
||||||
const styles = getStyles(config.theme);
|
const styles = getStyles(config.theme);
|
||||||
|
|
||||||
if (!panel) {
|
if (!panel) {
|
||||||
@ -309,43 +331,30 @@ export class PanelEditor extends PureComponent<Props, State> {
|
|||||||
</button>
|
</button>
|
||||||
<PanelTitle value={panel.title} onChange={this.onPanelTitleChange} />
|
<PanelTitle value={panel.title} onChange={this.onPanelTitleChange} />
|
||||||
</div>
|
</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}>
|
<Forms.Button variant="destructive" onClick={this.onDiscard}>
|
||||||
Discard
|
Discard
|
||||||
</Forms.Button>
|
</Forms.Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.panes}>
|
<div className={styles.panes}>
|
||||||
|
{showPanelOptions ? (
|
||||||
<SplitPane
|
<SplitPane
|
||||||
split="vertical"
|
split="vertical"
|
||||||
|
minSize={100}
|
||||||
primary="second"
|
primary="second"
|
||||||
minSize={50}
|
|
||||||
defaultSize={350}
|
defaultSize={350}
|
||||||
resizerClassName={styles.resizerV}
|
resizerClassName={styles.resizerV}
|
||||||
onDragStarted={() => (document.body.style.cursor = 'col-resize')}
|
onDragStarted={() => (document.body.style.cursor = 'col-resize')}
|
||||||
onDragFinished={this.onDragFinished}
|
onDragFinished={this.onDragFinished}
|
||||||
>
|
>
|
||||||
<SplitPane
|
{this.renderHorizontalSplit(styles)}
|
||||||
split="horizontal"
|
|
||||||
minSize={100}
|
|
||||||
primary="second"
|
|
||||||
defaultSize="40%"
|
|
||||||
resizerClassName={styles.resizerH}
|
|
||||||
onDragStarted={() => (document.body.style.cursor = 'row-resize')}
|
|
||||||
onDragFinished={this.onDragFinished}
|
|
||||||
>
|
|
||||||
<div className={styles.fill}>
|
|
||||||
<DashboardPanel
|
|
||||||
dashboard={dashboard}
|
|
||||||
panel={panel}
|
|
||||||
isEditing={false}
|
|
||||||
isInEditMode
|
|
||||||
isFullscreen={false}
|
|
||||||
isInView={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.noScrollPaneContent}>{this.renderBottomOptions()}</div>
|
|
||||||
</SplitPane>
|
|
||||||
<div className={styles.noScrollPaneContent}>
|
<div className={styles.noScrollPaneContent}>
|
||||||
<CustomScrollbar>
|
<CustomScrollbar>
|
||||||
<div style={{ padding: '10px' }}>
|
<div style={{ padding: '10px' }}>
|
||||||
@ -355,12 +364,37 @@ export class PanelEditor extends PureComponent<Props, State> {
|
|||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
</div>
|
</div>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
|
) : (
|
||||||
|
this.renderHorizontalSplit(styles)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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) => ({
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
location: state.location,
|
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