NewPanelEdit: Side options collapse/expand design update (#23161)

* WIP: Panel options search

* Panel options search

* Minor update

* Fixed ts issues

* StatPanel: Fixed duplicate option exception

* Added some polish

* Updated snapshot

* Minor fix

* updated snapshot
This commit is contained in:
Torkel Ödegaard
2020-03-30 14:39:18 +02:00
committed by GitHub
parent 1633bacba9
commit d524bb1ff0
9 changed files with 185 additions and 69 deletions

View File

@ -211,7 +211,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDe
}); });
export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => { export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const { addonAfter, addonBefore, prefix, suffix, invalid, loading, size = 'auto', ...restProps } = props; const { className, addonAfter, addonBefore, prefix, suffix, invalid, loading, size = 'auto', ...restProps } = props;
/** /**
* Prefix & suffix are positioned absolutely within inputWrapper. We use client rects below to apply correct padding to the input * Prefix & suffix are positioned absolutely within inputWrapper. We use client rects below to apply correct padding to the input
* when prefix/suffix is larger than default (28px = 16px(icon) + 12px(left/right paddings)). * when prefix/suffix is larger than default (28px = 16px(icon) + 12px(left/right paddings)).
@ -224,7 +224,7 @@ export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const styles = getInputStyles({ theme, invalid: !!invalid }); const styles = getInputStyles({ theme, invalid: !!invalid });
return ( return (
<div className={cx(styles.wrapper, inputSizes()[size])}> <div className={cx(styles.wrapper, inputSizes()[size], className)}>
{!!addonBefore && <div className={styles.addon}>{addonBefore}</div>} {!!addonBefore && <div className={styles.addon}>{addonBefore}</div>}
<div className={styles.inputWrapper}> <div className={styles.inputWrapper}>

View File

@ -13,11 +13,13 @@ export interface IconProps {
const getIconStyles = stylesFactory(() => { const getIconStyles = stylesFactory(() => {
return { return {
icon: css` icon: css`
display: inline-block; display: inline-flex;
width: 16px; width: 16px;
align-items: center;
height: 16px; height: 16px;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
&:before { &:before {
vertical-align: middle; vertical-align: middle;
} }

View File

@ -89,6 +89,7 @@ export type IconType =
| 'times-circle-o' | 'times-circle-o'
| 'check-circle-o' | 'check-circle-o'
| 'ban' | 'ban'
| 'remove'
| 'arrow-left' | 'arrow-left'
| 'arrow-right' | 'arrow-right'
| 'arrow-up' | 'arrow-up'

View File

@ -24,6 +24,8 @@ const getTabsBarStyles = stylesFactory((theme: GrafanaTheme, hideBorder = false)
position: relative; position: relative;
top: 1px; top: 1px;
display: flex; display: flex;
// Sometimes TabsBar is rendered without any tabs, and should preserve height
height: 41px;
`, `,
}; };
}); });

View File

@ -97,7 +97,7 @@ exports[`ServerStats Should render table with stats 1`] = `
className="page-header__tabs" className="page-header__tabs"
> >
<ul <ul
className="css-13jkosq" className="css-payll4"
> >
<li <li
className="css-1aiaexb" className="css-1aiaexb"

View File

@ -5,32 +5,35 @@ import { Tooltip } from '@grafana/ui';
import { e2e } from '@grafana/e2e'; import { e2e } from '@grafana/e2e';
interface Props { interface Props {
icon: string; icon?: string;
tooltip: string; tooltip: string;
classSuffix: string; classSuffix?: string;
onClick?: () => void; onClick?: () => void;
href?: string; href?: string;
children?: React.ReactNode;
} }
export const DashNavButton: FunctionComponent<Props> = ({ icon, tooltip, classSuffix, onClick, href }) => { export const DashNavButton: FunctionComponent<Props> = ({ icon, tooltip, classSuffix, onClick, href, children }) => {
if (onClick) { if (onClick) {
return ( return (
<Tooltip content={tooltip}> <Tooltip content={tooltip} placement="bottom">
<button <button
className={`btn navbar-button navbar-button--${classSuffix}`} className={`btn navbar-button navbar-button--${classSuffix}`}
onClick={onClick} onClick={onClick}
aria-label={e2e.pages.Dashboard.Toolbar.selectors.toolbarItems(tooltip)} aria-label={e2e.pages.Dashboard.Toolbar.selectors.toolbarItems(tooltip)}
> >
<i className={icon} /> {icon && <i className={icon} />}
{children}
</button> </button>
</Tooltip> </Tooltip>
); );
} }
return ( return (
<Tooltip content={tooltip}> <Tooltip content={tooltip} placement="bottom">
<a className={`btn navbar-button navbar-button--${classSuffix}`} href={href}> <a className={`btn navbar-button navbar-button--${classSuffix}`} href={href}>
<i className={icon} /> {icon && <i className={icon} />}
{children}
</a> </a>
</Tooltip> </Tooltip>
); );

View File

@ -1,24 +1,48 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState, CSSProperties } from 'react';
import Transition from 'react-transition-group/Transition';
import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin } from '@grafana/data'; import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin } from '@grafana/data';
import { DashboardModel, PanelModel } from '../../state'; import { DashboardModel, PanelModel } from '../../state';
import { CustomScrollbar, stylesFactory, Tab, TabContent, TabsBar, useTheme, Container } from '@grafana/ui'; import {
CustomScrollbar,
stylesFactory,
Tab,
TabContent,
TabsBar,
useTheme,
Container,
Forms,
Icon,
} from '@grafana/ui';
import { DefaultFieldConfigEditor, OverrideFieldConfigEditor } from './FieldConfigEditor'; import { DefaultFieldConfigEditor, OverrideFieldConfigEditor } from './FieldConfigEditor';
import { AngularPanelOptions } from './AngularPanelOptions'; import { AngularPanelOptions } from './AngularPanelOptions';
import { css } from 'emotion'; import { css } from 'emotion';
import { GeneralPanelOptions } from './GeneralPanelOptions'; import { GeneralPanelOptions } from './GeneralPanelOptions';
import { PanelOptionsEditor } from './PanelOptionsEditor'; import { PanelOptionsEditor } from './PanelOptionsEditor';
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
export const OptionsPaneContent: React.FC<{ export const OptionsPaneContent: React.FC<{
plugin?: PanelPlugin; plugin?: PanelPlugin;
panel: PanelModel; panel: PanelModel;
data: PanelData; data: PanelData;
dashboard: DashboardModel; dashboard: DashboardModel;
onClose: () => void;
onFieldConfigsChange: (config: FieldConfigSource) => void; onFieldConfigsChange: (config: FieldConfigSource) => void;
onPanelOptionsChanged: (options: any) => void; onPanelOptionsChanged: (options: any) => void;
onPanelConfigChange: (configKey: string, value: any) => void; onPanelConfigChange: (configKey: string, value: any) => void;
}> = ({ plugin, panel, data, onFieldConfigsChange, onPanelOptionsChanged, onPanelConfigChange, dashboard }) => { }> = ({
plugin,
panel,
data,
onFieldConfigsChange,
onPanelOptionsChanged,
onPanelConfigChange,
onClose,
dashboard,
}) => {
const theme = useTheme(); const theme = useTheme();
const styles = getStyles(theme); const styles = getStyles(theme);
const [activeTab, setActiveTab] = useState('defaults');
const [isSearching, setSearchMode] = useState(false);
const renderFieldOptions = useCallback( const renderFieldOptions = useCallback(
(plugin: PanelPlugin) => { (plugin: PanelPlugin) => {
@ -105,16 +129,75 @@ export const OptionsPaneContent: React.FC<{
[data, plugin, panel, onFieldConfigsChange] [data, plugin, panel, onFieldConfigsChange]
); );
const [activeTab, setActiveTab] = useState('defaults'); const renderSearchInput = useCallback(() => {
const defaultStyles = {
transition: 'width 50ms ease-in-out',
width: '50%',
display: 'flex',
};
const transitionStyles: { [str: string]: CSSProperties } = {
entered: { width: '100%' },
};
return (
<Transition in={true} timeout={0} appear={true}>
{state => {
return (
<div className={styles.searchWrapper}>
<div style={{ ...defaultStyles, ...transitionStyles[state] }}>
<Forms.Input
className={styles.searchInput}
type="text"
prefix={<Icon name="search" />}
ref={elem => elem && elem.focus()}
placeholder="Search all options"
suffix={
<Icon name="remove" onClick={() => setSearchMode(false)} className={styles.searchRemoveIcon} />
}
/>
</div>
</div>
);
}}
</Transition>
);
}, []);
return ( return (
<div className={styles.panelOptionsPane}> <div className={styles.panelOptionsPane}>
{plugin && ( {plugin && (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<TabsBar> <TabsBar className={styles.tabsBar}>
<Tab label="Options" active={activeTab === 'defaults'} onChangeTab={() => setActiveTab('defaults')} /> {isSearching && renderSearchInput()}
<Tab label="Overrides" active={activeTab === 'overrides'} onChangeTab={() => setActiveTab('overrides')} /> {!isSearching && (
<Tab label="General" active={activeTab === 'panel'} onChangeTab={() => setActiveTab('panel')} /> <>
<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')} />
<div className="flex-grow-1" />
<div className={styles.tabsButton}>
<DashNavButton
icon="fa fa-search"
tooltip="Search all options"
classSuffix="search-options"
onClick={() => setSearchMode(true)}
/>
</div>
<div className={styles.tabsButton}>
<DashNavButton
icon="fa fa-chevron-right"
tooltip="Close options pane"
classSuffix="close-options"
onClick={onClose}
/>
</div>
</>
)}
</TabsBar> </TabsBar>
<TabContent className={styles.tabContent}> <TabContent className={styles.tabContent}>
<CustomScrollbar> <CustomScrollbar>
@ -135,11 +218,26 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
padding-top: ${theme.spacing.sm};
`, `,
panelOptionsPane: css` panelOptionsPane: css`
height: 100%; height: 100%;
width: 100%; width: 100%;
border-bottom: none; `,
tabsBar: css`
padding-right: ${theme.spacing.sm};
`,
searchWrapper: css`
display: flex;
flex-grow: 1;
flex-direction: row-reverse;
`,
searchInput: css`
color: ${theme.colors.textWeak};
flex-grow: 1;
`,
searchRemoveIcon: css`
cursor: pointer;
`, `,
tabContent: css` tabContent: css`
padding: 0; padding: 0;
@ -150,6 +248,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
background: ${theme.colors.pageBg}; background: ${theme.colors.pageBg};
border-left: 1px solid ${theme.colors.pageHeaderBorder}; border-left: 1px solid ${theme.colors.pageHeaderBorder};
`, `,
tabsButton: css``,
legacyOptions: css` legacyOptions: css`
label: legacy-options; label: legacy-options;
.panel-options-grid { .panel-options-grid {

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin, SelectableValue } from '@grafana/data'; import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
import { Forms, stylesFactory, Button } from '@grafana/ui'; import { Forms, stylesFactory, Icon } 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 AutoSizer from 'react-virtualized-auto-sizer';
@ -25,6 +25,7 @@ import { PanelEditorUIState, setDiscardChanges } from './state/reducers';
import { getPanelEditorTabs } from './state/selectors'; import { getPanelEditorTabs } from './state/selectors';
import { getPanelStateById } from '../../state/selectors'; import { getPanelStateById } from '../../state/selectors';
import { OptionsPaneContent } from './OptionsPaneContent'; import { OptionsPaneContent } from './OptionsPaneContent';
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
enum Pane { enum Pane {
Right, Right,
@ -155,28 +156,31 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
onDragStarted={this.onDragStarted} onDragStarted={this.onDragStarted}
onDragFinished={size => this.onDragFinished(Pane.Top, size)} onDragFinished={size => this.onDragFinished(Pane.Top, size)}
> >
<div className={styles.panelWrapper}> <div className={styles.mainPaneWrapper}>
<AutoSizer> {this.renderToolbar(styles)}
{({ width, height }) => { <div className={styles.panelWrapper}>
if (width < 3 || height < 3) { <AutoSizer>
return null; {({ width, height }) => {
} if (width < 3 || height < 3) {
return ( return null;
<div className={styles.centeringContainer} style={{ width, height }}> }
<div style={calculatePanelSize(uiState.mode, width, height, panel)}> return (
<DashboardPanel <div className={styles.centeringContainer} style={{ width, height }}>
dashboard={dashboard} <div style={calculatePanelSize(uiState.mode, width, height, panel)}>
panel={panel} <DashboardPanel
isEditing={false} dashboard={dashboard}
isInEditMode panel={panel}
isFullscreen={false} isEditing={false}
isInView={true} isInEditMode
/> isFullscreen={false}
isInView={true}
/>
</div>
</div> </div>
</div> );
); }}
}} </AutoSizer>
</AutoSizer> </div>
</div> </div>
<div className={styles.tabsWrapper}> <div className={styles.tabsWrapper}>
<PanelEditorTabs panel={panel} dashboard={dashboard} tabs={tabs} onChangeTab={this.onChangeTab} data={data} /> <PanelEditorTabs panel={panel} dashboard={dashboard} tabs={tabs} onChangeTab={this.onChangeTab} data={data} />
@ -185,9 +189,8 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
); );
} }
renderToolbar() { renderToolbar(styles: any) {
const { dashboard, location, uiState } = this.props; const { dashboard, location, uiState } = this.props;
const styles = getStyles(config.theme);
return ( return (
<div className={styles.toolbar}> <div className={styles.toolbar}>
@ -197,9 +200,9 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
</div> </div>
<div className={styles.toolbarLeft}> <div className={styles.toolbarLeft}>
<div className={styles.toolbarItem}> <div className={styles.toolbarItem}>
<Button className={styles.toolbarItem} variant="secondary" onClick={this.onDiscard}> <DashNavButton tooltip="Discard all changes and return to dashboard" onClick={this.onDiscard}>
Discard changes Discard changes
</Button> </DashNavButton>
</div> </div>
<div className={styles.toolbarItem}> <div className={styles.toolbarItem}>
<Forms.Select <Forms.Select
@ -211,20 +214,23 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
<div className={styles.toolbarItem}> <div className={styles.toolbarItem}>
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} /> <DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
</div> </div>
<div className={styles.toolbarItem}> {!uiState.isPanelOptionsVisible && (
<Button <div className={styles.toolbarItem}>
className={styles.toolbarItem} <DashNavButton
icon="fa fa-sliders" onClick={this.onTogglePanelOptions}
variant="secondary" tooltip="Open options pane"
onClick={this.onTogglePanelOptions} classSuffix="close-options"
/> >
</div> <Icon name="chevron-left" /> <span style={{ paddingLeft: '6px' }}>Show options</span>
</DashNavButton>
</div>
)}
</div> </div>
</div> </div>
); );
} }
renderOptionsPane() { renderOptionsPane(styles: any) {
const { plugin, dashboard, data, panel } = this.props; const { plugin, dashboard, data, panel } = this.props;
if (!plugin) { if (!plugin) {
@ -237,6 +243,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
dashboard={dashboard} dashboard={dashboard}
data={data} data={data}
panel={panel} panel={panel}
onClose={this.onTogglePanelOptions}
onFieldConfigsChange={this.onFieldConfigChange} onFieldConfigsChange={this.onFieldConfigChange}
onPanelOptionsChanged={this.onPanelOptionsChanged} onPanelOptionsChanged={this.onPanelOptionsChanged}
onPanelConfigChange={this.onPanelConfigChanged} onPanelConfigChange={this.onPanelConfigChanged}
@ -259,14 +266,14 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
onDragFinished={size => this.onDragFinished(Pane.Right, size)} onDragFinished={size => this.onDragFinished(Pane.Right, size)}
> >
{this.renderHorizontalSplit(styles)} {this.renderHorizontalSplit(styles)}
{this.renderOptionsPane()} {this.renderOptionsPane(styles)}
</SplitPane> </SplitPane>
); );
} }
render() { render() {
const { initDone, uiState } = this.props; const { initDone, uiState } = this.props;
const styles = getStyles(config.theme); const styles = getStyles(config.theme, this.props);
if (!initDone) { if (!initDone) {
return null; return null;
@ -275,10 +282,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
return ( return (
<NewPanelEditorContext.Provider value={true}> <NewPanelEditorContext.Provider value={true}>
<div className={styles.wrapper}> <div className={styles.wrapper}>
{this.renderToolbar()} {uiState.isPanelOptionsVisible ? this.renderWithOptionsPane(styles) : this.renderHorizontalSplit(styles)}
<div className={styles.panesWrapper}>
{uiState.isPanelOptionsVisible ? this.renderWithOptionsPane(styles) : this.renderHorizontalSplit(styles)}
</div>
</div> </div>
</NewPanelEditorContext.Provider> </NewPanelEditorContext.Provider>
); );
@ -313,7 +317,8 @@ export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEdi
/* /*
* Styles * Styles
*/ */
const getStyles = stylesFactory((theme: GrafanaTheme) => { const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
const { uiState } = props;
const handleColor = theme.colors.blueLight; const handleColor = theme.colors.blueLight;
const paneSpaceing = theme.spacing.md; const paneSpaceing = theme.spacing.md;
@ -347,16 +352,18 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
`, `,
panesWrapper: css` mainPaneWrapper: css`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
padding-right: ${uiState.isPanelOptionsVisible ? 0 : paneSpaceing};
`,
panelWrapper: css`
flex: 1 1 0; flex: 1 1 0;
min-height: 0; min-height: 0;
width: 100%;
position: relative;
`,
panelWrapper: css`
width: 100%; width: 100%;
padding-left: ${paneSpaceing}; padding-left: ${paneSpaceing};
height: 100%;
`, `,
resizerV: cx( resizerV: cx(
resizer, resizer,
@ -384,6 +391,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
toolbar: css` toolbar: css`
display: flex; display: flex;
padding: ${theme.spacing.sm}; padding: ${theme.spacing.sm};
padding-right: 0;
justify-content: space-between; justify-content: space-between;
`, `,
toolbarLeft: css` toolbarLeft: css`

View File

@ -71,7 +71,7 @@ const getPanelEditorTabsStyles = stylesFactory(() => {
height: 100%; height: 100%;
`, `,
tabBar: css` tabBar: css`
padding-left: ${theme.spacing.sm}; padding-left: ${theme.spacing.md};
`, `,
tabContent: css` tabContent: css`
padding: 0; padding: 0;
@ -80,6 +80,7 @@ const getPanelEditorTabsStyles = stylesFactory(() => {
flex-grow: 1; flex-grow: 1;
min-height: 0; min-height: 0;
background: ${theme.colors.panelBg}; background: ${theme.colors.panelBg};
border-right: 1px solid ${theme.colors.pageHeaderBorder};
.toolbar { .toolbar {
background: transparent; background: transparent;