Commit fd409f11 by Marcus Efraimsson Committed by GitHub

Merge pull request #10986 from grafana/10427_addpanel_filter

add tabs and search filter to new panel control
parents 0ad63366 606257a1
...@@ -54,6 +54,10 @@ export default class ScrollBar extends React.Component<Props, any> { ...@@ -54,6 +54,10 @@ export default class ScrollBar extends React.Component<Props, any> {
return false; return false;
} }
update() {
this.scrollbar.update();
}
handleRef = ref => { handleRef = ref => {
this.container = ref; this.container = ref;
}; };
......
import React from 'react'; import React from 'react';
import _ from 'lodash'; import _ from 'lodash';
import classNames from 'classnames';
import config from 'app/core/config'; import config from 'app/core/config';
import { PanelModel } from '../panel_model'; import { PanelModel } from '../panel_model';
import { PanelContainer } from './PanelContainer'; import { PanelContainer } from './PanelContainer';
import ScrollBar from 'app/core/components/ScrollBar/ScrollBar'; import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
import store from 'app/core/store'; import store from 'app/core/store';
import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import Highlighter from 'react-highlight-words';
export interface AddPanelPanelProps { export interface AddPanelPanelProps {
panel: PanelModel; panel: PanelModel;
...@@ -16,21 +17,42 @@ export interface AddPanelPanelProps { ...@@ -16,21 +17,42 @@ export interface AddPanelPanelProps {
export interface AddPanelPanelState { export interface AddPanelPanelState {
filter: string; filter: string;
panelPlugins: any[]; panelPlugins: any[];
copiedPanelPlugins: any[];
tab: string;
} }
export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelPanelState> { export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelPanelState> {
private scrollbar: ScrollBar;
constructor(props) { constructor(props) {
super(props); super(props);
this.handleCloseAddPanel = this.handleCloseAddPanel.bind(this); this.handleCloseAddPanel = this.handleCloseAddPanel.bind(this);
this.renderPanelItem = this.renderPanelItem.bind(this); this.renderPanelItem = this.renderPanelItem.bind(this);
this.panelSizeChanged = this.panelSizeChanged.bind(this);
this.state = { this.state = {
panelPlugins: this.getPanelPlugins(), panelPlugins: this.getPanelPlugins(''),
copiedPanelPlugins: this.getCopiedPanelPlugins(''),
filter: '', filter: '',
tab: 'Add',
}; };
} }
getPanelPlugins() { componentDidMount() {
this.props.panel.events.on('panel-size-changed', this.panelSizeChanged);
}
componentWillUnmount() {
this.props.panel.events.off('panel-size-changed', this.panelSizeChanged);
}
panelSizeChanged() {
setTimeout(() => {
this.scrollbar.update();
});
}
getPanelPlugins(filter) {
let panels = _.chain(config.panels) let panels = _.chain(config.panels)
.filter({ hideFromList: false }) .filter({ hideFromList: false })
.map(item => item) .map(item => item)
...@@ -39,6 +61,19 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP ...@@ -39,6 +61,19 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
// add special row type // add special row type
panels.push({ id: 'row', name: 'Row', sort: 8, info: { logos: { small: 'public/img/icn-row.svg' } } }); panels.push({ id: 'row', name: 'Row', sort: 8, info: { logos: { small: 'public/img/icn-row.svg' } } });
panels = this.filterPanels(panels, filter);
// add sort by sort property
return _.sortBy(panels, 'sort');
}
getCopiedPanelPlugins(filter) {
let panels = _.chain(config.panels)
.filter({ hideFromList: false })
.map(item => item)
.value();
let copiedPanels = [];
let copiedPanelJson = store.get(LS_PANEL_COPY_KEY); let copiedPanelJson = store.get(LS_PANEL_COPY_KEY);
if (copiedPanelJson) { if (copiedPanelJson) {
let copiedPanel = JSON.parse(copiedPanelJson); let copiedPanel = JSON.parse(copiedPanelJson);
...@@ -48,12 +83,13 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP ...@@ -48,12 +83,13 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
pluginCopy.name = copiedPanel.title; pluginCopy.name = copiedPanel.title;
pluginCopy.sort = -1; pluginCopy.sort = -1;
pluginCopy.defaults = copiedPanel; pluginCopy.defaults = copiedPanel;
panels.push(pluginCopy); copiedPanels.push(pluginCopy);
} }
} }
// add sort by sort property copiedPanels = this.filterPanels(copiedPanels, filter);
return _.sortBy(panels, 'sort');
return _.sortBy(copiedPanels, 'sort');
} }
onAddPanel = panelPluginInfo => { onAddPanel = panelPluginInfo => {
...@@ -92,28 +128,117 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP ...@@ -92,28 +128,117 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
dashboard.removePanel(dashboard.panels[0]); dashboard.removePanel(dashboard.panels[0]);
} }
renderText(text: string) {
let searchWords = this.state.filter.split('');
return <Highlighter highlightClassName="highlight-search-match" textToHighlight={text} searchWords={searchWords} />;
}
renderPanelItem(panel, index) { renderPanelItem(panel, index) {
return ( return (
<div key={index} className="add-panel__item" onClick={() => this.onAddPanel(panel)} title={panel.name}> <div key={index} className="add-panel__item" onClick={() => this.onAddPanel(panel)} title={panel.name}>
<img className="add-panel__item-img" src={panel.info.logos.small} /> <img className="add-panel__item-img" src={panel.info.logos.small} />
<div className="add-panel__item-name">{panel.name}</div> <div className="add-panel__item-name">{this.renderText(panel.name)}</div>
</div> </div>
); );
} }
noCopiedPanelPlugins() {
return <div className="add-panel__no-panels">No copied panels yet.</div>;
}
filterChange(evt) {
this.setState({
filter: evt.target.value,
panelPlugins: this.getPanelPlugins(evt.target.value),
copiedPanelPlugins: this.getCopiedPanelPlugins(evt.target.value),
});
}
filterPanels(panels, filter) {
let regex = new RegExp(filter, 'i');
return panels.filter(panel => {
return regex.test(panel.name);
});
}
openCopy() {
this.setState({
tab: 'Copy',
filter: '',
panelPlugins: this.getPanelPlugins(''),
copiedPanelPlugins: this.getCopiedPanelPlugins(''),
});
}
openAdd() {
this.setState({
tab: 'Add',
filter: '',
panelPlugins: this.getPanelPlugins(''),
copiedPanelPlugins: this.getCopiedPanelPlugins(''),
});
}
render() { render() {
let addClass = classNames({
'active active--panel': this.state.tab === 'Add',
'': this.state.tab === 'Copy',
});
let copyClass = classNames({
'': this.state.tab === 'Add',
'active active--panel': this.state.tab === 'Copy',
});
let panelTab;
if (this.state.tab === 'Add') {
panelTab = this.state.panelPlugins.map(this.renderPanelItem);
} else if (this.state.tab === 'Copy') {
if (this.state.copiedPanelPlugins.length > 0) {
panelTab = this.state.copiedPanelPlugins.map(this.renderPanelItem);
} else {
panelTab = this.noCopiedPanelPlugins();
}
}
return ( return (
<div className="panel-container add-panel-container"> <div className="panel-container add-panel-container">
<div className="add-panel"> <div className="add-panel">
<div className="add-panel__header"> <div className="add-panel__header">
<i className="gicon gicon-add-panel" /> <i className="gicon gicon-add-panel" />
<span className="add-panel__title">New Panel</span> <span className="add-panel__title">New Panel</span>
<span className="add-panel__sub-title">Select a visualization</span> <ul className="gf-tabs">
<li className="gf-tabs-item">
<div className={'gf-tabs-link pointer ' + addClass} onClick={this.openAdd.bind(this)}>
Add
</div>
</li>
<li className="gf-tabs-item">
<div className={'gf-tabs-link pointer ' + copyClass} onClick={this.openCopy.bind(this)}>
Paste
</div>
</li>
</ul>
<button className="add-panel__close" onClick={this.handleCloseAddPanel}> <button className="add-panel__close" onClick={this.handleCloseAddPanel}>
<i className="fa fa-close" /> <i className="fa fa-close" />
</button> </button>
</div> </div>
<ScrollBar className="add-panel__items">{this.state.panelPlugins.map(this.renderPanelItem)}</ScrollBar> <ScrollBar ref={element => (this.scrollbar = element)} className="add-panel__items">
<div className="add-panel__searchbar">
<label className="gf-form gf-form--grow gf-form--has-input-icon">
<input
type="text"
className="gf-form-input max-width-20"
placeholder="Panel Search Filter"
value={this.state.filter}
onChange={this.filterChange.bind(this)}
/>
<i className="gf-form-input-icon fa fa-search" />
</label>
</div>
{panelTab}
</ScrollBar>
</div> </div>
</div> </div>
); );
......
import React from 'react';
import { AddPanelPanel } from './../dashgrid/AddPanelPanel';
import { PanelModel } from '../panel_model';
import { shallow } from 'enzyme';
import config from '../../../core/config';
jest.mock('app/core/store', () => ({
get: key => {
return null;
},
delete: key => {
return null;
},
}));
describe('AddPanelPanel', () => {
let wrapper, dashboardMock, getPanelContainer, panel;
beforeEach(() => {
config.panels = [
{
id: 'singlestat',
hideFromList: false,
name: 'Singlestat',
sort: 2,
info: {
logos: {
small: '',
},
},
},
{
id: 'hidden',
hideFromList: true,
name: 'Hidden',
sort: 100,
info: {
logos: {
small: '',
},
},
},
{
id: 'graph',
hideFromList: false,
name: 'Graph',
sort: 1,
info: {
logos: {
small: '',
},
},
},
{
id: 'alexander_zabbix',
hideFromList: false,
name: 'Zabbix',
sort: 100,
info: {
logos: {
small: '',
},
},
},
{
id: 'piechart',
hideFromList: false,
name: 'Piechart',
sort: 100,
info: {
logos: {
small: '',
},
},
},
];
dashboardMock = { toggleRow: jest.fn() };
getPanelContainer = jest.fn().mockReturnValue({
getDashboard: jest.fn().mockReturnValue(dashboardMock),
getPanelLoader: jest.fn(),
});
panel = new PanelModel({ collapsed: false });
wrapper = shallow(<AddPanelPanel panel={panel} getPanelContainer={getPanelContainer} />);
});
it('should fetch all panels sorted with core plugins first', () => {
//console.log(wrapper.debug());
//console.log(wrapper.find('.add-panel__item').get(0).props.title);
expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('Singlestat');
expect(wrapper.find('.add-panel__item').get(4).props.title).toBe('Piechart');
});
it('should filter', () => {
wrapper.find('input').simulate('change', { target: { value: 'p' } });
expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('Piechart');
expect(wrapper.find('.add-panel__item').get(0).props.title).toBe('Graph');
});
});
...@@ -194,8 +194,8 @@ export class PanelCtrl { ...@@ -194,8 +194,8 @@ export class PanelCtrl {
}); });
menu.push({ menu.push({
text: 'Add to Panel List', text: 'Copy',
click: 'ctrl.addToPanelList()', click: 'ctrl.copyPanel()',
role: 'Editor', role: 'Editor',
}); });
} }
...@@ -260,9 +260,9 @@ export class PanelCtrl { ...@@ -260,9 +260,9 @@ export class PanelCtrl {
}); });
} }
addToPanelList() { copyPanel() {
store.set(LS_PANEL_COPY_KEY, JSON.stringify(this.panel.getSaveModel())); store.set(LS_PANEL_COPY_KEY, JSON.stringify(this.panel.getSaveModel()));
appEvents.emit('alert-success', ['Panel temporarily added to panel list']); appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']);
} }
replacePanel(newPanel, oldPanel) { replacePanel(newPanel, oldPanel) {
......
...@@ -11,9 +11,12 @@ ...@@ -11,9 +11,12 @@
} }
.add-panel__header { .add-panel__header {
padding: 5px 15px; padding: 0 15px;
display: flex; display: flex;
align-items: center; align-items: center;
background: $page-header-bg;
box-shadow: $page-header-shadow;
border-bottom: 1px solid $page-header-border-color;
.gicon { .gicon {
font-size: 30px; font-size: 30px;
...@@ -31,7 +34,7 @@ ...@@ -31,7 +34,7 @@
.add-panel__title { .add-panel__title {
font-size: $font-size-md; font-size: $font-size-md;
margin-right: $spacer/2; margin-right: $spacer*2;
} }
.add-panel__sub-title { .add-panel__sub-title {
...@@ -47,6 +50,7 @@ ...@@ -47,6 +50,7 @@
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
overflow: auto; overflow: auto;
height: 100%;
align-content: flex-start; align-content: flex-start;
justify-content: space-around; justify-content: space-around;
position: relative; position: relative;
...@@ -84,3 +88,16 @@ ...@@ -84,3 +88,16 @@
.add-panel__item-icon { .add-panel__item-icon {
padding: 2px; padding: 2px;
} }
.add-panel__searchbar {
width: 100%;
margin-bottom: 10px;
margin-top: 7px;
}
.add-panel__no-panels {
color: $text-color-weak;
font-style: italic;
width: 100%;
padding: 3px 8px;
}
...@@ -44,18 +44,16 @@ ...@@ -44,18 +44,16 @@
&::before { &::before {
display: block; display: block;
content: " "; content: ' ';
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
height: 2px; height: 2px;
top: 0; top: 0;
background-image: linear-gradient( background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%);
to right,
#ffd500 0%,
#ff4400 99%,
#ff4400 100%
);
} }
} }
&.active--panel {
background: $panel-bg !important;
}
} }
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