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);
+}
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';
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';
};
}
+export function searchAction (components) {
+ return {
+ type: SEARCH,
+ components
+ };
+}
+
+export function updateQueryAction (query) {
+ return {
+ type: UPDATE_QUERY,
+ query
+ };
+}
+
export function startFetching () {
return { type: START_FETCHING };
}
]);
}
+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());
.then(() => dispatch(stopFetching()));
};
}
+
+export function search (query, baseComponent) {
+ return dispatch => {
+ dispatch(updateQueryAction(query));
+ if (query) {
+ requestTree(query, baseComponent, dispatch);
+ } else {
+ dispatch(searchAction(null));
+ }
+ };
+}
+
+
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';
}
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 });
<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
<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"> </td>
- </tr>
- </tbody>
+ {baseComponent && (
+ <tbody>
+ <Component
+ key={baseComponent.key}
+ component={baseComponent}
+ coverageMetric={coverageMetric}/>
+ <tr className="blank">
+ <td colSpan="7"> </td>
+ </tr>
+ </tbody>
+ )}
<tbody>
{components.length ? (
components.map(component => (
<Component
- key={component.uuid}
+ key={component.key}
component={component}
coverageMetric={coverageMetric}
onBrowse={onBrowse}/>
--- /dev/null
+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);
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) {
components: null,
breadcrumbs: null,
sourceViewer: null,
+ searchResults: null,
+ searchQuery: '',
coverageMetric: null,
baseBreadcrumbs: []
};
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:
.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;
.code-source-viewer .source-viewer-header-component {
visibility: hidden;
}
+
+.code-search-box {
+ padding-left: 10px;
+ overflow: hidden;
+}
import {
initComponentAction,
browseAction,
+ searchAction,
+ updateQueryAction,
startFetching,
stopFetching
} from '../../../src/main/js/apps/code/actions';
.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', () => {