mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
NewPanelEdit: add search, scroll and some layout to vis tab (#23253)
This commit is contained in:
parent
540d1d9b4e
commit
59467d3cb3
@ -1,7 +1,7 @@
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme, PanelPlugin, PanelPluginMeta } from '@grafana/data';
|
||||
import { useTheme, stylesFactory } from '@grafana/ui';
|
||||
import { CustomScrollbar, useTheme, stylesFactory, Forms, Icon } from '@grafana/ui';
|
||||
import { changePanelPlugin } from '../../state/actions';
|
||||
import { StoreState } from 'app/types';
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
@ -23,6 +23,7 @@ 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);
|
||||
|
||||
@ -36,15 +37,51 @@ export const VisualizationTabUnconnected: FC<Props> = ({ panel, plugin, changePa
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<VizTypePicker current={plugin.meta} onTypeChange={onPluginTypeChange} searchQuery={''} onClose={() => {}} />
|
||||
<div className={styles.search}>
|
||||
<Forms.Input
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.currentTarget.value)}
|
||||
prefix={<Icon name="filter" className={styles.icon} />}
|
||||
placeholder="Filter visualisations"
|
||||
autoFocus
|
||||
/>
|
||||
</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`
|
||||
color: ${theme.colors.gray33};
|
||||
`,
|
||||
wrapper: css`
|
||||
padding: ${theme.spacing.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
max-height: 100%;
|
||||
`,
|
||||
search: css`
|
||||
padding: ${theme.spacing.sm} ${theme.spacing.md};
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`,
|
||||
visList: css`
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding-left: ${theme.spacing.md};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import config from 'app/core/config';
|
||||
import VizTypePickerPlugin from './VizTypePickerPlugin';
|
||||
import { EmptySearchResult } from '@grafana/ui';
|
||||
import { PanelPluginMeta } from '@grafana/data';
|
||||
import { EmptySearchResult, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { GrafanaTheme, PanelPluginMeta } from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
|
||||
export interface Props {
|
||||
current: PanelPluginMeta;
|
||||
@ -12,62 +13,68 @@ export interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export class VizTypePicker extends PureComponent<Props> {
|
||||
searchInput: HTMLElement;
|
||||
pluginList = this.getPanelPlugins;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
get maxSelectedIndex() {
|
||||
const filteredPluginList = this.getFilteredPluginList();
|
||||
return filteredPluginList.length - 1;
|
||||
}
|
||||
|
||||
get getPanelPlugins(): PanelPluginMeta[] {
|
||||
export const VizTypePicker: React.FC<Props> = ({ searchQuery, onTypeChange, current }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
const pluginsList: PanelPluginMeta[] = useMemo(() => {
|
||||
const allPanels = config.panels;
|
||||
|
||||
return Object.keys(allPanels)
|
||||
.filter(key => allPanels[key]['hideFromList'] === false)
|
||||
.map(key => allPanels[key])
|
||||
.sort((a: PanelPluginMeta, b: PanelPluginMeta) => a.sort - b.sort);
|
||||
}
|
||||
}, []);
|
||||
|
||||
renderVizPlugin = (plugin: PanelPluginMeta, index: number) => {
|
||||
const { onTypeChange } = this.props;
|
||||
const isCurrent = plugin.id === this.props.current.id;
|
||||
const renderVizPlugin = (plugin: PanelPluginMeta, index: number) => {
|
||||
const isCurrent = plugin.id === current.id;
|
||||
const filteredPluginList = getFilteredPluginList();
|
||||
|
||||
const matchesQuery = filteredPluginList.indexOf(plugin) > -1;
|
||||
return (
|
||||
<VizTypePickerPlugin key={plugin.id} isCurrent={isCurrent} plugin={plugin} onClick={() => onTypeChange(plugin)} />
|
||||
<VizTypePickerPlugin
|
||||
disabled={!matchesQuery}
|
||||
key={plugin.id}
|
||||
isCurrent={isCurrent}
|
||||
plugin={plugin}
|
||||
onClick={() => onTypeChange(plugin)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getFilteredPluginList = (): PanelPluginMeta[] => {
|
||||
const { searchQuery } = this.props;
|
||||
const getFilteredPluginList = (): PanelPluginMeta[] => {
|
||||
const regex = new RegExp(searchQuery, 'i');
|
||||
const pluginList = this.pluginList;
|
||||
|
||||
const filtered = pluginList.filter(item => {
|
||||
return pluginsList.filter(item => {
|
||||
return regex.test(item.name);
|
||||
});
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
render() {
|
||||
const filteredPluginList = this.getFilteredPluginList();
|
||||
const hasResults = filteredPluginList.length > 0;
|
||||
return (
|
||||
<div className="viz-picker">
|
||||
<div className="viz-picker-list">
|
||||
{hasResults ? (
|
||||
filteredPluginList.map((plugin, index) => this.renderVizPlugin(plugin, index))
|
||||
) : (
|
||||
<EmptySearchResult>Could not find anything matching your query</EmptySearchResult>
|
||||
)}
|
||||
</div>
|
||||
const filteredPluginList = getFilteredPluginList();
|
||||
const hasResults = filteredPluginList.length > 0;
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.grid}>
|
||||
{hasResults ? (
|
||||
pluginsList.map((plugin, index) => renderVizPlugin(plugin, index))
|
||||
) : (
|
||||
<EmptySearchResult>Could not find anything matching your query</EmptySearchResult>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
wrapper: css`
|
||||
padding-right: ${theme.spacing.md};
|
||||
`,
|
||||
grid: css`
|
||||
max-width: 100%;
|
||||
display: grid;
|
||||
grid-gap: ${theme.spacing.md};
|
||||
grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -1,33 +1,90 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { PanelPluginMeta } from '@grafana/data';
|
||||
import { GrafanaTheme, PanelPluginMeta } from '@grafana/data';
|
||||
import { stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { css, cx } from 'emotion';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
interface Props {
|
||||
isCurrent: boolean;
|
||||
plugin: PanelPluginMeta;
|
||||
onClick: () => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const VizTypePickerPlugin = React.memo(
|
||||
({ isCurrent, plugin, onClick }: Props) => {
|
||||
const cssClass = classNames({
|
||||
'viz-picker__item': true,
|
||||
'viz-picker__item--current': isCurrent,
|
||||
});
|
||||
const VizTypePickerPlugin: React.FC<Props> = ({ isCurrent, plugin, onClick, disabled }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
const cssClass = cx({
|
||||
[styles.item]: true,
|
||||
[styles.current]: isCurrent,
|
||||
[styles.disabled]: disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cssClass} onClick={onClick} title={plugin.name}>
|
||||
<div className="viz-picker__item-name">{plugin.name}</div>
|
||||
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.isCurrent === nextProps.isCurrent) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div className={cssClass} onClick={disabled ? () => {} : onClick} title={plugin.name}>
|
||||
<div className={styles.name}>{plugin.name}</div>
|
||||
<img className={styles.img} src={plugin.info.logos.small} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
item: css`
|
||||
background: ${theme.isLight ? theme.colors.white : theme.colors.gray05};
|
||||
border: 1px solid ${theme.isLight ? theme.colors.gray3 : theme.colors.dark10};
|
||||
border-radius: 3px;
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 6px;
|
||||
transition: transform 1 ease;
|
||||
&:hover {
|
||||
box-shadow: 0 0 4px ${theme.colors.blueLight};
|
||||
background: ${theme.isLight
|
||||
? tinycolor(theme.colors.blueBase)
|
||||
.lighten(45)
|
||||
.toHexString()
|
||||
: tinycolor(theme.colors.blueBase)
|
||||
.darken(46)
|
||||
.toHexString()};
|
||||
border: 1px solid ${theme.colors.blueLight};
|
||||
}
|
||||
`,
|
||||
current: css`
|
||||
box-shadow: 0 0 6px ${theme.colors.orange} !important;
|
||||
border: 1px solid ${theme.colors.orange} !important;
|
||||
background: ${theme.isLight ? theme.colors.white : theme.colors.gray05};
|
||||
`,
|
||||
disabled: css`
|
||||
opacity: 0.2;
|
||||
filter: grayscale(1);
|
||||
cursor: pointer;
|
||||
`,
|
||||
name: css`
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
height: 23px;
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
`,
|
||||
img: css`
|
||||
height: 55px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export default VizTypePickerPlugin;
|
||||
|
@ -121,62 +121,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.viz-picker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.viz-picker-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.viz-picker__item {
|
||||
background: $panel-editor-viz-item-bg;
|
||||
border: $panel-editor-viz-item-border;
|
||||
border-radius: 3px;
|
||||
height: 100px;
|
||||
width: 145px;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 6px;
|
||||
transition: transform 1 ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $panel-editor-viz-item-shadow-hover;
|
||||
background: $panel-editor-viz-item-bg-hover;
|
||||
border: $panel-editor-viz-item-border-hover;
|
||||
}
|
||||
|
||||
&--current {
|
||||
box-shadow: 0 0 6px $orange !important;
|
||||
border: 1px solid $orange !important;
|
||||
background: $panel-editor-viz-item-bg !important;
|
||||
}
|
||||
}
|
||||
|
||||
.viz-picker__item-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: $font-size-sm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
height: 23px;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
|
||||
.viz-picker__item-img {
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.panel-editor-tabs {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
|
Loading…
Reference in New Issue
Block a user