summaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/api/components.js6
-rw-r--r--server/sonar-web/src/main/js/apps/code/actions/index.js70
-rw-r--r--server/sonar-web/src/main/js/apps/code/app.js18
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js13
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js19
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Code.js84
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Component.js64
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js14
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js17
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentName.js35
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Components.js42
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js16
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/SourceViewer.js32
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Truncated.js14
-rw-r--r--server/sonar-web/src/main/js/apps/code/reducers/index.js82
-rw-r--r--server/sonar-web/src/main/js/apps/code/store/configureStore.js16
-rw-r--r--server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js47
-rw-r--r--server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js15
-rw-r--r--server/sonar-web/src/main/less/pages.less1
-rw-r--r--server/sonar-web/src/main/less/pages/code.less40
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/resources_controller.rb1
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/code_controller.rb29
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/code/index.html.erb3
23 files changed, 678 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js
index 7b319dd0fe7..24f8c446a6f 100644
--- a/server/sonar-web/src/main/js/api/components.js
+++ b/server/sonar-web/src/main/js/api/components.js
@@ -43,3 +43,9 @@ export function getFiles (componentKey, metrics = []) {
return r.filter(component => component.qualifier === 'FIL');
});
}
+
+export function getComponent (componentKey, metrics = []) {
+ const url = baseUrl + '/api/resources/index';
+ const data = { resource: componentKey, metrics: metrics.join(',') };
+ return getJSON(url, data).then(r => r[0]);
+}
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
new file mode 100644
index 00000000000..9e9df67cfea
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/actions/index.js
@@ -0,0 +1,70 @@
+import _ from 'underscore';
+
+import { getChildren, getComponent } from '../../../api/components';
+
+
+const METRICS = [
+ 'ncloc',
+ 'sqale_index',
+ 'violations',
+ // TODO handle other types of coverage
+ 'coverage',
+ 'duplicated_lines_density'
+];
+
+
+export const INIT = 'INIT';
+export const BROWSE = 'BROWSE';
+export const RECEIVE_COMPONENTS = 'RECEIVE_COMPONENTS';
+export const SHOW_SOURCE = 'SHOW_SOURCE';
+
+
+export function requestComponents (baseComponent) {
+ return {
+ type: BROWSE,
+ baseComponent
+ };
+}
+
+
+export function receiveComponents (baseComponent, components) {
+ return {
+ type: RECEIVE_COMPONENTS,
+ baseComponent,
+ components
+ };
+}
+
+
+export function showSource (component) {
+ return {
+ type: SHOW_SOURCE,
+ component
+ };
+}
+
+
+function fetchChildren (dispatch, baseComponent) {
+ dispatch(requestComponents(baseComponent));
+ return getChildren(baseComponent.key, METRICS)
+ .then(components => _.sortBy(components, 'name'))
+ .then(components => dispatch(receiveComponents(baseComponent, components)));
+}
+
+
+export function initComponent (baseComponent) {
+ return dispatch => {
+ return getComponent(baseComponent.key, METRICS)
+ .then(component => fetchChildren(dispatch, component));
+ };
+}
+
+
+export function fetchComponents (baseComponent) {
+ return (dispatch, getState) => {
+ const { fetching } = getState();
+ if (!fetching) {
+ return fetchChildren(dispatch, baseComponent);
+ }
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/code/app.js b/server/sonar-web/src/main/js/apps/code/app.js
new file mode 100644
index 00000000000..c16b4aa3057
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/app.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { render } from 'react-dom';
+import { Provider } from 'react-redux';
+
+import Code from './components/Code';
+import configureStore from './store/configureStore';
+
+
+const store = configureStore();
+
+
+window.sonarqube.appStarted.then(({ el, ...other }) => {
+ render(
+ <Provider store={store}>
+ <Code {...other}/>
+ </Provider>,
+ document.querySelector(el));
+});
diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js b/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js
new file mode 100644
index 00000000000..2e8f4f484f0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/Breadcrumb.js
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import ComponentName from './ComponentName';
+
+
+const Breadcrumb = ({ component, onBrowse }) => (
+ <ComponentName
+ component={component}
+ onBrowse={onBrowse}/>
+);
+
+
+export default Breadcrumb;
diff --git a/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js
new file mode 100644
index 00000000000..95a54ffc6e1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.js
@@ -0,0 +1,19 @@
+import React from 'react';
+
+import Breadcrumb from './Breadcrumb';
+
+
+const Breadcrumbs = ({ breadcrumbs, onBrowse }) => (
+ <ul className="code-breadcrumbs">
+ {breadcrumbs.map((component, index) => (
+ <li key={component.key}>
+ <Breadcrumb
+ component={component}
+ onBrowse={index + 1 < breadcrumbs.length ? onBrowse : null}/>
+ </li>
+ ))}
+ </ul>
+);
+
+
+export default Breadcrumbs;
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
new file mode 100644
index 00000000000..d894ab8c952
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/Code.js
@@ -0,0 +1,84 @@
+import classNames from 'classnames';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+
+import Components from './Components';
+import Breadcrumbs from './Breadcrumbs';
+import SourceViewer from './SourceViewer';
+import { initComponent, fetchComponents, showSource } from '../actions';
+import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
+
+
+class Code extends Component {
+ componentDidMount () {
+ const { dispatch, component } = this.props;
+ dispatch(initComponent(component));
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (nextProps.component !== this.props.component) {
+ const { dispatch, component } = this.props;
+ dispatch(initComponent(component));
+ }
+ }
+
+ 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));
+ }
+ }
+
+ render () {
+ const { fetching, baseComponent, components, breadcrumbs, sourceViewer } = this.props;
+ const shouldShowBreadcrumbs = Array.isArray(breadcrumbs) && breadcrumbs.length > 1;
+ const shouldShowComponents = !sourceViewer && components;
+ const shouldShowSourceViewer = sourceViewer;
+
+ const componentsClassName = classNames('spacer-top', { 'new-loading': fetching });
+
+ return (
+ <TooltipsContainer options={{ delay: { show: 500, hide: 0 } }}>
+ <div className="page">
+ <header className="page-header">
+ <h1 className="page-title">{window.t('code.page')}</h1>
+
+ {fetching && (
+ <i className="spinner"/>
+ )}
+
+ {shouldShowBreadcrumbs && (
+ <Breadcrumbs
+ breadcrumbs={breadcrumbs}
+ onBrowse={this.handleBrowse.bind(this)}/>
+ )}
+ </header>
+
+ {shouldShowComponents && (
+ <div className={componentsClassName}>
+ <Components
+ baseComponent={baseComponent}
+ components={components}
+ onBrowse={this.handleBrowse.bind(this)}/>
+ </div>
+ )}
+
+ {shouldShowSourceViewer && (
+ <div className="spacer-top">
+ <SourceViewer component={sourceViewer}/>
+ </div>
+ )}
+ </div>
+ </TooltipsContainer>
+ );
+ }
+}
+
+
+export default connect(state => state)(Code);
diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.js b/server/sonar-web/src/main/js/apps/code/components/Component.js
new file mode 100644
index 00000000000..e319d9ef826
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/Component.js
@@ -0,0 +1,64 @@
+import React from 'react';
+
+import ComponentName from './ComponentName';
+import ComponentMeasure from './ComponentMeasure';
+import ComponentDetach from './ComponentDetach';
+
+
+const Component = ({ component, onBrowse }) => (
+ <tr>
+ <td className="thin nowrap">
+ <span className="spacer-right">
+ <ComponentDetach component={component}/>
+ </span>
+ </td>
+ <td>
+ <ComponentName
+ component={component}
+ onBrowse={onBrowse}/>
+ </td>
+ <td className="thin nowrap text-right">
+ <div className="code-components-cell">
+ <ComponentMeasure
+ component={component}
+ metricKey="ncloc"
+ metricType="SHORT_INT"/>
+ </div>
+ </td>
+ <td className="thin nowrap text-right">
+ <div className="code-components-cell">
+ <ComponentMeasure
+ component={component}
+ metricKey="sqale_index"
+ metricType="SHORT_WORK_DUR"/>
+ </div>
+ </td>
+ <td className="thin nowrap text-right">
+ <div className="code-components-cell">
+ <ComponentMeasure
+ component={component}
+ metricKey="violations"
+ metricType="SHORT_INT"/>
+ </div>
+ </td>
+ <td className="thin nowrap text-right">
+ <div className="code-components-cell">
+ <ComponentMeasure
+ component={component}
+ metricKey="coverage"
+ metricType="PERCENT"/>
+ </div>
+ </td>
+ <td className="thin nowrap text-right">
+ <div className="code-components-cell">
+ <ComponentMeasure
+ component={component}
+ metricKey="duplicated_lines_density"
+ metricType="PERCENT"/>
+ </div>
+ </td>
+ </tr>
+);
+
+
+export default Component;
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js
new file mode 100644
index 00000000000..5459c7a5a6b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import { getComponentUrl } from '../../../helpers/urls';
+
+
+const ComponentDetach = ({ component }) => (
+ <a
+ className="icon-detach"
+ target="_blank"
+ href={getComponentUrl(component.key)}/>
+);
+
+
+export default ComponentDetach;
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js
new file mode 100644
index 00000000000..112ee07dc9f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.js
@@ -0,0 +1,17 @@
+import _ from 'underscore';
+import React from 'react';
+
+import { formatMeasure } from '../../../helpers/measures';
+
+
+const ComponentMeasure = ({ component, metricKey, metricType }) => {
+ const measure = _.findWhere(component.msr, { key: metricKey });
+ return (
+ <span>
+ {measure ? formatMeasure(measure.val, metricType) : ''}
+ </span>
+ );
+};
+
+
+export default ComponentMeasure;
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
new file mode 100644
index 00000000000..40031d80ec5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
@@ -0,0 +1,35 @@
+import React from 'react';
+
+import Truncated from './Truncated';
+import QualifierIcon from '../../../components/shared/qualifier-icon';
+
+
+// TODO collapse dirs
+
+const Component = ({ component, onBrowse }) => {
+ const handleClick = (e) => {
+ e.preventDefault();
+ onBrowse(component);
+ };
+
+ return (
+ <Truncated title={component.name}>
+ <QualifierIcon qualifier={component.qualifier}/>
+ {' '}
+ {onBrowse ? (
+ <a
+ onClick={handleClick}
+ href="#">
+ {component.name}
+ </a>
+ ) : (
+ <span>
+ {component.name}
+ </span>
+ )}
+ </Truncated>
+ );
+};
+
+
+export default Component;
diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.js b/server/sonar-web/src/main/js/apps/code/components/Components.js
new file mode 100644
index 00000000000..1167e319296
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/Components.js
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import Component from './Component';
+import ComponentsEmpty from './ComponentsEmpty';
+
+
+const Components = ({ baseComponent, components, onBrowse }) => (
+ <table className="data zebra">
+ <thead>
+ <tr>
+ <th className="thin nowrap">&nbsp;</th>
+ <th>&nbsp;</th>
+ <th className="thin nowrap text-right">{window.t('metric.ncloc.name')}</th>
+ <th className="thin nowrap text-right">{window.t('metric.sqale_index.short_name')}</th>
+ <th className="thin nowrap text-right">{window.t('metric.violations.name')}</th>
+ <th className="thin nowrap text-right">{window.t('metric.coverage.name')}</th>
+ <th className="thin nowrap text-right">{window.t('metric.duplicated_lines_density.short_name')}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <Component component={baseComponent}/>
+ <tr className="blank">
+ <td colSpan="7">&nbsp;</td>
+ </tr>
+ </tbody>
+ <tbody>
+ {components.length ? (
+ components.map(component => (
+ <Component
+ key={component.key}
+ component={component}
+ onBrowse={onBrowse}/>
+ ))
+ ) : (
+ <ComponentsEmpty/>
+ )}
+ </tbody>
+ </table>
+);
+
+
+export default Components;
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js
new file mode 100644
index 00000000000..81f702fe578
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.js
@@ -0,0 +1,16 @@
+import React from 'react';
+
+
+const ComponentsEmpty = () => (
+ <tr>
+ <td colSpan="2">
+ {window.t('no_results')}
+ </td>
+ <td colSpan="5">
+ &nbsp;
+ </td>
+ </tr>
+);
+
+
+export default ComponentsEmpty;
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
new file mode 100644
index 00000000000..26fcc606833
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/SourceViewer.js
@@ -0,0 +1,32 @@
+import React, { Component } from 'react';
+
+import BaseSourceViewer from '../../../components/source-viewer/main';
+
+
+export default class SourceViewer extends Component {
+ componentDidMount () {
+ this.renderSourceViewer();
+ }
+
+ componentDidUpdate () {
+ this.renderSourceViewer();
+ }
+
+ componentWillUnmount() {
+ this.destroySourceViewer();
+ }
+
+ renderSourceViewer () {
+ this.sourceViewer = new BaseSourceViewer();
+ this.sourceViewer.render().$el.appendTo(this.refs.container);
+ this.sourceViewer.open(this.props.component.uuid);
+ }
+
+ destroySourceViewer () {
+ this.sourceViewer.destroy();
+ }
+
+ render () {
+ return <div ref="container"></div>;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/code/components/Truncated.js b/server/sonar-web/src/main/js/apps/code/components/Truncated.js
new file mode 100644
index 00000000000..cc58a7b79ff
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/Truncated.js
@@ -0,0 +1,14 @@
+import React from 'react';
+
+
+const Truncated = ({ children, title }) => (
+ <span
+ className="code-truncated"
+ title={title}
+ data-toggle="tooltip">
+ {children}
+ </span>
+);
+
+
+export default Truncated;
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
new file mode 100644
index 00000000000..d75ca950409
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/reducers/index.js
@@ -0,0 +1,82 @@
+import { combineReducers } from 'redux';
+
+import { BROWSE, RECEIVE_COMPONENTS, SHOW_SOURCE } 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;
+ }
+}
+
+
+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;
+ }
+}
+
+
+const rootReducer = combineReducers({
+ fetching,
+ baseComponent,
+ components,
+ breadcrumbs,
+ sourceViewer
+});
+
+
+export default rootReducer;
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
new file mode 100644
index 00000000000..20d1587895a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/store/configureStore.js
@@ -0,0 +1,16 @@
+import { createStore, applyMiddleware } from 'redux';
+import thunk from 'redux-thunk';
+import createLogger from 'redux-logger';
+
+import rootReducer from '../reducers';
+
+
+const createStoreWithMiddleware = applyMiddleware(
+ thunk,
+ createLogger()
+)(createStore);
+
+
+export default function configureStore () {
+ return createStoreWithMiddleware(rootReducer);
+}
diff --git a/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
index 0b48fb83fcd..c2328d6e7ae 100644
--- a/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
+++ b/server/sonar-web/src/main/js/components/mixins/tooltips-mixin.js
@@ -1,6 +1,8 @@
import $ from 'jquery';
+import React from 'react';
import ReactDOM from 'react-dom';
+
export const TooltipsMixin = {
componentDidMount () {
this.initTooltips();
@@ -39,3 +41,48 @@ export const TooltipsMixin = {
}
}
};
+
+
+export const TooltipsContainer = React.createClass({
+ componentDidMount () {
+ this.initTooltips();
+ },
+
+ componentWillUpdate() {
+ this.hideTooltips();
+ },
+
+ componentDidUpdate () {
+ this.initTooltips();
+ },
+
+ componentWillUnmount() {
+ this.destroyTooltips();
+ },
+
+ initTooltips () {
+ if ($.fn && $.fn.tooltip) {
+ const options = Object.assign({ container: 'body', placement: 'bottom', html: true }, this.props.options);
+ $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this))
+ .tooltip(options);
+ }
+ },
+
+ hideTooltips () {
+ if ($.fn && $.fn.tooltip) {
+ $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this))
+ .tooltip('hide');
+ }
+ },
+
+ destroyTooltips () {
+ if ($.fn && $.fn.tooltip) {
+ $('[data-toggle="tooltip"]', ReactDOM.findDOMNode(this))
+ .tooltip('destroy');
+ }
+ },
+
+ render () {
+ return this.props.children;
+ }
+});
diff --git a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
index b55e44cc2b3..1c097cb21e8 100644
--- a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
+++ b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
@@ -43,6 +43,11 @@ export default React.createClass({
return qualifier === 'DEV';
},
+ isView() {
+ const qualifier = _.last(this.props.component.breadcrumbs).qualifier;
+ return qualifier === 'VW' || qualifier === 'SVW';
+ },
+
periodParameter() {
let params = qs.parse(window.location.search.substr(1));
return params.period ? `&period=${params.period}` : '';
@@ -134,6 +139,15 @@ export default React.createClass({
</li>;
},
+ renderCodeLink() {
+ if (this.isView() || this.isDeveloper()) {
+ return null;
+ }
+
+ const url = `/code/index?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('code.page'), '/code');
+ },
+
renderComponentsLink() {
const url = `/components/index?id=${encodeURIComponent(this.props.component.key)}`;
return this.renderLink(url, window.t('components.page'), '/components');
@@ -300,6 +314,7 @@ export default React.createClass({
<ul className="nav navbar-nav nav-tabs">
{!this.isDeveloper() && this.renderFixedDashboards()}
{this.renderCustomDashboards()}
+ {this.renderCodeLink()}
{this.renderComponentsLink()}
{this.renderComponentIssuesLink()}
{this.renderTools()}
diff --git a/server/sonar-web/src/main/less/pages.less b/server/sonar-web/src/main/less/pages.less
index 05892910ed5..2280226ec49 100644
--- a/server/sonar-web/src/main/less/pages.less
+++ b/server/sonar-web/src/main/less/pages.less
@@ -7,3 +7,4 @@
@import "pages/login";
@import "pages/api-documentation";
@import "pages/overview";
+@import "pages/code";
diff --git a/server/sonar-web/src/main/less/pages/code.less b/server/sonar-web/src/main/less/pages/code.less
new file mode 100644
index 00000000000..ec86785ccc0
--- /dev/null
+++ b/server/sonar-web/src/main/less/pages/code.less
@@ -0,0 +1,40 @@
+@import (reference) "../mixins";
+@import (reference) "../variables";
+@import (reference) "../init/type";
+
+
+.code-breadcrumbs {
+ display: flex;
+ flex-wrap: wrap;
+ float: left;
+ margin-left: 10px;
+ padding-top: 5px;
+}
+
+.code-breadcrumbs > li {
+ padding: 0 5px;
+}
+
+.code-breadcrumbs > li::after {
+ position: relative;
+ top: -1px;
+ padding-left: 10px;
+ color: @secondFontColor;
+ font-size: 11px;
+ content: ">";
+}
+
+.code-breadcrumbs > li:last-child::after {
+ display: none;
+}
+
+.code-components-cell {
+ width: 100px;
+}
+
+.code-truncated {
+ display: inline-block;
+ vertical-align: text-top;
+ max-width: 300px;
+ .text-ellipsis;
+}
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/resources_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/resources_controller.rb
index 35918a606ab..eab0fdb9e0c 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/resources_controller.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/resources_controller.rb
@@ -366,6 +366,7 @@ class Api::ResourcesController < Api::ApiController
json = {
'id' => resource.id,
+ 'uuid' => resource.uuid,
'key' => resource.key,
'name' => resource.name,
'scope' => resource.scope,
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/code_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/code_controller.rb
new file mode 100644
index 00000000000..6eac33c87c5
--- /dev/null
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/code_controller.rb
@@ -0,0 +1,29 @@
+#
+# SonarQube, open source software quality management tool.
+# Copyright (C) 2008-2014 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# SonarQube is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# SonarQube is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+class CodeController < ApplicationController
+ before_filter :init_resource_for_user_role
+
+ SECTION=Navigation::SECTION_RESOURCE
+
+ def index
+
+ end
+
+end
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/code/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/code/index.html.erb
new file mode 100644
index 00000000000..f5600f6ca03
--- /dev/null
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/code/index.html.erb
@@ -0,0 +1,3 @@
+<% content_for :extra_script do %>
+ <script src="<%= ApplicationController.root_context -%>/js/bundles/code.js?v=<%= sonar_version -%>"></script>
+<% end %>