mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 10:50:37 -06:00
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:
parent
9464115fc0
commit
b37e66f915
@ -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')}>
|
||||
|
@ -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 || []}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user