Selaa lähdekoodia

Merge pull request #10986 from grafana/10427_addpanel_filter

add tabs and search filter to new panel control
Marcus Efraimsson 7 vuotta sitten
vanhempi
commit
fd409f119d

+ 4 - 0
public/app/core/components/ScrollBar/ScrollBar.tsx

@@ -54,6 +54,10 @@ export default class ScrollBar extends React.Component<Props, any> {
     return false;
   }
 
+  update() {
+    this.scrollbar.update();
+  }
+
   handleRef = ref => {
     this.container = ref;
   };

+ 134 - 9
public/app/features/dashboard/dashgrid/AddPanelPanel.tsx

@@ -1,12 +1,13 @@
 import React from 'react';
 import _ from 'lodash';
-
+import classNames from 'classnames';
 import config from 'app/core/config';
 import { PanelModel } from '../panel_model';
 import { PanelContainer } from './PanelContainer';
 import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
 import store from 'app/core/store';
 import { LS_PANEL_COPY_KEY } from 'app/core/constants';
+import Highlighter from 'react-highlight-words';
 
 export interface AddPanelPanelProps {
   panel: PanelModel;
@@ -16,21 +17,42 @@ export interface AddPanelPanelProps {
 export interface AddPanelPanelState {
   filter: string;
   panelPlugins: any[];
+  copiedPanelPlugins: any[];
+  tab: string;
 }
 
 export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelPanelState> {
+  private scrollbar: ScrollBar;
+
   constructor(props) {
     super(props);
     this.handleCloseAddPanel = this.handleCloseAddPanel.bind(this);
     this.renderPanelItem = this.renderPanelItem.bind(this);
+    this.panelSizeChanged = this.panelSizeChanged.bind(this);
 
     this.state = {
-      panelPlugins: this.getPanelPlugins(),
+      panelPlugins: this.getPanelPlugins(''),
+      copiedPanelPlugins: this.getCopiedPanelPlugins(''),
       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)
       .filter({ hideFromList: false })
       .map(item => item)
@@ -39,6 +61,19 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
     // add special row type
     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);
     if (copiedPanelJson) {
       let copiedPanel = JSON.parse(copiedPanelJson);
@@ -48,12 +83,13 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
         pluginCopy.name = copiedPanel.title;
         pluginCopy.sort = -1;
         pluginCopy.defaults = copiedPanel;
-        panels.push(pluginCopy);
+        copiedPanels.push(pluginCopy);
       }
     }
 
-    // add sort by sort property
-    return _.sortBy(panels, 'sort');
+    copiedPanels = this.filterPanels(copiedPanels, filter);
+
+    return _.sortBy(copiedPanels, 'sort');
   }
 
   onAddPanel = panelPluginInfo => {
@@ -92,28 +128,117 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
     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) {
     return (
       <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} />
-        <div className="add-panel__item-name">{panel.name}</div>
+        <div className="add-panel__item-name">{this.renderText(panel.name)}</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() {
+    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 (
       <div className="panel-container add-panel-container">
         <div className="add-panel">
           <div className="add-panel__header">
             <i className="gicon gicon-add-panel" />
             <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}>
               <i className="fa fa-close" />
             </button>
           </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>
     );

+ 102 - 0
public/app/features/dashboard/specs/AddPanelPanel.jest.tsx

@@ -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');
+  });
+});

+ 4 - 4
public/app/features/panel/panel_ctrl.ts

@@ -194,8 +194,8 @@ export class PanelCtrl {
       });
 
       menu.push({
-        text: 'Add to Panel List',
-        click: 'ctrl.addToPanelList()',
+        text: 'Copy',
+        click: 'ctrl.copyPanel()',
         role: 'Editor',
       });
     }
@@ -260,9 +260,9 @@ export class PanelCtrl {
     });
   }
 
-  addToPanelList() {
+  copyPanel() {
     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) {

+ 19 - 2
public/sass/components/_panel_add_panel.scss

@@ -11,9 +11,12 @@
 }
 
 .add-panel__header {
-  padding: 5px 15px;
+  padding: 0 15px;
   display: flex;
   align-items: center;
+  background: $page-header-bg;
+  box-shadow: $page-header-shadow;
+  border-bottom: 1px solid $page-header-border-color;
 
   .gicon {
     font-size: 30px;
@@ -31,7 +34,7 @@
 
 .add-panel__title {
   font-size: $font-size-md;
-  margin-right: $spacer/2;
+  margin-right: $spacer*2;
 }
 
 .add-panel__sub-title {
@@ -47,6 +50,7 @@
   flex-direction: row;
   flex-wrap: wrap;
   overflow: auto;
+  height: 100%;
   align-content: flex-start;
   justify-content: space-around;
   position: relative;
@@ -84,3 +88,16 @@
 .add-panel__item-icon {
   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;
+}

+ 5 - 7
public/sass/components/_tabs.scss

@@ -44,18 +44,16 @@
 
     &::before {
       display: block;
-      content: " ";
+      content: ' ';
       position: absolute;
       left: 0;
       right: 0;
       height: 2px;
       top: 0;
-      background-image: linear-gradient(
-        to right,
-        #ffd500 0%,
-        #ff4400 99%,
-        #ff4400 100%
-      );
+      background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%);
     }
   }
+  &.active--panel {
+    background: $panel-bg !important;
+  }
 }