]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7149 Add an ability to search for sub-components
authorStas Vilchik <vilchiks@gmail.com>
Tue, 22 Dec 2015 09:46:16 +0000 (10:46 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Tue, 22 Dec 2015 09:46:23 +0000 (10:46 +0100)
server/sonar-web/src/main/js/api/components.js
server/sonar-web/src/main/js/apps/code/actions/index.js
server/sonar-web/src/main/js/apps/code/components/Code.js
server/sonar-web/src/main/js/apps/code/components/Components.js
server/sonar-web/src/main/js/apps/code/components/Search.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/reducers/index.js
server/sonar-web/src/main/less/pages/code.less
server/sonar-web/tests/apps/code/store-test.js

index 24f8c446a6f8d43f54d3ac106da05da782ce1169..5d78f0b3477eaf2bdf4089b7454d27d1c211a8b6 100644 (file)
@@ -49,3 +49,9 @@ export function getComponent (componentKey, metrics = []) {
   const data = { resource: componentKey, metrics: metrics.join(',') };
   return getJSON(url, data).then(r => r[0]);
 }
+
+export function getTree(baseComponentKey, options = {}) {
+  const url = baseUrl + '/api/components/tree';
+  const data = Object.assign({}, options, { baseComponentKey });
+  return getJSON(url, data);
+}
index dcf7e47d55480d087ef84587cf736d521807b39f..a68df6429b79a110de9f6fe7c1308c89e7d96404 100644 (file)
@@ -1,7 +1,7 @@
 import _ from 'underscore';
 import { pushPath } from 'redux-simple-router';
 
-import { getChildren, getComponent } from '../../../api/components';
+import { getChildren, getComponent, getTree } from '../../../api/components';
 import { getComponentNavigation } from '../../../api/nav';
 
 
@@ -22,6 +22,8 @@ const METRICS_WITH_COVERAGE = [
 
 export const INIT = 'INIT';
 export const BROWSE = 'BROWSE';
+export const SEARCH = 'SEARCH';
+export const UPDATE_QUERY = 'UPDATE_QUERY';
 export const START_FETCHING = 'START_FETCHING';
 export const STOP_FETCHING = 'STOP_FETCHING';
 
@@ -43,6 +45,20 @@ export function browseAction (component, children = [], breadcrumbs = []) {
   };
 }
 
+export function searchAction (components) {
+  return {
+    type: SEARCH,
+    components
+  };
+}
+
+export function updateQueryAction (query) {
+  return {
+    type: UPDATE_QUERY,
+    query
+  };
+}
+
 export function startFetching () {
   return { type: START_FETCHING };
 }
@@ -83,6 +99,14 @@ function retrieveComponent (componentKey, bucket) {
   ]);
 }
 
