Commit 59467d3c by Dominik Prokop Committed by GitHub

NewPanelEdit: add search, scroll and some layout to vis tab (#23253)

parent 540d1d9b
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};
`,
};
});
......
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));
`,
};
});
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;
......
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