From cb5a1d7fe4859d95a5ebeea7a4aa717fcf03f3a8 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 22 Dec 2015 10:46:16 +0100 Subject: [PATCH] SONAR-7149 Add an ability to search for sub-components --- .../sonar-web/src/main/js/api/components.js | 6 +++ .../src/main/js/apps/code/actions/index.js | 39 ++++++++++++++- .../src/main/js/apps/code/components/Code.js | 28 +++++++---- .../js/apps/code/components/Components.js | 22 +++++---- .../main/js/apps/code/components/Search.js | 48 +++++++++++++++++++ .../src/main/js/apps/code/reducers/index.js | 10 +++- .../sonar-web/src/main/less/pages/code.less | 11 ++++- .../sonar-web/tests/apps/code/store-test.js | 28 +++++++++++ 8 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/code/components/Search.js 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 { - {shouldShowBreadcrumbs && ( - - )} + + {shouldShowBreadcrumbs && ( + + )} + + {shouldShowSearchResults && ( +
+ +
+ )} + {shouldShowComponents && (
{window.t('metric.duplicated_lines_density.short_name')} - - - -   - - + {baseComponent && ( + + + +   + + + )} {components.length ? ( components.map(component => ( 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 ( +
+ + +
+ ); + } +} + + +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', () => { -- 2.39.5