From 376ce0db87e5e8e26a3d05b15261f8a1b5a39174 Mon Sep 17 00:00:00 2001
From: Andrew Watkins <watts@influxdb.com>
Date: Mon, 26 Mar 2018 15:53:29 -0700
Subject: [PATCH] Introduce filtering to FuncButton

---
 ui/src/ifql/components/FuncsButton.tsx        | 43 +++++++++----
 .../{DropdownInput.js => DropdownInput.tsx}   | 30 +++++-----
 ui/test/ifql/components/FuncsButton.test.tsx  | 60 ++++++++++++++++++-
 3 files changed, 105 insertions(+), 28 deletions(-)
 rename ui/src/shared/components/{DropdownInput.js => DropdownInput.tsx} (60%)

diff --git a/ui/src/ifql/components/FuncsButton.tsx b/ui/src/ifql/components/FuncsButton.tsx
index 08e6097948..87abce896e 100644
--- a/ui/src/ifql/components/FuncsButton.tsx
+++ b/ui/src/ifql/components/FuncsButton.tsx
@@ -1,29 +1,31 @@
-import React, {PureComponent} from 'react'
+import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
 
 import FancyScrollbar from 'src/shared/components/FancyScrollbar'
+import DropdownInput from 'src/shared/components/DropdownInput'
 
 import OnClickOutside from 'src/shared/components/OnClickOutside'
 
 interface State {
   isOpen: boolean
+  inputText: string
 }
 
 interface Props {
   funcs: string[]
 }
 
-class FuncsButton extends PureComponent<Props, State> {
+export class FuncsButton extends PureComponent<Props, State> {
   constructor(props) {
     super(props)
 
     this.state = {
       isOpen: false,
+      inputText: '',
     }
   }
 
   public render() {
-    const {isOpen} = this.state
-    const {funcs} = this.props
+    const {isOpen, inputText} = this.state
 
     return (
       <div className={`dropdown dashboard-switcher ${isOpen ? 'open' : ''}`}>
@@ -34,14 +36,17 @@ class FuncsButton extends PureComponent<Props, State> {
           <span className="icon plus" />
         </button>
         <ul className="dropdown-menu funcs">
+          <DropdownInput
+            buttonSize="btn-xs"
+            buttonColor="btn-default"
+            onFilterChange={this.handleInputChange}
+            onFilterKeyPress={this.handleKeyDown}
+            searchTerm={inputText}
+          />
           <FancyScrollbar autoHide={false} autoHeight={true} maxHeight={240}>
             {isOpen &&
-              funcs.map((func, i) => (
-                <li
-                  className="dropdown-item func"
-                  data-test="func-item"
-                  key={i}
-                >
+              this.availableFuncs.map((func, i) => (
+                <li className="dropdown-item func" key={i}>
                   <a>{func}</a>
                 </li>
               ))}
@@ -51,6 +56,24 @@ class FuncsButton extends PureComponent<Props, State> {
     )
   }
 
+  private get availableFuncs(): string[] {
+    return this.props.funcs.filter(f =>
+      f.toLowerCase().includes(this.state.inputText)
+    )
+  }
+
+  private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
+    this.setState({inputText: e.target.value})
+  }
+
+  private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
+    if (e.key !== 'Escape') {
+      return
+    }
+
+    this.setState({inputText: '', isOpen: false})
+  }
+
   private handleClick = () => {
     this.setState({isOpen: !this.state.isOpen})
   }
diff --git a/ui/src/shared/components/DropdownInput.js b/ui/src/shared/components/DropdownInput.tsx
similarity index 60%
rename from ui/src/shared/components/DropdownInput.js
rename to ui/src/shared/components/DropdownInput.tsx
index 279642c4a3..84f92cfaaa 100644
--- a/ui/src/shared/components/DropdownInput.js
+++ b/ui/src/shared/components/DropdownInput.tsx
@@ -1,9 +1,21 @@
-import React from 'react'
-import PropTypes from 'prop-types'
+import React, {SFC, ChangeEvent, KeyboardEvent} from 'react'
 
 const disabledClass = disabled => (disabled ? ' disabled' : '')
 
-const DropdownInput = ({
+type OnFilterChangeHandler = (e: ChangeEvent<HTMLInputElement>) => void
+type OnFilterKeyPress = (e: KeyboardEvent<HTMLInputElement>) => void
+
+interface Props {
+  searchTerm: string
+  buttonSize: string
+  buttonColor: string
+  toggleStyle?: object
+  disabled?: boolean
+  onFilterChange: OnFilterChangeHandler
+  onFilterKeyPress: OnFilterKeyPress
+}
+
+const DropdownInput: SFC<Props> = ({
   searchTerm,
   buttonSize,
   buttonColor,
@@ -33,15 +45,3 @@ const DropdownInput = ({
 )
 
 export default DropdownInput
-
-const {bool, func, shape, string} = PropTypes
-
-DropdownInput.propTypes = {
-  searchTerm: string,
-  buttonSize: string,
-  buttonColor: string,
-  toggleStyle: shape({}),
-  disabled: bool,
-  onFilterChange: func.isRequired,
-  onFilterKeyPress: func.isRequired,
-}
diff --git a/ui/test/ifql/components/FuncsButton.test.tsx b/ui/test/ifql/components/FuncsButton.test.tsx
index 72a943c034..6882c84bd0 100644
--- a/ui/test/ifql/components/FuncsButton.test.tsx
+++ b/ui/test/ifql/components/FuncsButton.test.tsx
@@ -1,6 +1,7 @@
 import React from 'react'
 import {shallow} from 'enzyme'
-import FuncsButton from 'src/ifql/components/FuncsButton'
+import {FuncsButton} from 'src/ifql/components/FuncsButton'
+import DropdownInput from 'src/shared/components/DropdownInput'
 
 const setup = (override = {}) => {
   const props = {
@@ -39,14 +40,67 @@ describe('IFQL.Components.FuncsButton', () => {
       it('displays the list of functions', () => {
         const {wrapper} = setup()
 
-        wrapper.simulate('click')
+        const dropdownButton = wrapper.find('button')
+        dropdownButton.simulate('click')
 
-        const list = wrapper.find({'data-test': 'func-item'})
+        const list = wrapper.find('.func')
 
         expect(list.length).toBe(2)
         expect(list.first().text()).toBe('f1')
         expect(list.last().text()).toBe('f2')
       })
     })
+
+    describe('filtering the list', () => {
+      it('displays the filtered funcs', () => {
+        const {wrapper} = setup()
+
+        const dropdownButton = wrapper.find('button')
+        dropdownButton.simulate('click')
+
+        let list = wrapper.find('.func')
+
+        expect(list.length).toBe(2)
+        expect(list.first().text()).toBe('f1')
+        expect(list.last().text()).toBe('f2')
+
+        const input = wrapper
+          .find(DropdownInput)
+          .dive()
+          .find('input')
+
+        input.simulate('change', {target: {value: '2'}})
+        wrapper.update()
+
+        list = wrapper.find('.func')
+
+        expect(list.length).toBe(1)
+        expect(list.first().text()).toBe('f2')
+      })
+    })
+
+    describe('exiting the list', () => {
+      it('closes when ESC is pressed', () => {
+        const {wrapper} = setup()
+
+        const dropdownButton = wrapper.find('button')
+        dropdownButton.simulate('click')
+        let list = wrapper.find('.func')
+
+        expect(list.exists()).toBe(true)
+
+        const input = wrapper
+          .find(DropdownInput)
+          .dive()
+          .find('input')
+
+        input.simulate('keyDown', {key: 'Escape'})
+        wrapper.update()
+
+        list = wrapper.find('.func')
+
+        expect(list.exists()).toBe(false)
+      })
+    })
   })
 })