aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-12-22 10:46:16 +0100
committerStas Vilchik <vilchiks@gmail.com>2015-12-22 10:46:23 +0100
commitcb5a1d7fe4859d95a5ebeea7a4aa717fcf03f3a8 (patch)
tree9d6cf03e38aca013558ddd42b1da804377d6408a
parentbfa9e24fadd011c331e3af791ac189075a73b8e3 (diff)
downloadsonarqube-cb5a1d7fe4859d95a5ebeea7a4aa717fcf03f3a8.tar.gz
sonarqube-cb5a1d7fe4859d95a5ebeea7a4aa717fcf03f3a8.zip
SONAR-7149 Add an ability to search for sub-components
-rw-r--r--server/sonar-web/src/main/js/api/components.js6
-rw-r--r--server/sonar-web/src/main/js/apps/code/actions/index.js39
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Code.js28
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Components.js22
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Search.js48
-rw-r--r--server/sonar-web/src/main/js/apps/code/reducers/index.js10
-rw-r--r--server/sonar-web/src/main/less/pages/code.less11
-rw-r--r--server/sonar-web/tests/apps/code/store-test.js28
8 files changed, 169 insertions, 23 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js
index 24f8c446a6f..5d78f0b3477 100644
--- a/server/sonar-web/src/main/js/api/components.js
+++ b/server/sonar-web/src/main/js/api/components.js
@@ -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);
+}
diff --git a/server/sonar-web/src/main/js/apps/code/actions/index.js b/server/sonar-web/src/main/js/apps/code/actions/index.js
index dcf7e47d554..a68df6429b7 100644
--- a/server/sonar-web/src/main/js/apps/code/actions/index.js
+++ b/server/sonar-web/src/main/js/apps/code/actions/index.js
@@ -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));
+ }
+ };
+}
+
+
diff --git a/server/sonar-web/src/main/js/apps/code/components/Code.js b/server/sonar-web/src/main/js/apps/code/components/Code.js
index 7f364cf3939..6b38da6d007 100644
--- a/server/sonar-web/src/main/js/apps/code/components/Code.js
+++ b/server/sonar-web/src/main/js/apps/code/components/Code.js
@@ -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
diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.js b/server/sonar-web/src/main/js/apps/code/components/Components.js
index d2dffb67869..a1efd179300 100644
--- a/server/sonar-web/src/main/js/apps/code/components/Components.js
+++ b/server/sonar-web/src/main/js/apps/code/components/Components.js
@@ -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
index 00000000000..2956952c43e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/Search.js
@@ -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);
diff --git a/server/sonar-web/src/main/js/apps/code/reducers/index.js b/server/sonar-web/src/main/js/apps/code/reducers/index.js
index 08b74a6710e..c177efd55fc 100644
--- a/server/sonar-web/src/main/js/apps/code/reducers/index.js
+++ b/server/sonar-web/src/main/js/apps/code/reducers/index.js
@@ -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:
diff --git a/server/sonar-web/src/main/less/pages/code.less b/server/sonar-web/src/main/less/pages/code.less
index ec50b00aeff..578e392ed2b 100644
--- a/server/sonar-web/src/main/less/pages/code.less
+++ b/server/sonar-web/src/main/less/pages/code.less
@@ -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;
+}
diff --git a/server/sonar-web/tests/apps/code/store-test.js b/server/sonar-web/tests/apps/code/store-test.js
index 9d3445cf0af..9f883e7f8c6 100644
--- a/server/sonar-web/tests/apps/code/store-test.js
+++ b/server/sonar-web/tests/apps/code/store-test.js
@@ -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', () => {