"gulp-util": "3.0.6",
"handlebars": "^2.0.0",
"handlebars-loader": "^1.1.4",
+ "history": "^1.13.1",
"imports-loader": "^0.6.5",
"isparta": "^4.0.0",
"jquery": "2.1.4",
"react-addons-test-utils": "0.14.2",
"react-dom": "0.14.2",
"react-redux": "^4.0.1",
+ "react-router": "^1.0.2",
"react-select": "1.0.0-beta6",
"redux": "^3.0.5",
"redux-logger": "^2.2.1",
+ "redux-simple-router": "^1.0.1",
"redux-thunk": "^1.0.2",
"script-loader": "^0.6.1",
"sinon": "1.15.4",
import _ from 'underscore';
+import { pushPath } from 'redux-simple-router';
import { getChildren, getComponent } from '../../../api/components';
+import { getComponentNavigation } from '../../../api/nav';
const METRICS = [
'violations',
'duplicated_lines_density'
];
+
const METRICS_WITH_COVERAGE = [
...METRICS,
'coverage',
export const INIT = 'INIT';
export const BROWSE = 'BROWSE';
-export const RECEIVE_COMPONENTS = 'RECEIVE_COMPONENTS';
-export const SHOW_SOURCE = 'SHOW_SOURCE';
+export const START_FETCHING = 'START_FETCHING';
+export const STOP_FETCHING = 'STOP_FETCHING';
-export function requestComponents (baseComponent) {
+export function initComponentAction (component, breadcrumbs = []) {
return {
- type: BROWSE,
- baseComponent
+ type: INIT,
+ component,
+ breadcrumbs
};
}
-
-export function receiveComponents (baseComponent, components) {
+export function browseAction (component, children = [], breadcrumbs = []) {
return {
- type: RECEIVE_COMPONENTS,
- baseComponent,
- components
+ type: BROWSE,
+ component,
+ children,
+ breadcrumbs
};
}
+export function startFetching () {
+ return { type: START_FETCHING };
+}
-export function showSource (component) {
- return {
- type: SHOW_SOURCE,
- component
- };
+export function stopFetching () {
+ return { type: STOP_FETCHING };
}
-function fetchChildren (dispatch, getState, baseComponent) {
- dispatch(requestComponents(baseComponent));
+function getPath (componentKey) {
+ return '/' + encodeURIComponent(componentKey);
+}
- const { coverageMetric } = getState();
- const metrics = [...METRICS, coverageMetric];
+function retrieveComponentBase (componentKey, candidate) {
+ return candidate ?
+ Promise.resolve(candidate) :
+ getComponent(componentKey, METRICS_WITH_COVERAGE);
+}
- return getChildren(baseComponent.key, metrics)
- .then(components => _.sortBy(components, 'name'))
- .then(components => dispatch(receiveComponents(baseComponent, components)));
+function retrieveComponentChildren (componentKey, candidate) {
+ return candidate && candidate.children ?
+ Promise.resolve(candidate.children) :
+ getChildren(componentKey, METRICS_WITH_COVERAGE);
}
+function retrieveComponentBreadcrumbs (componentKey, candidate) {
+ return candidate && candidate.breadcrumbs ?
+ Promise.resolve(candidate.breadcrumbs) :
+ getComponentNavigation(componentKey).then(navigation => navigation.breadcrumbs);
+}
-export function initComponent (baseComponent) {
- return (dispatch, getState) => {
- return getComponent(baseComponent.key, METRICS_WITH_COVERAGE)
- .then(component => fetchChildren(dispatch, getState, component));
- };
+function retrieveComponent (componentKey, bucket) {
+ const candidate = _.findWhere(bucket, { key: componentKey });
+ return Promise.all([
+ retrieveComponentBase(componentKey, candidate),
+ retrieveComponentChildren(componentKey, candidate),
+ retrieveComponentBreadcrumbs(componentKey, candidate)
+ ]);
}
+export function initComponent (componentKey, breadcrumbs) {
+ return dispatch => {
+ dispatch(startFetching());
+ return getComponent(componentKey, METRICS_WITH_COVERAGE)
+ .then(component => dispatch(initComponentAction(component, breadcrumbs)))
+ .then(() => dispatch(stopFetching()));
+ };
+}
-export function fetchComponents (baseComponent) {
+export function browse (componentKey) {
return (dispatch, getState) => {
- const { fetching } = getState();
- if (!fetching) {
- return fetchChildren(dispatch, getState, baseComponent);
- }
+ const { bucket } = getState();
+ dispatch(startFetching());
+ return retrieveComponent(componentKey, bucket)
+ .then(([component, children, breadcrumbs]) => {
+ dispatch(browseAction(component, children, breadcrumbs));
+ })
+ .then(() => dispatch(pushPath(getPath(componentKey))))
+ .then(() => dispatch(stopFetching()));
};
}
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
+import { Router, Route } from 'react-router';
+import { createHashHistory } from 'history';
+import { syncReduxAndRouter } from 'redux-simple-router';
import Code from './components/Code';
import configureStore from './store/configureStore';
const store = configureStore();
+const history = createHashHistory({
+ queryKey: false
+});
+
+syncReduxAndRouter(history, store);
+
+window.sonarqube.appStarted.then(({ el, component }) => {
+ const CodeWithComponent = () => {
+ return <Code component={component}/>;
+ };
-window.sonarqube.appStarted.then(({ el, ...other }) => {
render(
<Provider store={store}>
- <Code {...other}/>
+ <Router history={history}>
+ <Route path="/" component={CodeWithComponent}/>
+ <Route path="/:path" component={CodeWithComponent}/>
+ </Router>
</Provider>,
document.querySelector(el));
});
import Components from './Components';
import Breadcrumbs from './Breadcrumbs';
import SourceViewer from './SourceViewer';
-import { initComponent, fetchComponents, showSource } from '../actions';
+import { initComponent, browse } from '../actions';
import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
class Code extends Component {
componentDidMount () {
- const { dispatch, component } = this.props;
- dispatch(initComponent(component));
+ const { dispatch, component, routing } = this.props;
+ const selectedKey = (routing.path && decodeURIComponent(routing.path.substr(1))) || component.key;
+ dispatch(initComponent(component.key, component.breadcrumbs))
+ .then(() => dispatch(browse(selectedKey)));
}
componentWillReceiveProps (nextProps) {
- if (nextProps.component !== this.props.component) {
- const { dispatch, component } = this.props;
- dispatch(initComponent(component));
+ if (nextProps.routing !== this.props.routing) {
+ const { dispatch, routing, component, fetching } = nextProps;
+ if (!fetching) {
+ const selectedKey = (routing.path && decodeURIComponent(routing.path.substr(1))) || component.key;
+ dispatch(browse(selectedKey));
+ }
}
}
- hasSourceCode (component) {
- return component.qualifier === 'FIL' || component.qualifier === 'UTS';
- }
-
handleBrowse (component) {
const { dispatch } = this.props;
- if (this.hasSourceCode(component)) {
- dispatch(showSource(component));
- } else {
- dispatch(fetchComponents(component));
- }
+ dispatch(browse(component.key));
}
render () {
}
}
-
-export default connect(state => state)(Code);
+export default connect(state => {
+ return Object.assign({ routing: state.routing }, state.current);
+})(Code);
this.renderSourceViewer();
}
+ shouldComponentUpdate (nextProps) {
+ return nextProps.component.uuid !== this.props.component.uuid;
+ }
+
+ componentWillUpdate () {
+ this.destroySourceViewer();
+ }
+
componentDidUpdate () {
this.renderSourceViewer();
}
import _ from 'underscore';
-import { combineReducers } from 'redux';
-import { BROWSE, RECEIVE_COMPONENTS, SHOW_SOURCE } from '../actions';
+import { INIT, BROWSE, START_FETCHING, STOP_FETCHING } from '../actions';
-export function fetching (state = false, action) {
- switch (action.type) {
- case BROWSE:
- return true;
- case RECEIVE_COMPONENTS:
- return false;
- default:
- return state;
- }
-}
-
-
-export function baseComponent (state = null, action) {
- switch (action.type) {
- case RECEIVE_COMPONENTS:
- return action.baseComponent;
- default:
- return state;
- }
-}
-
-
-export function components (state = null, action) {
- switch (action.type) {
- case RECEIVE_COMPONENTS:
- return action.components;
- default:
- return state;
- }
+function hasSourceCode (component) {
+ return component.qualifier === 'FIL' || component.qualifier === 'UTS';
}
-
-export function breadcrumbs (state = [], action) {
- switch (action.type) {
- case BROWSE:
- const existedIndex = state.findIndex(b => b.key === action.baseComponent.key);
- let nextBreadcrumbs;
-
- if (existedIndex === -1) {
- // browse deeper
- nextBreadcrumbs = [...state, action.baseComponent];
- } else {
- // use breadcrumbs
- nextBreadcrumbs = [...state.slice(0, existedIndex + 1)];
- }
-
- return nextBreadcrumbs;
- case SHOW_SOURCE:
- return [...state, action.component];
- default:
- return state;
- }
-}
-
-
-export function sourceViewer (state = null, action) {
- switch (action.type) {
- case BROWSE:
- return null;
- case SHOW_SOURCE:
- return action.component;
- default:
- return state;
- }
-}
-
-
function selectCoverageMetric (component) {
const coverage = _.findWhere(component.msr, { key: 'coverage' });
const itCoverage = _.findWhere(component.msr, { key: 'it_coverage' });
}
}
+function merge (components, candidate) {
+ const found = _.findWhere(components, { key: candidate.key });
+ const newEntry = Object.assign({}, found, candidate);
+ return [...(_.without(components, found)), newEntry];
+}
-export function coverageMetric (state = null, action) {
+
+export const initialState = {
+ fetching: false,
+ baseComponent: null,
+ components: null,
+ breadcrumbs: null,
+ sourceViewer: null,
+ coverageMetric: null,
+ baseBreadcrumbs: []
+};
+
+
+export function current (state = initialState, action) {
switch (action.type) {
+ case INIT:
+ const coverageMetric = selectCoverageMetric(action.component);
+ const baseBreadcrumbs = action.breadcrumbs.length > 1 ? _.initial(action.breadcrumbs) : [];
+
+ return { ...state, coverageMetric, baseBreadcrumbs };
case BROWSE:
- return state !== null ? state : selectCoverageMetric(action.baseComponent);
+ const baseComponent = hasSourceCode(action.component) ? null : action.component;
+ const components = hasSourceCode(action.component) ? null : _.sortBy(action.children, 'name');
+ const baseBreadcrumbsLength = state.baseBreadcrumbs.length;
+ const breadcrumbs = action.breadcrumbs.slice(baseBreadcrumbsLength);
+ const sourceViewer = hasSourceCode(action.component) ? action.component : null;
+
+ return { ...state, baseComponent, components, breadcrumbs, sourceViewer };
+ case START_FETCHING:
+ return { ...state, fetching: true };
+ case STOP_FETCHING:
+ return { ...state, fetching: false };
default:
return state;
}
}
-const rootReducer = combineReducers({
- fetching,
- baseComponent,
- components,
- breadcrumbs,
- sourceViewer,
- coverageMetric
-});
-
-
-export default rootReducer;
+export function bucket (state = [], action) {
+ switch (action.type) {
+ case INIT:
+ return merge(state, action.component);
+ case BROWSE:
+ const candidate = Object.assign({}, action.component, {
+ children: action.children,
+ breadcrumbs: action.breadcrumbs
+ });
+ const nextState = merge(state, candidate);
+ return action.children.reduce((currentState, nextComponent) => {
+ const nextComponentWidthBreadcrumbs = Object.assign({}, nextComponent, {
+ breadcrumbs: [...action.breadcrumbs, nextComponent]
+ });
+ return merge(currentState, nextComponentWidthBreadcrumbs);
+ }, nextState);
+ default:
+ return state;
+ }
+}
-import { createStore, applyMiddleware } from 'redux';
+import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
-
-import rootReducer from '../reducers';
+import { routeReducer } from 'redux-simple-router';
+import { current, bucket } from '../reducers';
const logger = createLogger({
- predicate: () => process.env.NODE_ENV === 'development'
+ predicate: () => process.env.NODE_ENV !== 'production'
});
const createStoreWithMiddleware = applyMiddleware(
logger
)(createStore);
+const reducer = combineReducers({
+ routing: routeReducer,
+ current,
+ bucket
+});
export default function configureStore () {
- return createStoreWithMiddleware(rootReducer);
+ return createStoreWithMiddleware(reducer);
}
id: navResponse.component.uuid,
key: navResponse.component.key,
name: navResponse.component.name,
- qualifier: _.last(navResponse.component.breadcrumbs).qualifier
+ qualifier: _.last(navResponse.component.breadcrumbs).qualifier,
+ breadcrumbs: navResponse.component.breadcrumbs
};
}
}
{ key: 'ncloc', val: 9757 }
];
const exampleComponent = {
+ uuid: 'A1',
key: 'A',
name: 'AA',
qualifier: 'TRK',
msr: measures
};
-const exampleComponent2 = { key: 'B' };
-const exampleComponent3 = { key: 'C' };
+const exampleComponent2 = { uuid: 'B2', key: 'B' };
+const exampleComponent3 = { uuid: 'C3', key: 'C' };
const exampleOnBrowse = sinon.spy();
import { expect } from 'chai';
+import { current, bucket, initialState } from '../../../src/main/js/apps/code/reducers';
import {
- fetching,
- baseComponent,
- components,
- breadcrumbs,
- sourceViewer,
- coverageMetric
-} from '../../../src/main/js/apps/code/reducers';
-import {
- requestComponents,
- receiveComponents,
- showSource
+ initComponentAction,
+ browseAction,
+ startFetching,
+ stopFetching
} from '../../../src/main/js/apps/code/actions';
const exampleComponent = { key: 'A' };
+const exampleComponents = [
+ { key: 'B' },
+ { key: 'C' }
+];
describe('Code :: Store', () => {
//describe('action creators');
describe('reducers', () => {
- describe('fetching', () => {
- it('should be initially false', () => {
- expect(fetching(undefined, {}))
- .to.equal(false);
- });
-
- it('should be true after requesting components', () => {
- expect(fetching(false, requestComponents()))
- .to.equal(true);
- });
-
- it('should be false after receiving components', () => {
- expect(fetching(true, receiveComponents({}, [])))
- .to.equal(false);
- });
- });
-
- describe('baseComponent', () => {
- it('should not be set after requesting components', () => {
- const component = {};
- expect(baseComponent(null, requestComponents(component)))
- .to.equal(null);
- });
-
- it('should be set after receiving components', () => {
- const component = {};
- expect(baseComponent(null, receiveComponents(component, [])))
- .to.equal(component);
- });
- });
+ describe('current', () => {
+ describe('fetching', () => {
+ it('should be set to true', () => {
+ expect(current({ ...initialState, fetching: false }, startFetching()).fetching)
+ .to.equal(true);
+ });
- describe('components', () => {
- it('should be set after receiving components', () => {
- const list = [exampleComponent];
- expect(components(null, receiveComponents({}, list)))
- .to.equal(list);
+ it('should be false', () => {
+ expect(current({ ...initialState, fetching: true }, stopFetching()).fetching)
+ .to.equal(false);
+ });
});
- });
+ describe('baseComponent', () => {
+ it('should be set', () => {
+ const component = {};
+ expect(current(initialState, browseAction(component)).baseComponent)
+ .to.equal(component);
+ });
- describe('breadcrumbs', () => {
- it('should push new component on BROWSE', () => {
- const stateBefore = [];
- const stateAfter = [exampleComponent];
- expect(breadcrumbs(stateBefore, requestComponents(exampleComponent)))
- .to.deep.equal(stateAfter);
+ it('should not be set for components with source code', () => {
+ const file = { qualifier: 'FIL' };
+ expect(current(initialState, browseAction(file, exampleComponents)).baseComponent)
+ .to.be.null;
+ const test = { qualifier: 'UTS' };
+ expect(current(initialState, browseAction(test, exampleComponents)).baseComponent)
+ .to.be.null;
+ });
});
+ describe('components', () => {
+ it('should be set', () => {
+ const component = {};
+ expect(current(initialState, browseAction(component, exampleComponents)).components)
+ .to.deep.equal(exampleComponents);
+ });
- it('should push new component on SHOW_SOURCE', () => {
- const stateBefore = [];
- const stateAfter = [exampleComponent];
- expect(breadcrumbs(stateBefore, showSource(exampleComponent)))
- .to.deep.equal(stateAfter);
- });
+ it('should sort components by name', () => {
+ const component = {};
+ const componentsBefore = [
+ { key: 'A', name: 'B' },
+ { key: 'B', name: 'A' }
+ ];
+ const componentsAfter = [
+ { key: 'B', name: 'A' },
+ { key: 'A', name: 'B' }
+ ];
+ expect(current(initialState, browseAction(component, componentsBefore)).components)
+ .to.deep.equal(componentsAfter);
+ });
- it('should cut the tail', () => {
- const stateBefore = [{ key: 'B' }, exampleComponent, { key: 'C' }];
- const stateAfter = [{ key: 'B' }, exampleComponent];
- expect(breadcrumbs(stateBefore, requestComponents(exampleComponent)))
- .to.deep.equal(stateAfter);
+ it('should not be set for components with source code', () => {
+ const file = { qualifier: 'FIL' };
+ expect(current(initialState, browseAction(file, exampleComponents)).components)
+ .to.be.null;
+ const test = { qualifier: 'UTS' };
+ expect(current(initialState, browseAction(test, exampleComponents)).components)
+ .to.be.null;
+ });
});
- });
+ describe('breadcrumbs', () => {
+ it('should be set', () => {
+ expect(current(initialState, browseAction(exampleComponent, [], exampleComponents)).breadcrumbs)
+ .to.deep.equal(exampleComponents);
+ });
- describe('sourceViewer', () => {
- it('should be set on SHOW_SOURCE', () => {
- expect(sourceViewer(null, showSource(exampleComponent)))
- .to.equal(exampleComponent);
+ it('should respect baseBreadcrumbs', () => {
+ const baseBreadcrumbs = [{ key: 'BASE1' }];
+ const breadcrumbsBefore = [{ key: 'BASE1' }, { key: 'BASE2' }, { key: 'C' }];
+ const breadcrumbsAfter = [{ key: 'BASE2' }, { key: 'C' }];
+ expect(current(
+ { ...initialState, baseBreadcrumbs },
+ browseAction(exampleComponent, [], breadcrumbsBefore)).breadcrumbs
+ ).to.deep.equal(breadcrumbsAfter);
+ });
});
+ describe('sourceViewer', () => {
+ it('should be set for components with source code', () => {
+ const file = { qualifier: 'FIL' };
+ expect(current(initialState, browseAction(file, exampleComponents)).sourceViewer)
+ .to.equal(file);
+ const test = { qualifier: 'UTS' };
+ expect(current(initialState, browseAction(test, exampleComponents)).sourceViewer)
+ .to.equal(test);
+ });
- it('should be unset on BROWSE', () => {
- expect(sourceViewer(exampleComponent, requestComponents({})))
- .to.equal(null);
+ it('should not be set for components without source code', () => {
+ const project = { qualifier: 'TRK' };
+ expect(current(initialState, browseAction(project, exampleComponents)).sourceViewer)
+ .to.be.null;
+ const unknown = {};
+ expect(current(initialState, browseAction(unknown, exampleComponents)).sourceViewer)
+ .to.be.null;
+ });
});
-
describe('coverageMetric', () => {
- it('should be initially null', () => {
- expect(coverageMetric(undefined, {}))
- .to.equal(null);
- });
-
it('should be set to "coverage"', () => {
const componentWithCoverage = {
...exampleComponent,
]
};
- expect(coverageMetric(null, requestComponents(componentWithCoverage)))
+ expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
.to.equal('coverage');
});
]
};
- expect(coverageMetric(null, requestComponents(componentWithCoverage)))
+ expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
.to.equal('it_coverage');
});
]
};
- expect(coverageMetric(null, requestComponents(componentWithCoverage)))
+ expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
.to.equal('overall_coverage');
});
msr: []
};
- expect(coverageMetric(null, requestComponents(componentWithCoverage)))
+ expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
.to.equal('it_coverage');
});
+ });
+ describe('baseBreadcrumbs', () => {
+ it('should be empty', () => {
+ const component = { key: 'A' };
+ const breadcrumbs = [{ key: 'A' }];
- it('should not be reset after set once', () => {
- const componentWithCoverage = {
- ...exampleComponent,
- msr: [
- { key: 'coverage', val: 13 }
- ]
- };
+ expect(current(initialState, initComponentAction(component, breadcrumbs)).baseBreadcrumbs)
+ .to.have.length(0);
+ });
- expect(coverageMetric('overall_coverage', requestComponents(componentWithCoverage)))
- .to.equal('overall_coverage');
+ it('should set baseBreadcrumbs', () => {
+ const component = { key: 'A' };
+ const breadcrumbs = [{ key: 'BASE' }, { key: 'A' }];
+
+ expect(current(initialState, initComponentAction(component, breadcrumbs)).baseBreadcrumbs)
+ .to.have.length(1);
});
});
});
+ describe('bucket', () => {
+ it('should add initial component', () => {
+ expect(bucket([], initComponentAction(exampleComponent)))
+ .to.deep.equal([exampleComponent]);
+ });
+
+ it('should add browsed component', () => {
+ const componentBefore = { key: 'A' };
+ const childrenBefore = [{ key: 'B' }];
+ const breadcrumbsBefore = [{ key: 'A' }];
+
+ const bucketAfter = [
+ { key: 'A', breadcrumbs: [{ key: 'A' }], children: [{ key: 'B' }] },
+ { key: 'B', breadcrumbs: [{ key: 'A' }, { key: 'B' }] }
+ ];
+
+ expect(bucket([], browseAction(componentBefore, childrenBefore, breadcrumbsBefore)))
+ .to.deep.equal(bucketAfter);
+ });
+
+ it('should merge new components', () => {
+ const componentBefore = { key: 'A' };
+ const childrenBefore = [{ key: 'B' }];
+ const breadcrumbsBefore = [{ key: 'A' }];
+
+ const bucketBefore = [
+ { key: 'A' },
+ { key: 'B' }
+ ];
+
+ const bucketAfter = [
+ {
+ key: 'A',
+ breadcrumbs: [{ key: 'A' }],
+ children: [{ key: 'B' }]
+ },
+ {
+ key: 'B',
+ breadcrumbs: [{ key: 'A' }, { key: 'B' }]
+ }
+ ];
+
+ expect(bucket(bucketBefore, browseAction(componentBefore, childrenBefore, breadcrumbsBefore)))
+ .to.deep.equal(bucketAfter);
+ });
+
+ it('should work twice in a row', () => {
+ const componentA = { key: 'A' };
+ const childrenA = [{ key: 'B' }];
+ const breadcrumbsA = [{ key: 'A' }];
+
+ const componentB = { key: 'B' };
+ const childrenB = [{ key: 'C' }];
+ const breadcrumbsB = [{ key: 'A' }, { key: 'B' }];
+
+ const bucketAfter = [
+ {
+ key: 'A',
+ breadcrumbs: [{ key: 'A' }],
+ children: [{ key: 'B' }]
+ },
+ {
+ key: 'B',
+ breadcrumbs: [{ key: 'A' }, { key: 'B' }],
+ children: [{ key: 'C' }]
+ },
+ {
+ key: 'C',
+ breadcrumbs: [{ key: 'A' }, { key: 'B' }, { key: 'C' }]
+ }
+ ];
+
+ const afterFirstPass = bucket([], browseAction(componentA, childrenA, breadcrumbsA));
+ const afterSecondPass = bucket(afterFirstPass, browseAction(componentB, childrenB, breadcrumbsB));
+
+ expect(afterSecondPass)
+ .to.deep.equal(bucketAfter);
+ });
+ });
});
});