diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-12-18 18:43:53 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-12-21 15:39:29 +0100 |
commit | 93aac6dc9b6926e901db41e0509d1cab6a9ef98b (patch) | |
tree | 3b70cb0a3a5d3caa6cd978a631f51517e4cf841e /server/sonar-web/src/main/js/apps/code | |
parent | 612eca3d6a8df519a502b31b7cbe8f735f34decb (diff) | |
download | sonarqube-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')
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); } |