NewPanelEditor: Move visualisation picker to the sidebar pane (#23696)

* Move visualisation picker to the sidebar pane

* Remove vis tab from bottom pane

* Visualisation to panel type title
This commit is contained in:
Dominik Prokop 2020-04-20 20:29:32 +02:00 committed by GitHub
parent 9464115fc0
commit b37e66f915
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 92 additions and 85 deletions

View File

@ -1,4 +1,4 @@
import React, { useState, FC } from 'react';
import React, { useState, FC, useEffect } from 'react';
import { css, cx } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { useTheme, Icon, stylesFactory } from '@grafana/ui';
@ -9,6 +9,7 @@ interface Props {
defaultToClosed?: boolean;
className?: string;
nested?: boolean;
onToggle?: (isExpanded: boolean) => void;
}
export const OptionsGroup: FC<Props> = ({
@ -18,10 +19,16 @@ export const OptionsGroup: FC<Props> = ({
renderTitle,
className,
nested = false,
onToggle,
}) => {
const [isExpanded, toggleExpand] = useState(defaultToClosed ? false : true);
const theme = useTheme();
const styles = getStyles(theme, isExpanded, nested);
useEffect(() => {
if (onToggle) {
onToggle(isExpanded);
}
}, [isExpanded]);
return (
<div className={cx(styles.box, className, 'options-group')}>

View File

@ -8,7 +8,6 @@ import { DashboardModel } from '../../state';
import { QueriesTab } from '../../panel_editor/QueriesTab';
import { PanelModel } from '../../state/PanelModel';
import { AlertTab } from 'app/features/alerting/AlertTab';
import { VisualizationTab } from './VisualizationTab';
import { TransformationsEditor } from '../TransformationsEditor/TransformationsEditor';
interface PanelEditorTabsProps {
@ -66,7 +65,6 @@ export const PanelEditorTabs: React.FC<PanelEditorTabsProps> = ({ panel, dashboa
<TabContent className={styles.tabContent}>
{activeTab.id === PanelEditorTabId.Query && <QueriesTab panel={panel} dashboard={dashboard} />}
{activeTab.id === PanelEditorTabId.Alert && <AlertTab panel={panel} dashboard={dashboard} />}
{activeTab.id === PanelEditorTabId.Visualize && <VisualizationTab panel={panel} />}
{activeTab.id === PanelEditorTabId.Transform && data.state !== LoadingState.NotStarted && (
<TransformationsEditor
transformations={panel.transformations || []}

View File

@ -1,4 +1,4 @@
import React, { FC, useMemo } from 'react';
import React, { FC, useMemo, useRef } from 'react';
import { DashboardModel, PanelModel } from '../../state';
import { FieldConfigSource, PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
import { Counter, DataLinksInlineEditor, Field, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui';
@ -7,6 +7,7 @@ import { getPanelLinksVariableSuggestions } from '../../../panel/panellinks/link
import { getVariables } from '../../../variables/state/selectors';
import { PanelOptionsEditor } from './PanelOptionsEditor';
import { AngularPanelOptions } from './AngularPanelOptions';
import { VisualizationTab } from './VisualizationTab';
interface Props {
panel: PanelModel;
@ -27,8 +28,9 @@ export const PanelOptionsTab: FC<Props> = ({
onPanelOptionsChanged,
onFieldConfigsChange,
}) => {
const elements: JSX.Element[] = [];
const visTabInputRef = useRef<HTMLInputElement>();
const linkVariablesSuggestions = useMemo(() => getPanelLinksVariableSuggestions(), []);
const elements: JSX.Element[] = [];
const panelLinksCount = panel && panel.links ? panel.links.length : 0;
const variableOptions = getVariableOptions();
@ -39,9 +41,14 @@ export const PanelOptionsTab: FC<Props> = ({
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map(value => ({ label: value.toString(), value }));
const focusVisPickerInput = (isExpanded: boolean) => {
if (isExpanded && visTabInputRef.current) {
visTabInputRef.current.focus();
}
};
// Fist common panel settings Title, description
elements.push(
<OptionsGroup title="Basic" key="basic settings">
<OptionsGroup title="Panel settings" key="Panel settings">
<Field label="Panel title">
<Input defaultValue={panel.title} onBlur={e => onPanelConfigChange('title', e.currentTarget.value)} />
</Field>
@ -57,6 +64,12 @@ export const PanelOptionsTab: FC<Props> = ({
</OptionsGroup>
);
elements.push(
<OptionsGroup title="Panel type" key="Panel type" defaultToClosed onToggle={focusVisPickerInput}>
<VisualizationTab panel={panel} ref={visTabInputRef} />
</OptionsGroup>
);
// Old legacy react editor
if (plugin.editor && panel && !plugin.optionEditors) {
elements.push(

View File

@ -1,4 +1,4 @@
import React, { FC, useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { css } from 'emotion';
import { GrafanaTheme, PanelPlugin, PanelPluginMeta } from '@grafana/data';
import { CustomScrollbar, useTheme, stylesFactory, Icon, Input } from '@grafana/ui';
@ -23,70 +23,71 @@ interface DispatchProps {
type Props = OwnProps & ConnectedProps & DispatchProps;
export const VisualizationTabUnconnected: FC<Props> = ({ panel, plugin, changePanelPlugin }) => {
const [searchQuery, setSearchQuery] = useState('');
const theme = useTheme();
const styles = getStyles(theme);
export const VisualizationTabUnconnected = React.forwardRef<HTMLInputElement, Props>(
({ panel, plugin, changePanelPlugin }, ref) => {
const [searchQuery, setSearchQuery] = useState('');
const theme = useTheme();
const styles = getStyles(theme);
if (!plugin) {
return null;
}
if (!plugin) {
return null;
}
const onPluginTypeChange = (meta: PanelPluginMeta) => {
changePanelPlugin(panel, meta.id);
};
const onPluginTypeChange = (meta: PanelPluginMeta) => {
changePanelPlugin(panel, meta.id);
};
const onKeyPress = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
const query = e.currentTarget.value;
const plugins = getAllPanelPluginMeta();
const match = filterPluginList(plugins, query, plugin.meta);
if (match && match.length) {
onPluginTypeChange(match[0]);
const onKeyPress = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
const query = e.currentTarget.value;
const plugins = getAllPanelPluginMeta();
const match = filterPluginList(plugins, query, plugin.meta);
if (match && match.length) {
onPluginTypeChange(match[0]);
}
}
}
},
[onPluginTypeChange]
);
},
[onPluginTypeChange]
);
const suffix =
searchQuery !== '' ? (
<span className={styles.searchClear} onClick={() => setSearchQuery('')}>
<Icon name="times" />
Clear filter
</span>
) : null;
const suffix =
searchQuery !== '' ? (
<span className={styles.searchClear} onClick={() => setSearchQuery('')}>
<Icon name="times" />
Clear filter
</span>
) : null;
return (
<div className={styles.wrapper}>
<div className={styles.search}>
<Field label="Filters">
<Input
value={searchQuery}
onChange={e => setSearchQuery(e.currentTarget.value)}
onKeyPress={onKeyPress}
prefix={<Icon name="filter" className={styles.icon} />}
suffix={suffix}
placeholder="Filter visualisations"
autoFocus
/>
</Field>
return (
<div className={styles.wrapper}>
<div className={styles.search}>
<Field>
<Input
value={searchQuery}
onChange={e => setSearchQuery(e.currentTarget.value)}
onKeyPress={onKeyPress}
prefix={<Icon name="filter" className={styles.icon} />}
suffix={suffix}
placeholder="Filter visualisations"
ref={ref}
/>
</Field>
</div>
<div className={styles.visList}>
<CustomScrollbar>
<VizTypePicker
current={plugin.meta}
onTypeChange={onPluginTypeChange}
searchQuery={searchQuery}
onClose={() => {}}
/>
</CustomScrollbar>
</div>
</div>
<div className={styles.visList}>
<CustomScrollbar>
<VizTypePicker
current={plugin.meta}
onTypeChange={onPluginTypeChange}
searchQuery={searchQuery}
onClose={() => {}}
/>
</CustomScrollbar>
</div>
</div>
);
};
);
}
);
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
icon: css`
@ -97,7 +98,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
flex-direction: column;
flex-grow: 1;
max-height: 100%;
padding: ${theme.spacing.md};
`,
search: css`
flex-grow: 0;
@ -123,4 +123,6 @@ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { changePanelPlugin };
export const VisualizationTab = connect(mapStateToProps, mapDispatchToProps)(VisualizationTabUnconnected);
export const VisualizationTab = connect(mapStateToProps, mapDispatchToProps, undefined, { forwardRef: true })(
VisualizationTabUnconnected
);

View File

@ -30,13 +30,6 @@ export const getPanelEditorTabs = memoizeOne((location: LocationState, plugin?:
});
}
tabs.push({
id: PanelEditorTabId.Visualize,
text: 'Visualize',
icon: 'chart-bar',
active: false,
});
if (plugin.meta.id === 'graph') {
tabs.push({
id: PanelEditorTabId.Alert,

View File

@ -85,24 +85,18 @@ export const VizTypePicker: React.FC<Props> = ({ searchQuery, onTypeChange, curr
const renderList = filteredPluginList.concat(pluginsList.filter(p => filteredPluginList.indexOf(p) === -1));
return (
<div className={styles.wrapper}>
<div className={styles.grid}>
{hasResults ? (
renderList.map((plugin, index) => renderVizPlugin(plugin, index))
) : (
<EmptySearchResult>Could not find anything matching your query</EmptySearchResult>
)}
</div>
<div className={styles.grid}>
{hasResults ? (
renderList.map((plugin, index) => renderVizPlugin(plugin, index))
) : (
<EmptySearchResult>Could not find anything matching your query</EmptySearchResult>
)}
</div>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
wrapper: css`
// this needed here to make the box shadow not be clicked by the parent scroll container
padding-top: ${theme.spacing.md};
`,
grid: css`
max-width: 100%;
display: grid;