mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #10986 from grafana/10427_addpanel_filter
add tabs and search filter to new panel control
This commit is contained in:
commit
fd409f119d
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
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 {
|
|||||||
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
|
|||||||
// 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
|
|||||||
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
|
|||||||
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>
|
||||||
);
|
);
|
||||||
|
102
public/app/features/dashboard/specs/AddPanelPanel.jest.tsx
Normal file
102
public/app/features/dashboard/specs/AddPanelPanel.jest.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
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 {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.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 @@
|
|||||||
|
|
||||||
.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 @@
|
|||||||
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 @@
|
|||||||
.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 @@
|
|||||||
|
|
||||||
&::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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user