Commit df1d4316 by Torkel Ödegaard Committed by GitHub

NewPanelEditor: Panel editor tabs in state (url) (#22102)

* tabs & style tweaks

* Styling updates

* ok look

* tweaks

* Updated snapshots

* Moved transforms

* Updated
parent 8080bbc8
......@@ -123,7 +123,7 @@ export interface PluginConfigPage<T extends PluginMeta> {
export class GrafanaPlugin<T extends PluginMeta = PluginMeta> {
// Meta is filled in by the plugin loading system
meta?: T;
meta: T;
// This is set if the plugin system had errors loading the plugin
loadError?: boolean;
......@@ -142,4 +142,8 @@ export class GrafanaPlugin<T extends PluginMeta = PluginMeta> {
this.configPages.push(tab);
return this;
}
constructor() {
this.meta = {} as T;
}
}
......@@ -25,6 +25,7 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme) => {
flex-direction: row;
white-space: nowrap;
cursor: pointer;
border-left: 2px solid transparent;
&:hover {
background: ${optionBgHover};
}
......
......@@ -8,21 +8,16 @@ import {
DynamicConfigValue,
VariableSuggestionsScope,
} from '@grafana/data';
import {
standardFieldConfigEditorRegistry,
Forms,
fieldMatchersUI,
ControlledCollapse,
ValuePicker,
} from '@grafana/ui';
import { standardFieldConfigEditorRegistry, Forms, fieldMatchersUI, ValuePicker } from '@grafana/ui';
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
import { OptionsGroup } from './OptionsGroup';
interface Props {
config: FieldConfigSource;
custom?: FieldConfigEditorRegistry; // custom fields
include?: string[]; // Ordered list of which fields should be shown/included
onChange: (config: FieldConfigSource) => void;
// Helpful for IntelliSense
/* Helpful for IntelliSense */
data: DataFrame[];
}
......@@ -33,6 +28,7 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
private setDefaultValue = (name: string, value: any, custom: boolean) => {
const defaults = { ...this.props.config.defaults };
const remove = value === undefined || value === null || '';
if (custom) {
if (defaults.custom) {
if (remove) {
......@@ -236,17 +232,14 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
render() {
return (
<div>
<ControlledCollapse label="Standard Field Configuration" collapsible>
{this.renderStandardConfigs()}
</ControlledCollapse>
{this.props.custom && (
<ControlledCollapse label="Standard Field Configuration">{this.renderCustomConfigs()}</ControlledCollapse>
)}
<ControlledCollapse label="Field Overrides" collapsible>
<OptionsGroup title="Field configuration">{this.renderStandardConfigs()}</OptionsGroup>
{this.props.custom && <OptionsGroup title="Visualization options">{this.renderCustomConfigs()}</OptionsGroup>}
<OptionsGroup title="Field Overrides">
{this.renderOverrides()}
{this.renderAddOverride()}
</ControlledCollapse>
</OptionsGroup>
</div>
);
}
......
import React, { useState, FC } from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { useTheme, Icon, stylesFactory } from '@grafana/ui';
interface Props {
title: string;
}
export const OptionsGroup: FC<Props> = ({ title, children }) => {
const [isExpanded, toggleExpand] = useState(false);
const theme = useTheme();
const styles = getStyles(theme);
return (
<div className={styles.box}>
<div className={styles.header} onClick={() => toggleExpand(!isExpanded)}>
{title}
<div className={styles.toggle}>
<Icon name={isExpanded ? 'chevron-down' : 'chevron-left'} />
</div>
</div>
{isExpanded && <div className={styles.body}>{children}</div>}
</div>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
box: css`
border-bottom: 1px solid ${theme.colors.pageHeaderBorder};
`,
toggle: css`
font-size: ${theme.typography.size.lg};
`,
header: css`
display: flex;
cursor: pointer;
justify-content: space-between;
align-items: center;
padding: ${theme.spacing.sm} ${theme.spacing.md};
font-weight: ${theme.typography.weight.semibold};
`,
body: css`
padding: ${theme.spacing.md};
`,
};
});
import React, { PureComponent } from 'react';
import { GrafanaTheme, FieldConfigSource, PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant, ControlledCollapse } from '@grafana/ui';
import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant } from '@grafana/ui';
import { css, cx } from 'emotion';
import config from 'app/core/config';
import AutoSizer from 'react-virtualized-auto-sizer';
......@@ -15,7 +15,7 @@ import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux';
import { updateLocation } from '../../../../core/reducers/location';
import { Unsubscribable } from 'rxjs';
import { PanelTitle } from './PanelTitle';
import { DisplayMode, displayModes } from './types';
import { DisplayMode, displayModes, PanelEditorTab } from './types';
import { PanelEditorTabs } from './PanelEditorTabs';
import { DashNavTimeControls } from '../DashNav/DashNavTimeControls';
import { LocationState } from 'app/types';
......@@ -23,6 +23,8 @@ import { calculatePanelSize } from './utils';
import { initPanelEditor, panelEditorCleanUp } from './state/actions';
import { setDisplayMode, toggleOptionsView, setDiscardChanges } from './state/reducers';
import { FieldConfigEditor } from './FieldConfigEditor';
import { OptionsGroup } from './OptionsGroup';
import { getPanelEditorTabs } from './state/selectors';
interface OwnProps {
dashboard: DashboardModel;
......@@ -37,6 +39,7 @@ interface ConnectedProps {
mode: DisplayMode;
isPanelOptionsVisible: boolean;
initDone: boolean;
tabs: PanelEditorTab[];
}
interface DispatchProps {
......@@ -76,6 +79,10 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
});
};
onChangeTab = (tab: PanelEditorTab) => {
this.props.updateLocation({ query: { tab: tab.id }, partial: true });
};
onFieldConfigsChange = (fieldOptions: FieldConfigSource) => {
// NOTE: for now, assume this is from 'fieldOptions' -- TODO? put on panel model directly?
const { panel } = this.props;
......@@ -135,7 +142,10 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
onDragFinished = () => {
document.body.style.cursor = 'auto';
console.log('TODO, save splitter settings');
};
onDragStarted = () => {
document.body.style.cursor = 'row-resize';
};
onPanelTitleChange = (title: string) => {
......@@ -152,16 +162,17 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
};
renderHorizontalSplit(styles: any) {
const { dashboard, panel, mode } = this.props;
const { dashboard, panel, mode, tabs, data } = this.props;
return (
<SplitPane
split="horizontal"
minSize={50}
primary="second"
defaultSize="40%"
primary="first"
defaultSize="45%"
pane2Style={{ minHeight: 0 }}
resizerClassName={styles.resizerH}
onDragStarted={() => (document.body.style.cursor = 'row-resize')}
onDragStarted={this.onDragStarted}
onDragFinished={this.onDragFinished}
>
<div className={styles.panelWrapper}>
......@@ -188,7 +199,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
</AutoSizer>
</div>
<div className={styles.noScrollPaneContent}>
<PanelEditorTabs panel={panel} dashboard={dashboard} />
<PanelEditorTabs panel={panel} dashboard={dashboard} tabs={tabs} onChangeTab={this.onChangeTab} data={data} />
</div>
</SplitPane>
);
......@@ -212,22 +223,35 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
<PanelTitle value={panel.title} onChange={this.onPanelTitleChange} />
</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 className={styles.toolbarItem}>
<Forms.Button
className={styles.toolbarItem}
icon="fa fa-remove"
variant="destructive"
onClick={this.onDiscard}
/>
</div>
<div className={styles.toolbarItem}>
<Forms.Select
value={displayModes.find(v => v.value === mode)}
options={displayModes}
onChange={this.onDiplayModeChange}
/>
</div>
<div className={styles.toolbarItem}>
<Forms.Button
className={styles.toolbarItem}
icon="fa fa-sliders"
variant="secondary"
onClick={this.onTogglePanelOptions}
/>
</div>
<div>
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
</div>
</div>
</div>
<div className={styles.panes}>
<div className={styles.editorBody}>
{isPanelOptionsVisible ? (
<SplitPane
split="vertical"
......@@ -239,14 +263,10 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
onDragFinished={this.onDragFinished}
>
{this.renderHorizontalSplit(styles)}
<div className={styles.noScrollPaneContent}>
<div className={styles.panelOptionsPane}>
<CustomScrollbar>
<div style={{ padding: '10px' }}>
{this.renderFieldOptions()}
<ControlledCollapse label="Visualization Settings" collapsible>
{this.renderVisSettings()}
</ControlledCollapse>
</div>
{this.renderFieldOptions()}
<OptionsGroup title="Old settings">{this.renderVisSettings()}</OptionsGroup>
</CustomScrollbar>
</div>
</SplitPane>
......@@ -259,15 +279,20 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
}
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => ({
location: state.location,
plugin: state.plugins.panels[props.sourcePanel.type],
panel: state.panelEditorNew.getPanel(),
mode: state.panelEditorNew.mode,
isPanelOptionsVisible: state.panelEditorNew.isPanelOptionsVisible,
data: state.panelEditorNew.getData(),
initDone: state.panelEditorNew.initDone,
});
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
const plugin = state.plugins.panels[props.sourcePanel.type];
return {
location: state.location,
plugin: plugin,
panel: state.panelEditorNew.getPanel(),
mode: state.panelEditorNew.mode,
isPanelOptionsVisible: state.panelEditorNew.isPanelOptionsVisible,
data: state.panelEditorNew.getData(),
initDone: state.panelEditorNew.initDone,
tabs: getPanelEditorTabs(state.location, plugin),
};
};
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
updateLocation,
......@@ -284,20 +309,22 @@ export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEdi
* Styles
*/
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const handleColor = selectThemeVariant(
{
dark: theme.colors.dark9,
light: theme.colors.gray6,
},
theme.type
);
const handleColor = theme.colors.blueLight;
const background = selectThemeVariant({ light: theme.colors.white, dark: theme.colors.inputBlack }, theme.type);
const resizer = css`
padding: 3px;
font-style: italic;
background: ${theme.colors.panelBg};
background: transparent;
border-top: 0;
border-right: 0;
border-bottom: 0;
border-left: 0;
border-color: transparent;
border-style: solid;
transition: 0.2s border-color ease-in-out;
&:hover {
background: ${handleColor};
border-color: ${handleColor};
}
`;
......@@ -311,9 +338,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
left: 0;
right: 0;
bottom: 0;
background: ${theme.colors.pageBg};
background: ${background};
`,
panelWrapper: css`
padding: 0 2px 2px ${theme.spacing.sm};
width: 100%;
height: 100%;
`,
......@@ -321,33 +349,49 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
resizer,
css`
cursor: col-resize;
width: 8px;
border-right-width: 1px;
`
),
resizerH: cx(
resizer,
css`
height: 8px;
cursor: row-resize;
position: relative;
top: 49px;
z-index: 1;
border-top-width: 1px;
`
),
noScrollPaneContent: css`
height: 100%;
width: 100%;
overflow: hidden;
`,
panelOptionsPane: css`
height: 100%;
width: 100%;
background: ${theme.colors.pageBg};
border-top: 1px solid ${theme.colors.pageHeaderBorder};
border-left: 1px solid ${theme.colors.pageHeaderBorder};
`,
toolbar: css`
padding: ${theme.spacing.sm};
height: 48px;
height: 55px;
display: flex;
justify-content: space-between;
`,
panes: css`
height: calc(100% - 48px);
editorBody: css`
height: calc(100% - 55px);
position: relative;
`,
toolbarLeft: css`
display: flex;
align-items: center;
`,
toolbarItem: css`
margin-right: ${theme.spacing.sm};
`,
centeringContainer: css`
display: flex;
justify-content: center;
......
import React, { useState } from 'react';
import React from 'react';
import { config } from 'app/core/config';
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 { TabsBar, Tab, stylesFactory, TabContent, TransformationsEditor } from '@grafana/ui';
import { DataTransformerConfig, LoadingState, PanelData } from '@grafana/data';
import { PanelEditorTab, PanelEditorTabId } from './types';
import { DashboardModel } from '../../state';
import { QueriesTab } from '../../panel_editor/QueriesTab';
import { PanelModel } from '../../state/PanelModel';
......@@ -12,60 +12,69 @@ import { AlertTab } from 'app/features/alerting/AlertTab';
interface PanelEditorTabsProps {
panel: PanelModel;
dashboard: DashboardModel;
tabs: PanelEditorTab[];
onChangeTab: (tab: PanelEditorTab) => void;
data: PanelData;
}
export const PanelEditorTabs: React.FC<PanelEditorTabsProps> = ({ panel, dashboard, tabs, data, onChangeTab }) => {
const styles = getPanelEditorTabsStyles();
const activeTab = tabs.find(item => item.active);
if (tabs.length === 0) {
return null;
}
const onTransformersChange = (transformers: DataTransformerConfig[]) => {
panel.setTransformations(transformers);
};
return (
<div className={styles.wrapper}>
<TabsBar className={styles.tabBar}>
{tabs.map(tab => {
return <Tab key={tab.id} label={tab.text} active={tab.active} onChangeTab={() => onChangeTab(tab)} />;
})}
</TabsBar>
<TabContent className={styles.tabContent}>
{activeTab.id === PanelEditorTabId.Queries && <QueriesTab panel={panel} dashboard={dashboard} />}
{activeTab.id === PanelEditorTabId.Alert && <AlertTab panel={panel} dashboard={dashboard} />}
{activeTab.id === PanelEditorTabId.Transform && data.state !== LoadingState.NotStarted && (
<TransformationsEditor
transformations={panel.transformations || []}
onChange={onTransformersChange}
dataFrames={data.series}
/>
)}
</TabContent>
</div>
);
};
const getPanelEditorTabsStyles = stylesFactory(() => {
const { theme } = config;
return {
wrapper: css`
display: flex;
flex-direction: column;
height: 100%;
`,
content: css`
tabBar: css`
padding: 0 ${theme.spacing.sm};
`,
tabContent: css`
padding: 0;
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 0;
background: ${theme.colors.pageBg};
border-right: 1px solid ${theme.colors.pageHeaderBorder};
.toolbar {
background: transparent;
}
`,
};
});
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 && <AlertTab panel={panel} dashboard={dashboard} />}
{activeTab === EditorTab.Transform && <div>TODO: Show Transform</div>}
</div>
);
}}
</AutoSizer>
</TabContent>
</div>
</div>
);
};
import memoizeOne from 'memoize-one';
import { LocationState } from 'app/types';
import { PanelPlugin } from '@grafana/data';
import { PanelEditorTab, PanelEditorTabId } from '../types';
export const getPanelEditorTabs = memoizeOne((location: LocationState, plugin?: PanelPlugin) => {
const tabs: PanelEditorTab[] = [];
if (!plugin) {
return tabs;
}
let defaultTab = PanelEditorTabId.Visualization;
if (!plugin.meta.skipDataQuery) {
defaultTab = PanelEditorTabId.Queries;
tabs.push({
id: PanelEditorTabId.Queries,
text: 'Queries',
active: false,
});
tabs.push({
id: PanelEditorTabId.Transform,
text: 'Transform',
active: false,
});
}
tabs.push({
id: PanelEditorTabId.Visualization,
text: 'Visualization',
active: false,
});
if (plugin.meta.id === 'graph') {
tabs.push({
id: PanelEditorTabId.Alert,
text: 'Alert',
active: false,
});
}
const activeTab = tabs.find(item => item.id === (location.query.tab || defaultTab));
activeTab.active = true;
return tabs;
});
import { PanelModel } from '../../state/PanelModel';
export interface PanelEditorTab {
id: string;
text: string;
active: boolean;
}
export enum PanelEditorTabId {
Queries = 'queries',
Transform = 'transform',
Visualization = 'visualization',
Alert = 'alert',
}
export enum DisplayMode {
Fill = 0,
......@@ -11,15 +22,3 @@ export const displayModes = [
{ 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 },
];
// Libraries
import React, { PureComponent } from 'react';
import _ from 'lodash';
import { css } from 'emotion';
// Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { QueryInspector } from './QueryInspector';
import { QueryOptions } from './QueryOptions';
import { PanelOptionsGroup, TransformationsEditor, AlphaNotice } from '@grafana/ui';
import { PanelOptionsGroup } from '@grafana/ui';
import { QueryEditorRows } from './QueryEditorRows';
// Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
......@@ -16,15 +15,7 @@ import config from 'app/core/config';
// Types
import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel';
import {
LoadingState,
DataTransformerConfig,
DefaultTimeRange,
DataSourceSelectItem,
DataQuery,
PanelData,
PluginState,
} from '@grafana/data';
import { LoadingState, DefaultTimeRange, DataSourceSelectItem, DataQuery, PanelData } from '@grafana/data';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { addQuery } from 'app/core/utils/query';
import { Unsubscribable } from 'rxjs';
......@@ -219,11 +210,6 @@ export class QueriesTab extends PureComponent<Props, State> {
this.forceUpdate();
};
onTransformersChange = (transformers: DataTransformerConfig[]) => {
this.props.panel.setTransformations(transformers);
this.forceUpdate();
};
setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement;
this.setState({ scrollTop: target.scrollTop });
......@@ -256,7 +242,7 @@ export class QueriesTab extends PureComponent<Props, State> {
};
render() {
const { scrollTop, data } = this.state;
const { scrollTop } = this.state;
const queryInspector: EditorToolbarView = {
title: 'Query Inspector',
render: this.renderQueryInspector,
......@@ -268,8 +254,6 @@ export class QueriesTab extends PureComponent<Props, State> {
render: this.renderHelp,
};
const enableTransformations = config.featureToggles.transformations;
return (
<EditorTabBody
heading="Query"
......@@ -278,33 +262,7 @@ export class QueriesTab extends PureComponent<Props, State> {
setScrollTop={this.setScrollTop}
scrollTop={scrollTop}
>
<>
{this.renderQueryBody()}
{enableTransformations && (
<PanelOptionsGroup
title={
<>
Query results
<AlphaNotice
state={PluginState.alpha}
className={css`
margin-left: 16px;
`}
/>
</>
}
>
{this.state.data.state !== LoadingState.NotStarted && (
<TransformationsEditor
transformations={this.props.panel.transformations || []}
onChange={this.onTransformersChange}
dataFrames={data.series}
/>
)}
</PanelOptionsGroup>
)}
</>
<>{this.renderQueryBody()}</>
</EditorTabBody>
);
}
......
......@@ -103,6 +103,7 @@ exports[`Render should render alpha info text 1`] = `
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
"meta": Object {},
}
}
/>
......@@ -292,6 +293,7 @@ exports[`Render should render is ready only message 1`] = `
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
"meta": Object {},
}
}
/>
......
import { DashboardAcl } from './acl';
import { DataQuery } from '@grafana/data';
import { AngularComponent } from '@grafana/runtime';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
export interface DashboardDTO {
......@@ -70,7 +69,6 @@ export interface QueriesToUpdateOnDashboardLoad {
export interface PanelState {
pluginId: string;
angularPanel?: AngularComponent;
}
export interface DashboardState {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment