public inbox for [email protected]  
help / color / mirror / Atom feed
From: Pramod Ahire <[email protected]>
To: pgadmin-hackers <[email protected]>
Subject: Quick search for menu items & help articles
Date: Thu, 14 Jan 2021 16:54:05 +0530
Message-ID: <[email protected]> (raw)

Hi Team, 

 

Please find the attached designs & patch that contains complete functionality except below to do for quick search.

 

To Do: 

 
Unit test cases are not that sufficient to cover complete code, but will be working in background to cover up those one
In pgadmin, for disabled menu items we need to add info that will describe why menu has disabled & how it will be enabled. Either another way to enable all of them & show respective reason in popup that menu is disabled for. 
 

Please do let me know if I missed anything or suggestion of yours.

 

Thanks !

 

Pramod Ahire

Software Engineer

 

 

C: +91-020-66449600/601

D: +91-9028697679

edbpostgres.com

 



Attachments:

  [application/octet-stream] quick_search_v1.patch (33.9K, 3-quick_search_v1.patch)
  download | inline diff:
diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py
index 640a05468..672b3e646 100644
--- a/web/pgadmin/browser/register_browser_preferences.py
+++ b/web/pgadmin/browser/register_browser_preferences.py
@@ -443,6 +443,19 @@ def register_browser_preferences(self):
         fields=fields
     )
 
+    self.preference.register(
+        'keyboard_shortcuts', 'open_quick_search',
+        gettext('Quick Search'), 'keyboardshortcut',
+        {
+            'alt': False,
+            'shift': True,
+            'control': True,
+            'key': {'key_code': 70, 'char': 'f'}
+        },
+        category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
+        fields=fields
+    )
+
     self.dynamic_tab_title = self.preference.register(
         'tab settings', 'dynamic_tabs',
         gettext("Dynamic tab size"), 'boolean', False,
diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js
index 3381162d0..a69c7009f 100644
--- a/web/pgadmin/browser/static/js/keyboard.js
+++ b/web/pgadmin/browser/static/js/keyboard.js
@@ -44,6 +44,7 @@ _.extend(pgBrowser.keyboardNavigation, {
         'drop_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_multiple').value),
         'drop_cascade_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_cascade_multiple').value),
         'add_grid_row': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'add_grid_row').value),
+        'open_quick_search': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'open_quick_search').value),
 
       };
       this.shortcutMethods = {
@@ -66,6 +67,7 @@ _.extend(pgBrowser.keyboardNavigation, {
         'bindDropMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_multiple_objects}, // Grid Menu Drop Multiple
         'bindDropCascadeMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_cascade_multiple_objects}, // Grid Menu Drop Cascade Multiple
         'bindAddGridRow': {'shortcuts': this.keyboardShortcut.add_grid_row}, // Subnode Grid Add Row
+        'bindOpenQuickSearch': {'shortcuts': this.keyboardShortcut.open_quick_search}, // Subnode Grid Refresh Row
       };
       this.bindShortcuts();
     }
@@ -383,6 +385,9 @@ _.extend(pgBrowser.keyboardNavigation, {
 
     return new dialogTabNavigator.dialogTabNavigator(dialogContainer, backward_shortcut, forward_shortcut);
   },
+  bindOpenQuickSearch: function() {
+    $('#search_icon').trigger('click');
+  },
 });
 
 module.exports = pgAdmin.Browser.keyboardNavigation;
diff --git a/web/pgadmin/browser/static/js/quick_search.js b/web/pgadmin/browser/static/js/quick_search.js
new file mode 100644
index 000000000..82d95dfd4
--- /dev/null
+++ b/web/pgadmin/browser/static/js/quick_search.js
@@ -0,0 +1,28 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {Search} from './quick_search/trigger_search';
+
+// TODO: GUI, Add the logic to show loading screen while fetching result
+const onResultFetch = (url, data) => {
+  // URL can be used for displaying all the result in new page
+  // data will be array of search <name> -> <link>
+  console.warn('URL = ' + url);
+  console.warn(data);
+};
+
+// Entry point - Quick search functionality
+if (document.getElementById('quick-search-component')) {
+  ReactDOM.render(
+    <Search onResult={onResultFetch} />,
+    document.getElementById('quick-search-component')
+  );
+}
diff --git a/web/pgadmin/browser/static/js/quick_search/iframe_component.js b/web/pgadmin/browser/static/js/quick_search/iframe_component.js
new file mode 100644
index 000000000..bf9f8c059
--- /dev/null
+++ b/web/pgadmin/browser/static/js/quick_search/iframe_component.js
@@ -0,0 +1,44 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
+
+// Allow us to render IFrame using React
+// Here we will add the event listener on Iframe load event
+export class Iframe extends Component {
+  static get propTypes() {
+    return {
+      id: PropTypes.string.isRequired,
+      srcURL: PropTypes.string.isRequired,
+      onLoad: PropTypes.func.isRequired,
+    };
+  }
+
+  render () {
+    const iframeStyle = {
+      border: '0',
+      display: 'block',
+      position:'absolute',
+      opacity:'0',
+    };
+    const {id, srcURL, onLoad} = this.props;
+
+    return (
+      <iframe
+        id={id}
+        src={srcURL}
+        onLoad={onLoad}
+        width={'20'}
+        height={'20'}
+        style={iframeStyle}
+      />
+    );
+  }
+}
diff --git a/web/pgadmin/browser/static/js/quick_search/menuitems_help.js b/web/pgadmin/browser/static/js/quick_search/menuitems_help.js
new file mode 100644
index 000000000..b3225129a
--- /dev/null
+++ b/web/pgadmin/browser/static/js/quick_search/menuitems_help.js
@@ -0,0 +1,85 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import gettext from 'sources/gettext';
+
+const LAST_MENU = gettext('About pgAdmin 4');
+
+//  Allow us to
+const getMenuName = (item) => {
+  let aLinks = item.getElementsByTagName('a');
+  let name;
+  if (aLinks.length > 0) {
+    name = (aLinks[0].text).trim();
+    name = name.replace(/\.+$/g, '');
+  }
+  return name;
+};
+
+export function menuSearch(param, props) {
+  param = param.trim();
+  const setState = props.setState;
+  let result = [];
+  // Here we will add the matches
+  const parseLI = (_menu, path) => {
+    let _name = getMenuName(_menu);
+    if (_name && _name.toLowerCase().indexOf(param.toLowerCase()) != -1) {
+      let _res = {};
+      _res[_name] = path;
+      _res['element'] = _menu.children[0];
+      result.push(_res);
+    }
+    // Check if last menu then update the parent component's state
+    if (_name === LAST_MENU) {
+      setState(state => ({
+        ...state,
+        fetched: true,
+        data: result,
+      }));
+    }
+  };
+
+  // Recursive function to search in UL
+  const parseUL = (menu, path) => {
+    const menus = Array.from(menu.children);
+    menus.forEach((_menu) => {
+      let _name, _path;
+      if (_menu.tagName == 'UL') {
+        _name = getMenuName(_menu);
+        _path = `${path}/${_name}`;
+        iterItem(_menu, _path);
+      } else if (_menu.tagName == 'LI') {
+        if (_menu.classList.contains('dropdown-submenu')) {
+          _name = getMenuName(_menu);
+          _path = `${path}/${_name}`;
+          iterItem(_menu, _path);
+        } else {
+          parseLI(_menu, path);
+        }
+      }
+    });
+  };
+
+  // Expects LI of menus which contains A & UL
+  const iterItem = (menu, path) => {
+    const subMenus = Array.from(menu.children);
+    subMenus.forEach((_menu) => {
+      if (_menu.tagName == 'UL') {
+        parseUL(_menu, path);
+      }
+    });
+  };
+
+  // Starting Point
+  const navbar = document.querySelector('.navbar-nav');
+  const mainMenus = Array.from(navbar.children);
+
+  mainMenus.forEach((menu) => {
+    iterItem(menu, getMenuName(menu));
+  });
+}
diff --git a/web/pgadmin/browser/static/js/quick_search/online_help.js b/web/pgadmin/browser/static/js/quick_search/online_help.js
new file mode 100644
index 000000000..9808ad007
--- /dev/null
+++ b/web/pgadmin/browser/static/js/quick_search/online_help.js
@@ -0,0 +1,103 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {Iframe} from './iframe_component';
+import url_for from 'sources/url_for';
+
+const extractSearchResult = (list) => {
+  let result = {};
+  for (let idx = 0; idx < list.length; idx++) {
+    let link = list[idx].getElementsByTagName('A');
+    // we are not going to display more than first 10 result as per design
+    if (link.length == 0) {
+      break;
+    }
+    let topicName  = link[0].text;
+    let topicLink  = url_for('help.static', {
+      'filename': link[0].getAttribute('href'),
+    });
+    result[topicName] = topicLink;
+  }
+  return result;
+};
+
+export function onlineHelpSearch(param, props) {
+  param = param.split(' ').join('+');
+  const setState = props.setState;
+  const helpURL = url_for('help.static', {
+    'filename': 'search.html',
+  });
+  const srcURL = `${helpURL}?q=${param}`;
+  let isIFrameLoaded = false;
+  if(document.getElementById('hidden-quick-search-iframe')){
+    document.getElementById('hidden-quick-search-iframe').contentDocument.location.reload(true);
+  }
+
+  // Below function will be called when the page will be loaded in Iframe
+  const _iframeLoaded = () => {
+    if (isIFrameLoaded) {
+      return false;
+    }
+    isIFrameLoaded = true;
+    let iframe = document.getElementById('hidden-quick-search-iframe');
+    let content = (iframe.contentWindow || iframe.contentDocument);
+    let iframeHTML = content.document;
+    window.pooling = setInterval(() => {
+      let resultEl = iframeHTML.getElementById('search-results');
+      let searchResultsH2Tags = resultEl.getElementsByTagName('h2');
+      let list = resultEl && resultEl.getElementsByTagName('LI');
+      let pooling = window.pooling;
+      if ((list && list.length > 0 )) {
+        let res = extractSearchResult(list);
+        // After getting the data, we need to call the Parent component function
+        // which will render the data on the screen
+        if(searchResultsH2Tags[0]['childNodes'][0]['textContent'] != 'Searching'){
+          window.clearInterval(pooling);
+          setState(state => ({
+            ...state,
+            fetched: true,
+            clearedPooling: true,
+            url: srcURL,
+            data: res,
+          }));
+          isIFrameLoaded = false;
+          ReactDOM.unmountComponentAtNode(document.getElementById('quick-search-iframe-container'));
+        }else{
+          setState(state => ({
+            ...state,
+            fetched: true,
+            clearedPooling: false,
+            url: srcURL,
+            data: res,
+          }));
+        }
+
+
+      }else if(searchResultsH2Tags[0]['childNodes'][0]['textContent'] == 'Search Results'){
+        setState(state => ({
+          ...state,
+          fetched: true,
+          clearedPooling: true,
+          url: srcURL,
+          data: {},
+        }));
+        ReactDOM.unmountComponentAtNode(document.getElementById('quick-search-iframe-container'));
+        isIFrameLoaded = false;
+        window.clearInterval(pooling);
+      }
+    }, 500);
+  };
+
+  // Render IFrame
+  ReactDOM.render(
+    <Iframe id='hidden-quick-search-iframe' srcURL={srcURL} onLoad={_iframeLoaded}/>,
+    document.getElementById('quick-search-iframe-container'),
+  );
+}
diff --git a/web/pgadmin/browser/static/js/quick_search/trigger_search.js b/web/pgadmin/browser/static/js/quick_search/trigger_search.js
new file mode 100644
index 000000000..46779ae33
--- /dev/null
+++ b/web/pgadmin/browser/static/js/quick_search/trigger_search.js
@@ -0,0 +1,251 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import React, {useState, useEffect} from 'react';
+import {useDelayDebounce} from 'sources/custom_hooks';
+import {onlineHelpSearch} from './online_help';
+import {menuSearch} from './menuitems_help';
+import $ from 'jquery';
+import gettext from 'sources/gettext';
+
+export function Search() {
+  const [searchTerm, setSearchTerm] = useState('');
+  const [isShowMinLengthMsg, setIsShowMinLengthMsg] = useState(false);
+  let helpLinkTitles = [];
+  let helpLinks = [];
+  const [isMenuLoading, setIsMenuLoading] = useState(false);
+  const [isHelpLoading, setIsHelpLoading] = useState(false);
+  const [menuSearchResult, setMenuSearchResult] = useState({
+    fetched: false,
+    data: [],
+  });
+  const [helpSearchResult, setHelpSearchResult] = useState({
+    fetched: false,
+    clearedPooling: true,
+    url: '',
+    data: [],
+  });
+
+  const [showResults, setShowResults] = useState(false);
+
+  const resetSearchState = () => {
+    setMenuSearchResult(state => ({
+      ...state,
+      fetched: false,
+      data: [],
+    }));
+    setHelpSearchResult(state => ({
+      ...state,
+      fetched: false,
+      clearedPooling: true,
+      url: '',
+      data: {},
+    }));
+  };
+
+  // Below will be called when any changes has been made to state
+  useEffect(() => {
+    helpLinkTitles = Object.keys(helpSearchResult.data);
+    for(let i = 0; i<helpLinkTitles.length;i++){
+      helpLinks.push(<a href={''} target='_blank' rel='noreferrer'>helpLinkTitles[i]</a>);
+    }
+
+    if(menuSearchResult.fetched == true){
+      setIsMenuLoading(false);
+    }
+
+    if(helpSearchResult.fetched == true){
+      setIsHelpLoading(false);
+    }
+  }, [menuSearchResult, helpSearchResult]);
+
+  const initSearch = (param) => {
+    setIsMenuLoading(true);
+    setIsHelpLoading(true);
+
+    onlineHelpSearch(param,  {
+      state: helpSearchResult,
+      setState: setHelpSearchResult,
+    });
+    menuSearch(param, {
+      state: menuSearchResult,
+      setState: setMenuSearchResult,
+    });
+  };
+
+
+  // Debounse logic to avoid multiple re-render with each keypress
+  useDelayDebounce(initSearch, searchTerm, 1000);
+
+  const toggleDropdownMenu = () => {
+    let pooling = window.pooling;
+    if(pooling){
+      window.clearInterval(pooling);
+    }
+    document.getElementsByClassName('live-search-field')[0].value = '';
+    setTimeout(function(){
+      document.getElementById('live-search-field').focus();
+    },100);
+    resetSearchState();
+    setShowResults(!showResults);
+    setIsMenuLoading(false);
+    setIsHelpLoading(false);
+    setIsShowMinLengthMsg(false);
+  };
+
+  const refactorMenuItems = (items) => {
+    if(items.length > 0){
+      let menuItemsHtmlElement = [];
+      for(let i=0; i < items.length; i++){
+        Object.keys(items[i]).map( (value) => {
+          if(value != 'element' && value != 'No object selected'){
+            menuItemsHtmlElement.push( <li role='menuitem' key={ 'li-menu-' + i }><a id={ 'li-menu-' + i } href={'#'} className='menu-groups-a dropdown-item' key={ 'menu-' + i } onClick={() => {items[i]['element'].click(); toggleDropdownMenu();}}>
+              {value}
+              <span key={ 'menu-span-' + i }>{refactorPathToMenu(items[i][value])}</span>
+            </a></li>);
+          }
+        });
+      }
+      return menuItemsHtmlElement;
+    }
+  };
+
+  const refactorPathToMenu = (path) => {
+    if(path){
+      let pathArray = path.split('/');
+      let spanElement = [];
+      for(let i = 0; i < pathArray.length; i++ ){
+        if(i == (pathArray.length -1)){
+          spanElement.push(pathArray[i]);
+        }else{
+          spanElement.push(<span key={ 'menu-span-sub' + i }> {pathArray[i]} <i className='fa fa-angle-right' aria-hidden='true'></i> </span>);
+        }
+      }
+      return spanElement;
+    }
+  };
+
+  const onInputValueChange = (value) => {
+    let pooling = window.pooling;
+    if(pooling){
+      window.clearInterval(pooling);
+    }
+    resetSearchState();
+    setSearchTerm('');
+    if(value.length >= 3){
+      setSearchTerm(value);
+      setIsMenuLoading(true);
+      setIsHelpLoading(true);
+      setIsShowMinLengthMsg(false);
+    }else{
+      setIsMenuLoading(false);
+      setIsHelpLoading(false);
+      setIsShowMinLengthMsg(true);
+    }
+  };
+
+  const handleKeyDown = ({ keyCode }) => {
+    if(keyCode == 40){
+      $('a:focus').closest('li').next().find('a').first().focus();
+      return false;
+    }else if(keyCode == 38){
+      $('a:focus').closest('li').prev().find('a').first().focus();
+      return false;
+    }else if(keyCode == 27){
+      toggleDropdownMenu();
+    }
+  };
+
+  const handleEscKeyPress = ({ keyCode }) => {
+    if(keyCode == 27){
+      toggleDropdownMenu();
+    }
+  };
+
+  const handleInputKeyDown = ({ keyCode }) => {
+    if(keyCode == 40){
+      $('#li-menu-0').focus();
+    }
+  };
+
+  return (
+    <div id='quick-search-container' onClick={setSearchTerm}></div>,
+    <div id='quick-search-container' role="menu">
+      <div className='custom-dropdown'>
+        <a href='#'><div tabIndex='0' className='fa fa-search search_icon' id='search_icon' onClick={() => toggleDropdownMenu()} ></div></a>
+
+        <div id='myDropdown' className={showResults ? 'custom-dropdown-content visible' : 'custom-dropdown-content hidden'} onKeyDown={handleEscKeyPress}>
+
+          <input tabIndex='0' autoFocus type='text' autoComplete='off' className='form-control live-search-field' onKeyDown={handleInputKeyDown}
+            aria-label='live-search-field' id='live-search-field' placeholder={gettext('Search')} onChange={(e) => {onInputValueChange(e.target.value);} } />
+
+          <ul onKeyDown={handleKeyDown} style={{marginBottom:0}}>
+            <div className='content-groups' >
+
+              { isShowMinLengthMsg
+                ? (<div className='pad-12 no-results'>
+                  <span className='fa fa-info-circle'></span>
+                   &nbsp;Please enter minimum 3 characters to search
+                </div>)
+                :''}
+              <div className='fixed-max-height' >
+
+                { (menuSearchResult.fetched == true && isMenuLoading == false ) ?
+                  <div>
+                    <div className='menu-groups'>
+                      <span className='fa fa-window-maximize'></span> &nbsp;{gettext('MENU ITEMS')} ({menuSearchResult.data.length})
+                    </div>
+
+
+                    {refactorMenuItems(menuSearchResult.data)}
+                  </div> : ( (isMenuLoading) ? (<div className='pad-12'><div className="search-icon">{gettext('Searching...')}</div></div>) : '')}
+
+                {(menuSearchResult.data.length == 0 && menuSearchResult.fetched == true && isMenuLoading == false) ? (<div className='pad-12 no-results'><span className='fa fa-info-circle'></span> {gettext('No search results')}</div>):''}
+
+                { (helpSearchResult.fetched == true && isHelpLoading == false) ?
+                  <div>
+                    <div className='help-groups'>
+                      <span className='fa fa-question-circle'></span> &nbsp;{gettext('HELP ARTICLES')} {Object.keys(helpSearchResult.data).length > 10 ?
+                        <span>(10 of {Object.keys(helpSearchResult.data).length} )
+                        </span>:
+                        '(' + Object.keys(helpSearchResult.data).length + ')'}&nbsp;
+                      { !helpSearchResult.clearedPooling ? <img src='/static/img/loading.gif' alt={gettext('Loading...')} className='help_loading_icon'/> :''}
+                      { Object.keys(helpSearchResult.data).length > 10 ? <a href={helpSearchResult.url} className='pull-right no-padding' target='_blank' rel='noreferrer'>{gettext('Show all')} &nbsp;<span className='fas fa-external-link-alt' ></span></a> : ''}
+                    </div>
+
+                    {Object.keys(helpSearchResult.data).map( (value, index) => {
+                      if(index <= 9) {  return <li role='menuitem' key={ 'li-help-' + index }><a tabIndex='0' href={helpSearchResult.data[value]} key={ 'help-' + index } className='dropdown-item' target='_blank' rel='noreferrer'>{value}</a></li>; }
+                    })}
+
+                    {(Object.keys(helpSearchResult.data).length == 0) ? (<div className='pad-12 no-results'><span className='fa fa-info-circle'></span> {gettext('No search results')}</div>):''}
+                  </div> : ( (isHelpLoading && isMenuLoading == false) ? (
+                    <div>
+                      <div className='help-groups'>
+                        <span className='fa fa-question-circle'></span>
+                          &nbsp;HELP ARTICLES
+                        {Object.keys(helpSearchResult.data).length > 10
+                          ? '(10 of ' + Object.keys(helpSearchResult.data).length + ')'
+                          : '(' + Object.keys(helpSearchResult.data).length + ')'
+                        }
+                        { Object.keys(helpSearchResult.data).length > 10
+                          ? <a href={helpSearchResult.url} className='pull-right no-padding' target='_blank' rel='noreferrer'>
+                          Show all &nbsp;<span className='fas fa-external-link-alt' ></span></a> : ''
+                        }
+                      </div>
+                      <div className='pad-12'><div className="search-icon">{gettext('Searching...')}</div></div>
+                    </div>) : '')}
+              </div>
+            </div>
+          </ul>
+        </div>
+      </div>
+      <div id='quick-search-iframe-container' />
+    </div>
+  );
+
+}
diff --git a/web/pgadmin/browser/static/scss/_quick_search.scss b/web/pgadmin/browser/static/scss/_quick_search.scss
new file mode 100644
index 000000000..4b505a090
--- /dev/null
+++ b/web/pgadmin/browser/static/scss/_quick_search.scss
@@ -0,0 +1,137 @@
+#myInput {
+  box-sizing: border-box;
+  background-position: 14px 12px;
+  background-repeat: no-repeat;
+  font-size: 16px;
+  padding: 14px 20px 12px 45px;
+  border: none;
+  border-bottom: 1px solid #ddd;
+}
+
+#myInput:focus {outline: 3px solid #ddd;}
+
+.custom-dropdown {
+  position: relative;
+  display: inline-block;
+}
+
+.custom-dropdown-content {
+  display: none;
+  position: absolute;
+  background-color: $color-bg;
+  min-width: 376px;
+  overflow: auto;
+  border: 1px solid #ddd;
+  z-index: 1;
+  right:0.10em;
+  border-radius: 4px;
+  box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.2);
+}
+
+.custom-dropdown-content a {
+  color: $dropdown-link-color;
+  padding: 6px 12px 6px 21px;
+  text-decoration: none;
+  display: block;
+  cursor:pointer;
+}
+
+.custom-dropdown-content a:hover {
+  color: $black;
+}
+
+#myDropdown a:hover {background-color: $dropdown-link-hover-bg; color:$quick-search-a-text-color ;}
+
+.search_icon{
+  color: $white;
+  cursor: pointer;
+  padding-right: 8px;
+  border-right: 2px solid #fff;
+}
+.hidden { display:none; }
+
+.visible { display:block; }
+
+.live-search-field{
+  margin: 6px;
+  width: 360px !important;
+  position: fixed;
+}
+
+.menu-groups, .help-groups{
+  background-color: $color-gray-light;
+  padding: 6px;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.menu-groups .fa, .fas{
+  font-weight:normal !important;
+}
+
+.help-groups .fa, .fas{
+  font-weight:600 !important;
+}
+
+.pad-12{
+  padding:12px;
+}
+
+.no-results{
+  font-size: 14px;
+  color: #697986;
+  text-align: center;
+}
+
+.no-padding{
+  padding:0 !important;
+}
+
+.menu-groups-a{
+  display:flex !important;
+  flex-direction:column;
+}
+
+.menu-groups-a span{
+  font-size: 0.9em;
+  font-weight: 100;
+  color: $quick-search-span-text;
+}
+
+.menu-groups-a:hover span{
+  color: $quick-search-span-text-hover ;
+}
+
+.menu-groups-a:hover span{
+  color: $quick-search-span-text;
+}
+
+.content-groups{
+  margin-top: 3.0em;
+}
+
+.fixed-max-height{
+  max-height: 90vh;
+  overflow-y: auto;
+}
+
+.search-icon{
+  background: $loader-icon-small center center no-repeat;
+  margin: auto !important;
+  height: 22px !important;
+  width: 130px !important;
+  background-position: left !important;
+  font-size: 14px;
+  color: $dropdown-link-color;
+  padding-left: 30px;
+}
+
+.help_loading_icon{
+  height: 16px;
+}
+#myDropdown ul {
+  list-style: none;
+}
+
+
+
diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html
index c571df246..328d7e63d 100644
--- a/web/pgadmin/browser/templates/browser/index.html
+++ b/web/pgadmin/browser/templates/browser/index.html
@@ -136,6 +136,9 @@ window.onload = function(e){
                 <ul class="dropdown-menu" role="menu"></ul>
             </li>
         </ul>
+
+        <div id="quick-search-component"></div>
+
         {% if config.SERVER_MODE %}
         <ul class="navbar-nav">
             <li class="nav-item active dropdown">
diff --git a/web/pgadmin/static/img/loading.gif b/web/pgadmin/static/img/loading.gif
new file mode 100644
index 0000000000000000000000000000000000000000..3288d1035d70bb86517e2c233f1a904e41f06b29
GIT binary patch
literal 3208
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
Bt<?Yk

literal 0
HcmV?d00001

diff --git a/web/pgadmin/static/js/custom_hooks.js b/web/pgadmin/static/js/custom_hooks.js
index 9163f0d6c..cc59c1c66 100644
--- a/web/pgadmin/static/js/custom_hooks.js
+++ b/web/pgadmin/static/js/custom_hooks.js
@@ -26,4 +26,15 @@ export function usePrevious(value) {
     ref.current = value;
   });
   return ref.current;
