aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/code
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-12-18 18:43:53 +0100
committerStas Vilchik <vilchiks@gmail.com>2015-12-21 15:39:29 +0100
commit93aac6dc9b6926e901db41e0509d1cab6a9ef98b (patch)
tree3b70cb0a3a5d3caa6cd978a631f51517e4cf841e /server/sonar-web/src/main/js/apps/code
parent612eca3d6a8df519a502b31b7cbe8f735f34decb (diff)
downloadsonarqube-93aac6dc9b6926e901db41e0509d1cab6a9ef98b.tar.gz
sonarqube-93aac6dc9b6926e901db41e0509d1cab6a9ef98b.zip
SONAR-7147 keep current state in the url
Diffstat (limited to 'server/sonar-web/src/main/js/apps/code')
-rw-r--r--server/sonar-web/src/main/js/apps/code/actions/index.js92
-rw-r--r--server/sonar-web/src/main/js/apps/code/app.js19
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Code.js32
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/SourceViewer.js8
-rw-r--r--server/sonar-web/src/main/js/apps/code/reducers/index.js137
-rw-r--r--server/sonar-web/src/main/js/apps/code/store/configureStore.js15
6 files changed, 167 insertions, 136 deletions
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 135c77758ab..dcf7e47d554 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,6 +1,8 @@
import _ from 'underscore';
+import { pushPath } from 'redux-simple-router';
import { getChildren, getComponent } from '../../../api/components';
+import { getComponentNavigation } from '../../../api/nav';
const METRICS = [
@@ -9,6 +11,7 @@ const METRICS = [
'violations',
'duplicated_lines_density'
];
+
const METRICS_WITH_COVERAGE = [
...METRICS,
'coverage',
@@ -19,60 +22,85 @@ const METRICS_WITH_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()));
};
}
diff --git a/server/sonar-web/src/main/js/apps/code/app.js b/server/sonar-web/src/main/js/apps/code/app.js
index c16b4aa3057..ce973644f69 100644
--- a/server/sonar-web/src/main/js/apps/code/app.js
+++ b/server/sonar-web/src/main/js/apps/code/app.js
@@ -1,18 +1,33 @@
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));
});
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 ff463ebf5d0..7f364cf3939 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,34 +5,31 @@ import { connect } from 'react-redux';
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 () {
@@ -83,5 +80,6 @@ class Code extends Component {
}
}
-
-export default connect(state => state)(Code);
+export default connect(state => {
+ return Object.assign({ routing: state.routing }, state.current);
+})(Code);
diff --git a/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js b/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js
index f207e4a0faf..09457d8dd06 100644
--- a/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js
+++ b/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js
@@ -8,6 +8,14 @@ export default class SourceViewer extends Component {
this.renderSourceViewer();
}
+ shouldComponentUpdate (nextProps) {
+ return nextProps.component.uuid !== this.props.component.uuid;
+ }
+
+ componentWillUpdate () {
+ this.destroySourceViewer();
+ }
+
componentDidUpdate () {
this.renderSourceViewer();
}
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 3a86e706e8d..08b74a6710e 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,76 +1,12 @@
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' });
@@ -85,25 +21,66 @@ function selectCoverageMetric (component) {
}
}
+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;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/code/store/configureStore.js b/server/sonar-web/src/main/js/apps/code/store/configureStore.js
index 821c23f5ae0..4b9f858a893 100644
--- a/server/sonar-web/src/main/js/apps/code/store/configureStore.js
+++ b/server/sonar-web/src/main/js/apps/code/store/configureStore.js
@@ -1,11 +1,11 @@
-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(
@@ -13,7 +13,12 @@ const createStoreWithMiddleware = applyMiddleware(
logger
)(createStore);
+const reducer = combineReducers({
+ routing: routeReducer,
+ current,
+ bucket
+});
export default function configureStore () {
- return createStoreWithMiddleware(rootReducer);
+ return createStoreWithMiddleware(reducer);
}