+let requestTree = (query, baseComponent, dispatch) => {
+  dispatch(startFetching());
+  return getTree(baseComponent.key, { q: query, s: 'qualifier,name' })
+      .then(r => dispatch(searchAction(r.components)))
+      .then(() => dispatch(stopFetching()));
+};
+requestTree = _.debounce(requestTree, 250);
+
 export function initComponent (componentKey, breadcrumbs) {
   return dispatch => {
     dispatch(startFetching());
@@ -104,3 +128,16 @@ export function browse (componentKey) {
         .then(() => dispatch(stopFetching()));
   };
 }
+
+export function search (query, baseComponent) {
+  return dispatch => {
+    dispatch(updateQueryAction(query));
+    if (query) {
+      requestTree(query, baseComponent, dispatch);
+    } else {
+      dispatch(searchAction(null));
+    }
+  };
+}
+
+
index 7f364cf3939668c69c7128624017ba7a66cc219e..6b38da6d0077fd9f5f0be7aa6feb317fb95c151a 100644 (file)
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
 import Components from './Components';
 import Breadcrumbs from './Breadcrumbs';
 import SourceViewer from './SourceViewer';
+import Search from './Search';
 import { initComponent, browse } from '../actions';
 import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
 
@@ -33,10 +34,11 @@ class Code extends Component {
   }
 
   render () {
-    const { fetching, baseComponent, components, breadcrumbs, sourceViewer, coverageMetric } = this.props;
+    const { fetching, baseComponent, components, breadcrumbs, sourceViewer, coverageMetric, searchResults } = this.props;
     const shouldShowBreadcrumbs = Array.isArray(breadcrumbs) && breadcrumbs.length > 1;
-    const shouldShowComponents = !sourceViewer && components;
-    const shouldShowSourceViewer = sourceViewer;
+    const shouldShowSearchResults = !!searchResults;
+    const shouldShowSourceViewer = !!sourceViewer;
+    const shouldShowComponents = !shouldShowSearchResults && !shouldShowSourceViewer && components;
 
     const componentsClassName = classNames('spacer-top', { 'new-loading': fetching });
 
@@ -52,13 +54,23 @@ class Code extends Component {
                 <i className="spinner"/>
               </div>
 
-              {shouldShowBreadcrumbs && (
-                  <Breadcrumbs
-                      breadcrumbs={breadcrumbs}
-                      onBrowse={this.handleBrowse.bind(this)}/>
-              )}
+              <Search component={this.props.component}/>
             </header>
 
+            {shouldShowBreadcrumbs && (
+                <Breadcrumbs
+                    breadcrumbs={breadcrumbs}
+                    onBrowse={this.handleBrowse.bind(this)}/>
+            )}
+
+            {shouldShowSearchResults && (
+                <div className={componentsClassName}>
+                  <Components
+                      components={searchResults}
+                      onBrowse={this.handleBrowse.bind(this)}/>
+                </div>
+            )}
+
             {shouldShowComponents && (
                 <div className={componentsClassName}>
                   <Components
index d2dffb6786966ade8d1fcb5eb1b86db5d874699b..a1efd179300b53abee84450f85af4515639d1dbe 100644 (file)
@@ -17,20 +17,22 @@ const Components = ({ baseComponent, components, coverageMetric, onBrowse }) =>
           <th className="thin nowrap text-right">{window.t('metric.duplicated_lines_density.short_name')}</th>
         </tr>
       </thead>
-      <tbody>
-        <Component
-            key={baseComponent.uuid}
-            component={baseComponent}
-            coverageMetric={coverageMetric}/>
-        <tr className="blank">
-          <td colSpan="7">&nbsp;</td>
-        </tr>
-      </tbody>
+      {baseComponent && (
+          <tbody>
+            <Component
+                key={baseComponent.key}
+                component={baseComponent}
+                coverageMetric={coverageMetric}/>
+            <tr className="blank">
+              <td colSpan="7">&nbsp;</td>
+            </tr>
+          </tbody>
+      )}
       <tbody>
         {components.length ? (
             components.map(component => (
                 <Component
-                    key={component.uuid}
+                    key={component.key}
                     component={component}
                     coverageMetric={coverageMetric}
                     onBrowse={onBrowse}/>
diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.js b/server/sonar-web/src/main/js/apps/code/components/Search.js
new file mode 100644 (file)
index 0000000..2956952
--- /dev/null
@@ -0,0 +1,48 @@
+import _ from 'underscore';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+
+import { search } from '../actions';
+
+
+class Search extends Component {
+  componentDidMount () {
+    this.refs.input.focus();
+  }
+
+  handleSearch (e) {
+    e.preventDefault();
+    const { dispatch, component } = this.props;
+    const query = this.refs.input.value;
+    dispatch(search(query, component));
+  }
+
+  render () {
+    const { query } = this.props;
+
+    return (
+        <form
+            onSubmit={this.handleSearch.bind(this)}
+            className="search-box code-search-box">
+          <button className="search-box-submit button-clean">
+            <i className="icon-search"></i>
+          </button>
+          <input
+              ref="input"
+              onChange={this.handleSearch.bind(this)}
+              value={query}
+              className="search-box-input"
+              type="search"
+              name="q"
+              placeholder="Search"
+              maxLength="100"
+              autoComplete="off"/>
+        </form>
+    );
+  }
+}
+
+
+export default connect(state => {
+  return { query: state.current.searchQuery };
+})(Search);
index 08b74a6710e3cac2996ef3564ccbfe5d8ba5d607..c177efd55fc8c39ba3039e53dd691e0a670da087 100644 (file)
@@ -1,6 +1,6 @@
 import _ from 'underscore';
 
-import { INIT, BROWSE, START_FETCHING, STOP_FETCHING } from '../actions';
+import { INIT, BROWSE, SEARCH, UPDATE_QUERY, START_FETCHING, STOP_FETCHING } from '../actions';
 
 
 function hasSourceCode (component) {
@@ -34,6 +34,8 @@ export const initialState = {
   components: null,
   breadcrumbs: null,
   sourceViewer: null,
+  searchResults: null,
+  searchQuery: '',
   coverageMetric: null,
   baseBreadcrumbs: []
 };
@@ -53,7 +55,11 @@ export function current (state = initialState, action) {
       const breadcrumbs = action.breadcrumbs.slice(baseBreadcrumbsLength);
       const sourceViewer = hasSourceCode(action.component) ? action.component : null;
 
-      return { ...state, baseComponent, components, breadcrumbs, sourceViewer };
+      return { ...state, baseComponent, components, breadcrumbs, sourceViewer, searchResults: null, searchQuery: '' };
+    case SEARCH:
+      return { ...state, searchResults: action.components };
+    case UPDATE_QUERY:
+      return { ...state, searchQuery: action.query };
     case START_FETCHING:
       return { ...state, fetching: true };
     case STOP_FETCHING:
index ec50b00aeffec2db0191c685b57774601414286a..578e392ed2b5a41032d2fde214f22bde8e40c58b 100644 (file)
@@ -6,14 +6,16 @@
 .code-breadcrumbs {
   display: flex;
   flex-wrap: wrap;
-  padding-left: 10px;
-  overflow: hidden;
 }
 
 .code-breadcrumbs > li {
   padding: 5px 5px 3px;
 }
 
+.code-breadcrumbs > li:first-child {
+  padding-left: 0;
+}
+
 .code-breadcrumbs > li::after {
   position: relative;
   top: -1px;
@@ -49,3 +51,8 @@
 .code-source-viewer .source-viewer-header-component {
   visibility: hidden;
 }
+
+.code-search-box {
+  padding-left: 10px;
+  overflow: hidden;
+}
index 9d3445cf0af2da41fa7ddd4e3608b8407cf1dccd..9f883e7f8c6ef3bc8f78ce0c7e6007deba81e2a6 100644 (file)
@@ -4,6 +4,8 @@ import { current, bucket, initialState } from '../../../src/main/js/apps/code/re
 import {
     initComponentAction,
     browseAction,
+    searchAction,
+    updateQueryAction,
     startFetching,
     stopFetching
 } from '../../../src/main/js/apps/code/actions';
@@ -179,6 +181,32 @@ describe('Code :: Store', () => {
               .to.have.length(1);
         });
       });
+      describe('searchResults', () => {
+        it('should be set', () => {
+          const results = [{ key: 'A' }, { key: 'B' }];
+          expect(current(initialState, searchAction(results)).searchResults)
+              .to.deep.equal(results)
+        });
+
+        it('should be reset', () => {
+          const results = [{ key: 'A' }, { key: 'B' }];
+          const stateBefore = Object.assign({}, initialState, { searchResults: results });
+          expect(current(stateBefore, browseAction(exampleComponent)).searchResults)
+              .to.be.null;
+        });
+      });
+      describe('searchQuery', () => {
+        it('should be set', () => {
+          expect(current(initialState, updateQueryAction('query')).searchQuery)
+              .to.equal('query');
+        });
+
+        it('should be reset', () => {
+          const stateBefore = Object.assign({}, initialState, { searchQuery: 'query' });
+          expect(current(stateBefore, browseAction(exampleComponent)).searchQuery)
+              .to.equal('');
+        });
+      });
     });
     describe('bucket', () => {
       it('should add initial component', () => {