-}
\ No newline at end of file
+}
+
+export function useDelayDebounce(callback, args, delay) {
+  useEffect(() => {
+    const delayDebounceFn = setTimeout(() => {
+      if (args) {
+        callback(args);
+      }
+    }, delay);
+    return () => clearTimeout(delayDebounceFn);
+  }, [args]);
+}
diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss
index fadd71b7b..33f2d108b 100644
--- a/web/pgadmin/static/scss/_pgadmin.style.scss
+++ b/web/pgadmin/static/scss/_pgadmin.style.scss
@@ -1123,3 +1123,9 @@ select:-webkit-autofill:focus {
 .pull-left{
   float:left
 }
+
+.menu-groups-a:hover span, .menu-groups-a:focus span{
+  color: $white !important;
+}
+
+#myDropdown a:hover {background-color: $dropdown-link-hover-bg; color:$white !important;}
diff --git a/web/pgadmin/static/scss/resources/_default.variables.scss b/web/pgadmin/static/scss/resources/_default.variables.scss
index 9d913788d..9a56f1c99 100644
--- a/web/pgadmin/static/scss/resources/_default.variables.scss
+++ b/web/pgadmin/static/scss/resources/_default.variables.scss
@@ -4,6 +4,9 @@ $enable-flex: true;
 $white: #fff;
 $black: #000;
 
+$span-text-color: #6B6B6B !default;
+$span-text-color-hover: #6B6B6B !default;
+
 $color-bg: $white !default;
 $color-fg: #222222 !default;
 
@@ -349,3 +352,6 @@ $grid-hover-fg-color: $color-fg !default;
 
 $btn-copied-color-fg: $active-color !default;
 
+$quick-search-a-text-color: $black !default;
+$quick-search-span-text: $span-text-color !default;
+$quick-search-span-text-hover: $span-text-color-hover !default;
diff --git a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
index 129b9a214..12a2b87df 100644
--- a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
+++ b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
@@ -121,3 +121,7 @@ $color-success-hover-fg: $color-fg;
 $datagrid-selected-color: $color-primary-fg;
 
 $select2-placeholder: #999;
+
+$span-text-color: #9D9FA1 !default;
+$span-text-color-hover: $white !default;
+$quick-search-a-text-color: $white !default;
diff --git a/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss b/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss
index b895a1223..6459e5714 100644
--- a/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss
+++ b/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss
@@ -202,4 +202,6 @@ $grid-hover-fg-color: #010B15;
 
 $btn-copied-color-fg: #010B15;
 
-
+$span-text-color: #9D9FA1 !default;
+$span-text-color-hover: $black !default;
+$quick-search-a-text-color: $black !default;
diff --git a/web/regression/javascript/quick_search/quick_search_spec.js b/web/regression/javascript/quick_search/quick_search_spec.js
new file mode 100644
index 000000000..d7f59563f
--- /dev/null
+++ b/web/regression/javascript/quick_search/quick_search_spec.js
@@ -0,0 +1,44 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import { Search } from 'browser/quick_search/trigger_search';
+
+let container;
+
+describe('quick search test cases', function () {
+  beforeEach(() => {
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    act(() => {
+      ReactDOM.render(<Search />, container);
+    });
+  });
+
+  afterEach(() => {
+    document.body.removeChild(container);
+    container = null;
+  });
+
+  it('should have rendered quick-search-container', () => {
+    expect(container.firstChild.id).toEqual('quick-search-container');
+  });
+
+  it('should have 2 childs in quick-search-container', () => {
+    expect(container.firstChild.childNodes.length).toEqual(2);
+  });
+
+  it('element should be html element', () => {
+    let inputElement = document.getElementById('live-search-field');
+    expect(inputElement instanceof HTMLElement).toBeTruthy();
+  });
+
+});
diff --git a/web/webpack.config.js b/web/webpack.config.js
index 96ef708ab..e6655512a 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -421,6 +421,7 @@ module.exports = [{
       use: {
         loader: 'imports-loader?' +
         'pgadmin.dashboard' +
+        ',pgadmin.browser.quick_search' +
         ',pgadmin.tools.user_management' +
         ',pgadmin.browser.object_statistics' +
         ',pgadmin.browser.dependencies' +
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 6fc26021e..bddcda9b8 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -181,6 +181,7 @@ var webpackShimConfig = {
     'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'),
     'pgadmin.browser.menu': path.join(__dirname, './pgadmin/browser/static/js/menu'),
     'pgadmin.browser.activity': path.join(__dirname, './pgadmin/browser/static/js/activity'),
+    'pgadmin.browser.quick_search': path.join(__dirname, './pgadmin/browser/static/js/quick_search'),
     'pgadmin.browser.messages': '/browser/js/messages',
     'pgadmin.browser.node': path.join(__dirname, './pgadmin/browser/static/js/node'),
     'pgadmin.browser.node.ui': path.join(__dirname, './pgadmin/browser/static/js/node.ui'),


  [image/png] Screenshot 2021-01-14 at 4.07.02 PM.png (307.3K, 4-Screenshot%202021-01-14%20at%204.07.02%20PM.png)
  download | view image

  [image/png] Screenshot 2021-01-14 at 4.07.45 PM.png (375.7K, 5-Screenshot%202021-01-14%20at%204.07.45%20PM.png)
  download | view image

  [image/png] Screenshot 2021-01-14 at 4.08.03 PM.png (311.9K, 6-Screenshot%202021-01-14%20at%204.08.03%20PM.png)
  download | view image

  [image/png] Screenshot 2021-01-14 at 4.08.15 PM.png (366.8K, 7-Screenshot%202021-01-14%20at%204.08.15%20PM.png)
  download | view image

  [image/png] image001.png (68.4K, 8-image001.png)
  download | view image

view thread (17+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected]
  Subject: Re: Quick search for menu items & help articles
  In-Reply-To: <[email protected]>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox