Commit bdb56599 by Dominik Prokop Committed by GitHub

NewPanelEdit: Organise sidebar in tabs (#22870)

* Refactor value mappings UI to work better with new panel edit

* TS fix

* Experimenting with tabs in the sidebar

* Small refactor and added Panel general settings

* Merge fixes

* fix fieldOptions being used instead of fieldConfig

* Added icons to tabs (testing)

* Only 3 tabs i think, panel specific options need ot exist in first tab, some style tweaks

* Moved title and no value up

* Updated

* Render panel options in Options tab and add old options styles hack to display those vertically

* Basic settings to Panel settings

* Make nullcheck pass

* Snaps bump

* Fix standard configs not update

* Organise sidebar better, add tmp NewPanelEditorContext to hide duplicate legacy options

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
parent c600a085
......@@ -10,7 +10,7 @@ import { DataLinksListItem } from './DataLinksListItem';
import { DataLinkEditorModalContent } from './DataLinkEditorModalContent';
interface DataLinksInlineEditorProps {
links: DataLink[];
links?: DataLink[];
onChange: (links: DataLink[]) => void;
suggestions: VariableSuggestion[];
data: DataFrame[];
......@@ -23,6 +23,9 @@ export const DataLinksInlineEditor: React.FC<DataLinksInlineEditorProps> = ({ li
const styles = getDataLinksInlineEditorStyles(theme);
const onDataLinkChange = (index: number, link: DataLink) => {
if (!links) {
return;
}
const update = cloneDeep(links);
update[index] = link;
onChange(update);
......@@ -48,6 +51,9 @@ export const DataLinksInlineEditor: React.FC<DataLinksInlineEditorProps> = ({ li
};
const onDataLinkRemove = (index: number) => {
if (!links) {
return;
}
const update = cloneDeep(links);
update.splice(index, 1);
onChange(update);
......@@ -55,7 +61,7 @@ export const DataLinksInlineEditor: React.FC<DataLinksInlineEditorProps> = ({ li
return (
<>
{links && (
{links && links.length > 0 && (
<div className={styles.wrapper}>
{links.map((l, i) => {
return (
......@@ -84,7 +90,7 @@ export const DataLinksInlineEditor: React.FC<DataLinksInlineEditorProps> = ({ li
>
<DataLinkEditorModalContent
index={editIndex}
link={links[editIndex]}
link={links![editIndex]}
data={data}
onChange={onDataLinkChange}
onClose={() => setEditIndex(null)}
......
......@@ -158,5 +158,5 @@ export const getStandardFieldConfigs = () => {
shouldApply: () => true,
};
return [unit, min, max, decimals, thresholds, mappings, title, noValue, links];
return [unit, min, max, decimals, title, noValue, thresholds, mappings, links];
};
......@@ -17,6 +17,11 @@ export interface LayoutProps {
justify?: Justify;
}
export interface ContainerProps {
padding?: Spacing;
margin?: Spacing;
}
export const Layout: React.FC<LayoutProps> = ({
children,
orientation = Orientation.Horizontal,
......@@ -45,6 +50,12 @@ export const VerticalGroup: React.FC<Omit<LayoutProps, 'orientation'>> = ({ chil
</Layout>
);
export const Container: React.FC<ContainerProps> = ({ children, padding, margin }) => {
const theme = useTheme();
const styles = getContainerStyles(theme, padding, margin);
return <div className={styles.wrapper}>{children}</div>;
};
const getStyles = stylesFactory((theme: GrafanaTheme, orientation: Orientation, spacing: Spacing, justify: Justify) => {
return {
layout: css`
......@@ -64,3 +75,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme, orientation: Orientation,
`,
};
});
const getContainerStyles = stylesFactory((theme: GrafanaTheme, padding?: Spacing, margin?: Spacing) => {
const paddingSize = (padding && theme.spacing[padding]) || 0;
const marginSize = (margin && theme.spacing[margin]) || 0;
return {
wrapper: css`
margin: ${marginSize};
padding: ${paddingSize};
`,
};
});
import React, { FC } from 'react';
import { css, cx } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { selectThemeVariant, stylesFactory, useTheme } from '../../themes';
import { stylesFactory, useTheme } from '../../themes';
export interface TabProps {
label: string;
......@@ -12,7 +12,6 @@ export interface TabProps {
const getTabStyles = stylesFactory((theme: GrafanaTheme) => {
const colors = theme.colors;
const tabBorderColor = selectThemeVariant({ dark: colors.dark9, light: colors.gray5 }, theme.type);
return {
tabItem: css`
......@@ -42,7 +41,7 @@ const getTabStyles = stylesFactory((theme: GrafanaTheme) => {
}
`,
activeStyle: css`
border-color: ${colors.orange} ${tabBorderColor} transparent;
border-color: ${colors.orange} ${colors.pageHeaderBorder} transparent;
background: ${colors.pageBg};
color: ${colors.link};
overflow: hidden;
......
......@@ -155,7 +155,7 @@ exports[`TimePicker renders buttons correctly 1`] = `
"orange": "#eb7b18",
"orangeDark": "#ff780a",
"pageBg": "#161719",
"pageHeaderBorder": "#343436",
"pageHeaderBorder": "#202226",
"panelBg": "#212124",
"purple": "#9933cc",
"queryGreen": "#74e680",
......@@ -464,7 +464,7 @@ exports[`TimePicker renders content correctly after beeing open 1`] = `
"orange": "#eb7b18",
"orangeDark": "#ff780a",
"pageBg": "#161719",
"pageHeaderBorder": "#343436",
"pageHeaderBorder": "#202226",
"panelBg": "#212124",
"purple": "#9933cc",
"queryGreen": "#74e680",
......
......@@ -105,6 +105,7 @@ export * from './SingleStatShared/index';
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu';
export { DataLinksEditor } from './DataLinks/DataLinksEditor';
export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor';
export { DataLinkInput } from './DataLinks/DataLinkInput';
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
export { SeriesIcon } from './Legend/SeriesIcon';
......@@ -152,4 +153,4 @@ export { default as Forms, ButtonVariant } from './Forms';
export { ValuePicker } from './ValuePicker/ValuePicker';
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';
export { getStandardFieldConfigs } from './FieldConfigs/standardFieldConfigEditors';
export { HorizontalGroup, VerticalGroup } from './Layout/Layout';
export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout';
......@@ -74,7 +74,7 @@ const darkTheme: GrafanaTheme = {
linkHover: basicColors.white,
linkExternal: basicColors.blue,
headingColor: basicColors.gray4,
pageHeaderBorder: basicColors.dark9,
pageHeaderBorder: basicColors.gray15,
panelBg: basicColors.dark4,
// Next-gen forms functional colors
......
......@@ -100,7 +100,7 @@ exports[`ServerStats Should render table with stats 1`] = `
className="css-13jkosq"
>
<li
className="css-b418eg"
className="css-14totda"
onClick={[Function]}
>
<i
......
import React from 'react';
import React, { useCallback } from 'react';
import cloneDeep from 'lodash/cloneDeep';
import {
FieldConfigSource,
......@@ -9,10 +9,10 @@ import {
PanelPlugin,
SelectableValue,
} from '@grafana/data';
import { Forms, fieldMatchersUI, ValuePicker } from '@grafana/ui';
import { Forms, fieldMatchersUI, ValuePicker, useTheme } from '@grafana/ui';
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
import { OptionsGroup } from './OptionsGroup';
import { OverrideEditor } from './OverrideEditor';
import { css } from 'emotion';
interface Props {
plugin: PanelPlugin;
......@@ -26,50 +26,25 @@ interface Props {
/**
* Expects the container div to have size set and will fill it 100%
*/
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) {
defaults.custom = { ...defaults.custom };
delete defaults.custom[name];
} else {
defaults.custom = { ...defaults.custom, [name]: value };
}
} else if (!remove) {
defaults.custom = { [name]: value };
}
} else if (remove) {
delete (defaults as any)[name];
} else {
(defaults as any)[name] = value;
}
export const OverrideFieldConfigEditor: React.FC<Props> = props => {
const theme = useTheme();
this.props.onChange({
...this.props.config,
defaults,
});
};
onOverrideChange = (index: number, override: any) => {
const { config } = this.props;
const onOverrideChange = (index: number, override: any) => {
const { config } = props;
let overrides = cloneDeep(config.overrides);
overrides[index] = override;
this.props.onChange({ ...config, overrides });
props.onChange({ ...config, overrides });
};
onOverrideRemove = (overrideIndex: number) => {
const { config } = this.props;
const onOverrideRemove = (overrideIndex: number) => {
const { config } = props;
let overrides = cloneDeep(config.overrides);
overrides.splice(overrideIndex, 1);
this.props.onChange({ ...config, overrides });
props.onChange({ ...config, overrides });
};
onOverrideAdd = (value: SelectableValue<string>) => {
const { onChange, config } = this.props;
const onOverrideAdd = (value: SelectableValue<string>) => {
const { onChange, config } = props;
onChange({
...config,
overrides: [
......@@ -84,46 +59,8 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
});
};
renderEditor(item: FieldPropertyEditorItem, custom: boolean) {
const { data } = this.props;
const config = this.props.config.defaults;
const value = custom ? (config.custom ? config.custom[item.id] : undefined) : (config as any)[item.id];
return (
<Forms.Field label={item.name} description={item.description} key={`${item.id}/${custom}`}>
<item.editor
item={item}
value={value}
onChange={v => this.setDefaultValue(item.id, v, custom)}
context={{
data,
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
}}
/>
</Forms.Field>
);
}
renderStandardConfigs() {
const { include } = this.props;
if (include) {
return include.map(f => this.renderEditor(standardFieldConfigEditorRegistry.get(f), false));
}
return standardFieldConfigEditorRegistry.list().map(f => this.renderEditor(f, false));
}
renderCustomConfigs() {
const { plugin } = this.props;
if (!plugin.customFieldConfigs) {
return null;
}
return plugin.customFieldConfigs.list().map(f => this.renderEditor(f, true));
}
renderOverrides() {
const { config, data, plugin } = this.props;
const renderOverrides = () => {
const { config, data, plugin } = props;
const { customFieldConfigs } = plugin;
if (config.overrides.length === 0) {
......@@ -157,8 +94,8 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
key={`${o.matcher.id}/${i}`}
data={data}
override={o}
onChange={value => this.onOverrideChange(i, value)}
onRemove={() => this.onOverrideRemove(i)}
onChange={value => onOverrideChange(i, value)}
onRemove={() => onOverrideRemove(i)}
configPropertiesOptions={configPropertiesOptions}
customPropertiesRegistry={customFieldConfigs}
/>
......@@ -166,9 +103,9 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
})}
</div>
);
}
};
renderAddOverride = () => {
const renderAddOverride = () => {
return (
<ValuePicker
icon="plus"
......@@ -176,29 +113,95 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
options={fieldMatchersUI
.list()
.map<SelectableValue<string>>(i => ({ label: i.name, value: i.id, description: i.description }))}
onChange={value => this.onOverrideAdd(value)}
onChange={value => onOverrideAdd(value)}
/>
);
};
render() {
const { plugin } = this.props;
return (
<div
className={css`
padding: ${theme.spacing.md};
`}
>
{renderOverrides()}
{renderAddOverride()}
</div>
);
};
export const DefaultFieldConfigEditor: React.FC<Props> = ({ include, data, onChange, config, plugin }) => {
const setDefaultValue = useCallback(
(name: string, value: any, custom: boolean) => {
const defaults = { ...config.defaults };
const remove = value === undefined || value === null || '';
if (custom) {
if (defaults.custom) {
if (remove) {
defaults.custom = { ...defaults.custom };
delete defaults.custom[name];
} else {
defaults.custom = { ...defaults.custom, [name]: value };
}
} else if (!remove) {
defaults.custom = { [name]: value };
}
} else if (remove) {
delete (defaults as any)[name];
} else {
(defaults as any)[name] = value;
}
return (
<div>
{plugin.customFieldConfigs && (
<OptionsGroup title={`${plugin.meta.name} options`}>{this.renderCustomConfigs()}</OptionsGroup>
)}
onChange({
...config,
defaults,
});
},
[config, onChange]
);
const renderEditor = useCallback(
(item: FieldPropertyEditorItem, custom: boolean) => {
const defaults = config.defaults;
const value = custom ? (defaults.custom ? defaults.custom[item.id] : undefined) : (defaults as any)[item.id];
return (
<Forms.Field label={item.name} description={item.description} key={`${item.id}/${custom}`}>
<item.editor
item={item}
value={value}
onChange={v => setDefaultValue(item.id, v, custom)}
context={{
data,
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
}}
/>
</Forms.Field>
);
},
[config]
);
<OptionsGroup title="Field defaults">{this.renderStandardConfigs()}</OptionsGroup>
const renderStandardConfigs = useCallback(() => {
if (include) {
return <>{include.map(f => renderEditor(standardFieldConfigEditorRegistry.get(f), false))}</>;
}
return <>{standardFieldConfigEditorRegistry.list().map(f => renderEditor(f, false))}</>;
}, [plugin, config]);
<OptionsGroup title="Field overrides">
{this.renderOverrides()}
{this.renderAddOverride()}
</OptionsGroup>
</div>
);
}
}
const renderCustomConfigs = useCallback(() => {
if (!plugin.customFieldConfigs) {
return null;
}
return plugin.customFieldConfigs.list().map(f => renderEditor(f, true));
}, [plugin, config]);
export default FieldConfigEditor;
return (
<>
{plugin.customFieldConfigs && renderCustomConfigs()}
{renderStandardConfigs()}
</>
);
};
import React, { useCallback, useState, useMemo } from 'react';
import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin } from '@grafana/data';
import { DashboardModel, PanelModel } from '../../state';
import {
CustomScrollbar,
stylesFactory,
Tab,
TabContent,
TabsBar,
useTheme,
Forms,
DataLinksInlineEditor,
Container,
} from '@grafana/ui';
import { DefaultFieldConfigEditor, OverrideFieldConfigEditor } from './FieldConfigEditor';
import { AngularPanelOptions } from './AngularPanelOptions';
import { css } from 'emotion';
import { OptionsGroup } from './OptionsGroup';
import { getPanelLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
export const OptionsPaneContent: React.FC<{
plugin?: PanelPlugin;
panel: PanelModel;
data: PanelData;
dashboard: DashboardModel;
onFieldConfigsChange: (config: FieldConfigSource) => void;
onPanelOptionsChanged: (options: any) => void;
onPanelConfigChange: (configKey: string, value: any) => void;
}> = ({ plugin, panel, data, onFieldConfigsChange, onPanelOptionsChanged, onPanelConfigChange, dashboard }) => {
const theme = useTheme();
const styles = getStyles(theme);
const linkVariablesSuggestions = useMemo(() => getPanelLinksVariableSuggestions(), []);
const renderFieldOptions = useCallback(
(plugin: PanelPlugin) => {
const fieldConfig = panel.getFieldConfig();
if (!fieldConfig) {
return null;
}
return (
<Container padding="md">
{renderCustomPanelSettings(plugin)}
<DefaultFieldConfigEditor
config={fieldConfig}
plugin={plugin}
onChange={onFieldConfigsChange}
data={data.series}
/>
</Container>
);
},
[data, plugin, panel, onFieldConfigsChange]
);
const renderFieldOverrideOptions = useCallback(
(plugin: PanelPlugin) => {
const fieldConfig = panel.getFieldConfig();
if (!fieldConfig) {
return null;
}
return (
<OverrideFieldConfigEditor
config={fieldConfig}
plugin={plugin}
onChange={onFieldConfigsChange}
data={data.series}
/>
);
},
[data, plugin, panel, onFieldConfigsChange]
);
const renderCustomPanelSettings = useCallback(
(plugin: PanelPlugin) => {
if (plugin.editor && panel) {
return (
<div className={styles.legacyOptions}>
<plugin.editor
data={data}
options={panel.getOptions()}
onOptionsChange={onPanelOptionsChanged}
fieldConfig={panel.getFieldConfig()}
onFieldConfigChange={onFieldConfigsChange}
/>
</div>
);
}
return (
<div className={styles.legacyOptions}>
<AngularPanelOptions panel={panel} dashboard={dashboard} plugin={plugin} />
</div>
);
},
[data, plugin, panel, onFieldConfigsChange]
);
const renderPanelSettings = useCallback(() => {
console.log(panel.transparent);
return (
<div>
<OptionsGroup title="Panel settings">
<>
<Forms.Field label="Panel title">
<Forms.Input
defaultValue={panel.title}
onBlur={e => onPanelConfigChange('title', e.currentTarget.value)}
/>
</Forms.Field>
<Forms.Field label="Description" description="Panel description supports markdown and links">
<Forms.TextArea
defaultValue={panel.description}
onBlur={e => onPanelConfigChange('description', e.currentTarget.value)}
/>
</Forms.Field>
<Forms.Field label="Transparent" description="Display panel without background">
<Forms.Switch
value={panel.transparent}
onChange={e => onPanelConfigChange('transparent', e.currentTarget.checked)}
/>
</Forms.Field>
</>
</OptionsGroup>
<OptionsGroup title="Panel links">
<DataLinksInlineEditor
links={panel.links}
onChange={links => onPanelConfigChange('links', links)}
suggestions={linkVariablesSuggestions}
data={data.series}
/>
</OptionsGroup>
<OptionsGroup title="Panel repeating">
<div>TODO</div>
</OptionsGroup>
</div>
);
}, [data, plugin, panel, onFieldConfigsChange]);
const [activeTab, setActiveTab] = useState('defaults');
return (
<div className={styles.panelOptionsPane}>
{plugin && (
<div className={styles.wrapper}>
<TabsBar>
<Tab label="Options" active={activeTab === 'defaults'} onChangeTab={() => setActiveTab('defaults')} />
<Tab label="Overrides" active={activeTab === 'overrides'} onChangeTab={() => setActiveTab('overrides')} />
<Tab label="General" active={activeTab === 'panel'} onChangeTab={() => setActiveTab('panel')} />
</TabsBar>
<TabContent className={styles.tabContent}>
<CustomScrollbar>
{activeTab === 'defaults' && renderFieldOptions(plugin)}
{activeTab === 'overrides' && renderFieldOverrideOptions(plugin)}
{activeTab === 'panel' && renderPanelSettings()}
</CustomScrollbar>
</TabContent>
</div>
)}
</div>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
wrapper: css`
display: flex;
flex-direction: column;
height: 100%;
`,
panelOptionsPane: css`
height: 100%;
width: 100%;
border-bottom: none;
`,
tabContent: css`
padding: 0;
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 0;
background: ${theme.colors.pageBg};
border-left: 1px solid ${theme.colors.pageHeaderBorder};
`,
legacyOptions: css`
label: legacy-options;
.panel-options-grid {
display: flex;
flex-direction: column;
}
.panel-options-group {
margin-bottom: 0;
}
.panel-options-group__body {
padding: ${theme.spacing.md} 0;
}
.section {
display: block;
margin: ${theme.spacing.md} 0;
&:first-child {
margin-top: 0;
}
}
`,
};
});
import React, { PureComponent } from 'react';
import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
import { CustomScrollbar, Forms, selectThemeVariant, stylesFactory } from '@grafana/ui';
import { Forms, selectThemeVariant, stylesFactory } from '@grafana/ui';
import { css, cx } from 'emotion';
import config from 'app/core/config';
import AutoSizer from 'react-virtualized-auto-sizer';
......@@ -23,11 +23,9 @@ import { LocationState } from 'app/types';
import { calculatePanelSize } from './utils';
import { initPanelEditor, panelEditorCleanUp, updatePanelEditorUIState } from './state/actions';
import { PanelEditorUIState, setDiscardChanges } from './state/reducers';
import { FieldConfigEditor } from './FieldConfigEditor';
import { OptionsGroup } from './OptionsGroup';
import { getPanelEditorTabs } from './state/selectors';
import { getPanelStateById } from '../../state/selectors';
import { AngularPanelOptions } from './AngularPanelOptions';
import { OptionsPaneContent } from './OptionsPaneContent';
enum Pane {
Right,
......@@ -59,6 +57,9 @@ interface DispatchProps {
type Props = OwnProps & ConnectedProps & DispatchProps;
// TODO[NewPanelEdit]: Remove when we switch to new panel editor
export const NewPanelEditorContext = React.createContext(false);
export class PanelEditorUnconnected extends PureComponent<Props> {
querySubscription: Unsubscribable;
......@@ -98,43 +99,17 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
this.forceUpdate();
};
renderFieldOptions(plugin: PanelPlugin) {
const { panel, data } = this.props;
const { fieldConfig } = panel;
if (!fieldConfig) {
return null;
}
return (
<FieldConfigEditor config={fieldConfig} plugin={plugin} onChange={this.onFieldConfigChange} data={data.series} />
);
}
onPanelOptionsChanged = (options: any) => {
this.props.panel.updateOptions(options);
this.forceUpdate();
};
renderPanelSettings(plugin: PanelPlugin) {
const { data, panel, dashboard } = this.props;
if (plugin.editor && panel) {
return (
<div style={{ marginTop: '10px' }}>
<plugin.editor
data={data}
options={panel.getOptions()}
onOptionsChange={this.onPanelOptionsChanged}
fieldConfig={panel.getFieldConfig()}
onFieldConfigChange={this.onFieldConfigChange}
/>
</div>
);
}
return <AngularPanelOptions panel={panel} dashboard={dashboard} plugin={plugin} />;
}
onPanelConfigChanged = (configKey: string, value: any) => {
// @ts-ignore
this.props.panel[configKey] = value;
this.props.panel.render();
this.forceUpdate();
};
onDragFinished = (pane: Pane, size: number) => {
document.body.style.cursor = 'auto';
......@@ -250,20 +225,23 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
);
}
renderOptionsPane(styles: any) {
const { plugin } = this.props;
renderOptionsPane() {
const { plugin, dashboard, data, panel } = this.props;
if (!plugin) {
return null;
}
return (
<div className={styles.panelOptionsPane}>
<CustomScrollbar>
{plugin && (
<>
{this.renderFieldOptions(plugin)}
<OptionsGroup title={`${plugin.meta.name} options`}>{this.renderPanelSettings(plugin)}</OptionsGroup>
</>
)}
</CustomScrollbar>
</div>
<OptionsPaneContent
plugin={plugin}
dashboard={dashboard}
data={data}
panel={panel}
onFieldConfigsChange={this.onFieldConfigChange}
onPanelOptionsChanged={this.onPanelOptionsChanged}
onPanelConfigChange={this.onPanelConfigChanged}
/>
);
}
......@@ -282,7 +260,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
onDragFinished={size => this.onDragFinished(Pane.Right, size)}
>
{this.renderHorizontalSplit(styles)}
{this.renderOptionsPane(styles)}
{this.renderOptionsPane()}
</SplitPane>
);
}
......@@ -296,12 +274,14 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
}
return (
<div className={styles.wrapper}>
{this.renderToolbar()}
<div className={styles.panesWrapper}>
{uiState.isPanelOptionsVisible ? this.renderWithOptionsPane(styles) : this.renderHorizontalSplit(styles)}
<NewPanelEditorContext.Provider value={true}>
<div className={styles.wrapper}>
{this.renderToolbar()}
<div className={styles.panesWrapper}>
{uiState.isPanelOptionsVisible ? this.renderWithOptionsPane(styles) : this.renderHorizontalSplit(styles)}
</div>
</div>
</div>
</NewPanelEditorContext.Provider>
);
}
}
......@@ -402,12 +382,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
height: 100%;
width: 100%;
`,
panelOptionsPane: css`
height: 100%;
width: 100%;
background: ${theme.colors.pageBg};
border-bottom: none;
`,
toolbar: css`
display: flex;
padding: ${theme.spacing.sm};
......
......@@ -34,7 +34,15 @@ export const PanelEditorTabs: React.FC<PanelEditorTabsProps> = ({ panel, dashboa
<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)} />;
return (
<Tab
key={tab.id}
label={tab.text}
active={tab.active}
onChangeTab={() => onChangeTab(tab)}
icon={tab.icon}
/>
);
})}
</TabsBar>
<TabContent className={styles.tabContent}>
......
......@@ -18,12 +18,14 @@ export const getPanelEditorTabs = memoizeOne((location: LocationState, plugin?:
tabs.push({
id: PanelEditorTabId.Queries,
text: 'Queries',
icon: 'gicon gicon-datasources',
active: false,
});
tabs.push({
id: PanelEditorTabId.Transform,
text: 'Transform',
icon: 'fa fa-exchange',
active: false,
});
}
......@@ -31,6 +33,7 @@ export const getPanelEditorTabs = memoizeOne((location: LocationState, plugin?:
tabs.push({
id: PanelEditorTabId.Visualization,
text: 'Visualization',
icon: 'fa fa-bar-chart',
active: false,
});
......@@ -38,6 +41,7 @@ export const getPanelEditorTabs = memoizeOne((location: LocationState, plugin?:
tabs.push({
id: PanelEditorTabId.Alert,
text: 'Alert',
icon: 'gicon gicon-alert',
active: false,
});
}
......
......@@ -2,6 +2,7 @@ export interface PanelEditorTab {
id: string;
text: string;
active: boolean;
icon: string;
}
export enum PanelEditorTabId {
......
......@@ -27,6 +27,7 @@ import {
getCalculationValueDataLinksVariableSuggestions,
getDataLinksVariableSuggestions,
} from '../../../features/panel/panellinks/link_srv';
import { NewPanelEditorContext } from '../../../features/dashboard/components/PanelEditor/PanelEditor';
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
onDisplayOptionsChanged = (fieldOptions: FieldDisplayOptions) =>
......@@ -92,64 +93,84 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
: getCalculationValueDataLinksVariableSuggestions(this.props.data.series);
return (
<>
<PanelOptionsGrid>
<PanelOptionsGroup title="Display">
<FieldDisplayEditor onChange={this.onDisplayOptionsChanged} value={fieldOptions} labelWidth={labelWidth} />
<div className="form-field">
<FormLabel width={labelWidth}>Orientation</FormLabel>
<Select
width={12}
options={orientationOptions}
defaultValue={orientationOptions[0]}
onChange={this.onOrientationChange}
value={orientationOptions.find(item => item.value === options.orientation)}
/>
</div>
<div className="form-field">
<FormLabel width={labelWidth}>Mode</FormLabel>
<Select
width={12}
options={displayModes}
defaultValue={displayModes[0]}
onChange={this.onDisplayModeChange}
value={displayModes.find(item => item.value === options.displayMode)}
/>
</div>
<NewPanelEditorContext.Consumer>
{useNewEditor => {
return (
<>
{options.displayMode !== 'lcd' && (
<Switch
label="Unfilled"
labelClass={`width-${labelWidth}`}
checked={options.showUnfilled}
onChange={this.onToggleShowUnfilled}
/>
)}
</>
</PanelOptionsGroup>
<PanelOptionsGroup title="Field">
<FieldPropertiesEditor
showMinMax={true}
showTitle={true}
onChange={this.onDefaultsChange}
value={defaults}
/>
</PanelOptionsGroup>
<PanelOptionsGrid>
<PanelOptionsGroup title="Display">
<FieldDisplayEditor
onChange={this.onDisplayOptionsChanged}
value={fieldOptions}
labelWidth={labelWidth}
/>
<div className="form-field">
<FormLabel width={labelWidth}>Orientation</FormLabel>
<Select
width={12}
options={orientationOptions}
defaultValue={orientationOptions[0]}
onChange={this.onOrientationChange}
value={orientationOptions.find(item => item.value === options.orientation)}
/>
</div>
<div className="form-field">
<FormLabel width={labelWidth}>Mode</FormLabel>
<Select
width={12}
options={displayModes}
defaultValue={displayModes[0]}
onChange={this.onDisplayModeChange}
value={displayModes.find(item => item.value === options.displayMode)}
/>
</div>
<>
{options.displayMode !== 'lcd' && (
<Switch
label="Unfilled"
labelClass={`width-${labelWidth}`}
checked={options.showUnfilled}
onChange={this.onToggleShowUnfilled}
/>
)}
</>
</PanelOptionsGroup>
<>
{!useNewEditor && (
<>
<PanelOptionsGroup title="Field">
<FieldPropertiesEditor
showMinMax={true}
showTitle={true}
onChange={this.onDefaultsChange}
value={defaults}
/>
</PanelOptionsGroup>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
</PanelOptionsGrid>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
</>
)}
</>
</PanelOptionsGrid>
<LegacyValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
{!useNewEditor && (
<LegacyValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
)}
<PanelOptionsGroup title="Data links">
<DataLinksEditor
value={defaults.links}
onChange={this.onDataLinksChanged}
suggestions={suggestions}
maxLinks={10}
/>
</PanelOptionsGroup>
</>
{!useNewEditor && (
<PanelOptionsGroup title="Data links">
<DataLinksEditor
value={defaults.links}
onChange={this.onDataLinksChanged}
suggestions={suggestions}
maxLinks={10}
/>
</PanelOptionsGroup>
)}
</>
);
}}
</NewPanelEditorContext.Consumer>
);
}
}
......@@ -24,6 +24,7 @@ import {
getCalculationValueDataLinksVariableSuggestions,
getDataLinksVariableSuggestions,
} from '../../../features/panel/panellinks/link_srv';
import { NewPanelEditorContext } from '../../../features/dashboard/components/PanelEditor/PanelEditor';
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
labelWidth = 6;
......@@ -100,50 +101,67 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
const suggestions = fieldOptions.values
? getDataLinksVariableSuggestions(this.props.data.series)
: getCalculationValueDataLinksVariableSuggestions(this.props.data.series);
return (
<>
<PanelOptionsGrid>
<PanelOptionsGroup title="Display">
<FieldDisplayEditor
onChange={this.onDisplayOptionsChanged}
value={fieldOptions}
labelWidth={this.labelWidth}
/>
<Switch
label="Labels"
labelClass={`width-${this.labelWidth}`}
checked={showThresholdLabels}
onChange={this.onToggleThresholdLabels}
/>
<Switch
label="Markers"
labelClass={`width-${this.labelWidth}`}
checked={showThresholdMarkers}
onChange={this.onToggleThresholdMarkers}
/>
</PanelOptionsGroup>
<NewPanelEditorContext.Consumer>
{useNewEditor => {
return (
<>
<PanelOptionsGrid>
<PanelOptionsGroup title="Display">
<FieldDisplayEditor
onChange={this.onDisplayOptionsChanged}
value={fieldOptions}
labelWidth={this.labelWidth}
/>
<Switch
label="Labels"
labelClass={`width-${this.labelWidth}`}
checked={showThresholdLabels}
onChange={this.onToggleThresholdLabels}
/>
<Switch
label="Markers"
labelClass={`width-${this.labelWidth}`}
checked={showThresholdMarkers}
onChange={this.onToggleThresholdMarkers}
/>
</PanelOptionsGroup>
<PanelOptionsGroup title="Field">
<FieldPropertiesEditor
showMinMax={true}
showTitle={true}
onChange={this.onDefaultsChange}
value={defaults}
/>
</PanelOptionsGroup>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
</PanelOptionsGrid>
<LegacyValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
<>
{!useNewEditor && (
<>
<PanelOptionsGroup title="Field">
<FieldPropertiesEditor
showMinMax={true}
showTitle={true}
onChange={this.onDefaultsChange}
value={defaults}
/>
</PanelOptionsGroup>
<PanelOptionsGroup title="Data links">
<DataLinksEditor
value={defaults.links}
onChange={this.onDataLinksChanged}
suggestions={suggestions}
maxLinks={10}
/>
</PanelOptionsGroup>
</>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
</>
)}
</>
</PanelOptionsGrid>
{!useNewEditor && (
<>
<LegacyValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
<PanelOptionsGroup title="Data links">
<DataLinksEditor
value={defaults.links}
onChange={this.onDataLinksChanged}
suggestions={suggestions}
maxLinks={10}
/>
</PanelOptionsGroup>
</>
)}
</>
);
}}
</NewPanelEditorContext.Consumer>
);
}
}
......@@ -15,6 +15,7 @@ import {
} from '@grafana/ui';
import { Options, GraphOptions } from './types';
import { GraphLegendEditor } from './GraphLegendEditor';
import { NewPanelEditorContext } from 'app/features/dashboard/components/PanelEditor/PanelEditor';
export class GraphPanelEditor extends PureComponent<PanelEditorProps<Options>> {
onGraphOptionsChange = (options: Partial<GraphOptions>) => {
......@@ -61,36 +62,46 @@ export class GraphPanelEditor extends PureComponent<PanelEditorProps<Options>> {
} = this.props.options;
return (
<>
<div className="section gf-form-group">
<h5 className="section-heading">Draw Modes</h5>
<Switch label="Lines" labelClass="width-5" checked={showLines} onChange={this.onToggleLines} />
<Switch label="Bars" labelClass="width-5" checked={showBars} onChange={this.onToggleBars} />
<Switch label="Points" labelClass="width-5" checked={showPoints} onChange={this.onTogglePoints} />
</div>
<PanelOptionsGrid>
<PanelOptionsGroup title="Field">
<FieldPropertiesEditor
showMinMax={false}
onChange={this.onDefaultsChange}
value={this.props.fieldConfig.defaults}
/>
</PanelOptionsGroup>
<PanelOptionsGroup title="Tooltip">
<Select
value={{ value: mode, label: mode === 'single' ? 'Single' : 'All series' }}
onChange={value => {
this.onTooltipOptionsChange({ mode: value.value as any });
}}
options={[
{ label: 'All series', value: 'multi' },
{ label: 'Single', value: 'single' },
]}
/>
</PanelOptionsGroup>
<GraphLegendEditor options={this.props.options.legend} onChange={this.onLegendOptionsChange} />
</PanelOptionsGrid>
</>
<NewPanelEditorContext.Consumer>
{useNewEditor => {
return (
<>
<div className="section gf-form-group">
<h5 className="section-heading">Draw Modes</h5>
<Switch label="Lines" labelClass="width-5" checked={showLines} onChange={this.onToggleLines} />
<Switch label="Bars" labelClass="width-5" checked={showBars} onChange={this.onToggleBars} />
<Switch label="Points" labelClass="width-5" checked={showPoints} onChange={this.onTogglePoints} />
</div>
<PanelOptionsGrid>
<>
{!useNewEditor && (
<PanelOptionsGroup title="Field">
<FieldPropertiesEditor
showMinMax={false}
onChange={this.onDefaultsChange}
value={this.props.fieldConfig.defaults}
/>
</PanelOptionsGroup>
)}
</>
<PanelOptionsGroup title="Tooltip">
<Select
value={{ value: mode, label: mode === 'single' ? 'Single' : 'All series' }}
onChange={value => {
this.onTooltipOptionsChange({ mode: value.value as any });
}}
options={[
{ label: 'All series', value: 'multi' },
{ label: 'Single', value: 'single' },
]}
/>
</PanelOptionsGroup>
<GraphLegendEditor options={this.props.options.legend} onChange={this.onLegendOptionsChange} />
</PanelOptionsGrid>
</>
);
}}
</NewPanelEditorContext.Consumer>
);
}
}
......@@ -28,6 +28,7 @@ import {
getCalculationValueDataLinksVariableSuggestions,
getDataLinksVariableSuggestions,
} from '../../../features/panel/panellinks/link_srv';
import { NewPanelEditorContext } from '../../../features/dashboard/components/PanelEditor/PanelEditor';
export class StatPanelEditor extends PureComponent<PanelEditorProps<StatPanelOptions>> {
onThresholdsChanged = (thresholds: ThresholdsConfig) => {
......@@ -91,73 +92,88 @@ export class StatPanelEditor extends PureComponent<PanelEditorProps<StatPanelOpt
: getCalculationValueDataLinksVariableSuggestions(this.props.data.series);
return (
<>
<PanelOptionsGrid>
<PanelOptionsGroup title="Display">
<FieldDisplayEditor onChange={this.onDisplayOptionsChanged} value={fieldOptions} labelWidth={8} />
<div className="form-field">
<FormLabel width={8}>Orientation</FormLabel>
<Select
width={12}
options={orientationOptions}
defaultValue={orientationOptions[0]}
onChange={this.onOrientationChange}
value={orientationOptions.find(item => item.value === options.orientation)}
/>
</div>
<div className="form-field">
<FormLabel width={8}>Color</FormLabel>
<Select
width={12}
options={colorModes}
defaultValue={colorModes[0]}
onChange={this.onColorModeChanged}
value={colorModes.find(item => item.value === options.colorMode)}
/>
</div>
<div className="form-field">
<FormLabel width={8}>Graph</FormLabel>
<Select
width={12}
options={graphModes}
defaultValue={graphModes[0]}
onChange={this.onGraphModeChanged}
value={graphModes.find(item => item.value === options.graphMode)}
/>
</div>
<div className="form-field">
<FormLabel width={8}>Justify</FormLabel>
<Select
width={12}
options={justifyModes}
defaultValue={justifyModes[0]}
onChange={this.onJustifyModeChanged}
value={justifyModes.find(item => item.value === options.justifyMode)}
/>
</div>
</PanelOptionsGroup>
<PanelOptionsGroup title="Field">
<FieldPropertiesEditor
showMinMax={true}
onChange={this.onDefaultsChange}
value={defaults}
showTitle={true}
/>
</PanelOptionsGroup>
<NewPanelEditorContext.Consumer>
{useNewEditor => {
return (
<>
<PanelOptionsGrid>
<PanelOptionsGroup title="Display">
<FieldDisplayEditor onChange={this.onDisplayOptionsChanged} value={fieldOptions} labelWidth={8} />
<div className="form-field">
<FormLabel width={8}>Orientation</FormLabel>
<Select
width={12}
options={orientationOptions}
defaultValue={orientationOptions[0]}
onChange={this.onOrientationChange}
value={orientationOptions.find(item => item.value === options.orientation)}
/>
</div>
<div className="form-field">
<FormLabel width={8}>Color</FormLabel>
<Select
width={12}
options={colorModes}
defaultValue={colorModes[0]}
onChange={this.onColorModeChanged}
value={colorModes.find(item => item.value === options.colorMode)}
/>
</div>
<div className="form-field">
<FormLabel width={8}>Graph</FormLabel>
<Select
width={12}
options={graphModes}
defaultValue={graphModes[0]}
onChange={this.onGraphModeChanged}
value={graphModes.find(item => item.value === options.graphMode)}
/>
</div>
<div className="form-field">
<FormLabel width={8}>Justify</FormLabel>
<Select
width={12}
options={justifyModes}
defaultValue={justifyModes[0]}
onChange={this.onJustifyModeChanged}
value={justifyModes.find(item => item.value === options.justifyMode)}
/>
</div>
</PanelOptionsGroup>
<>
{!useNewEditor && (
<>
<PanelOptionsGroup title="Field">
<FieldPropertiesEditor
showMinMax={true}
onChange={this.onDefaultsChange}
value={defaults}
showTitle={true}
/>
</PanelOptionsGroup>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
</>
)}
</>
</PanelOptionsGrid>
{!useNewEditor && (
<>
<LegacyValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
</PanelOptionsGrid>
<LegacyValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
<PanelOptionsGroup title="Data links">
<DataLinksEditor
value={defaults.links}
onChange={this.onDataLinksChanged}
suggestions={suggestions}
maxLinks={10}
/>
</PanelOptionsGroup>
</>
<PanelOptionsGroup title="Data links">
<DataLinksEditor
value={defaults.links}
onChange={this.onDataLinksChanged}
suggestions={suggestions}
maxLinks={10}
/>
</PanelOptionsGroup>
</>
)}
</>
);
}}
</NewPanelEditorContext.Consumer>
);
}
}
......@@ -3,7 +3,7 @@
echo -e "Collecting code stats (typescript errors & more)"
ERROR_COUNT_LIMIT=821
ERROR_COUNT_LIMIT=820
DIRECTIVES_LIMIT=172
CONTROLLERS_LIMIT=139
......
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