aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/apps/nav/app.jsx9
-rw-r--r--server/sonar-web/src/main/js/apps/nav/component/component-nav-breadcrumbs.jsx23
-rw-r--r--server/sonar-web/src/main/js/apps/nav/component/component-nav-favorite.jsx15
-rw-r--r--server/sonar-web/src/main/js/apps/nav/component/component-nav-menu.jsx226
-rw-r--r--server/sonar-web/src/main/js/apps/nav/component/component-nav-meta.jsx13
-rw-r--r--server/sonar-web/src/main/js/apps/nav/component/component-nav.jsx50
-rw-r--r--server/sonar-web/src/main/js/apps/nav/dashboard-name-mixin.jsx11
-rw-r--r--server/sonar-web/src/main/js/apps/nav/global/global-nav-branding.jsx (renamed from server/sonar-web/src/main/js/apps/nav/global-nav-branding.jsx)0
-rw-r--r--server/sonar-web/src/main/js/apps/nav/global/global-nav-menu.jsx (renamed from server/sonar-web/src/main/js/apps/nav/global-nav-menu.jsx)13
-rw-r--r--server/sonar-web/src/main/js/apps/nav/global/global-nav-search.jsx (renamed from server/sonar-web/src/main/js/apps/nav/global-nav-search.jsx)0
-rw-r--r--server/sonar-web/src/main/js/apps/nav/global/global-nav-user.jsx (renamed from server/sonar-web/src/main/js/apps/nav/global-nav-user.jsx)0
-rw-r--r--server/sonar-web/src/main/js/apps/nav/global/global-nav.jsx (renamed from server/sonar-web/src/main/js/apps/nav/global-nav.jsx)0
-rw-r--r--server/sonar-web/src/main/js/apps/nav/global/search-view.js (renamed from server/sonar-web/src/main/js/apps/nav/search-view.js)2
-rw-r--r--server/sonar-web/src/main/js/apps/nav/global/shortcuts-help-view.js (renamed from server/sonar-web/src/main/js/apps/nav/shortcuts-help-view.js)2
-rw-r--r--server/sonar-web/src/main/js/components/shared/favorite.jsx44
-rw-r--r--server/sonar-web/src/main/js/components/shared/qualifier-icon.jsx11
-rw-r--r--server/sonar-web/src/main/less/init/icons.less33
-rw-r--r--server/sonar-web/test/intern.js10
-rw-r--r--server/sonar-web/test/unit/nav/component/component-nav-breadcrumbs.spec.js27
19 files changed, 474 insertions, 15 deletions
diff --git a/server/sonar-web/src/main/js/apps/nav/app.jsx b/server/sonar-web/src/main/js/apps/nav/app.jsx
index d7bcc337d81..48a708e321b 100644
--- a/server/sonar-web/src/main/js/apps/nav/app.jsx
+++ b/server/sonar-web/src/main/js/apps/nav/app.jsx
@@ -1,15 +1,22 @@
import React from 'react';
-import GlobalNav from './global-nav';
+import GlobalNav from './global/global-nav';
+import ComponentNav from './component/component-nav';
export default {
start(options) {
window.requestMessages().done(() => {
this.renderGlobalNav(options);
+ options.space === 'component' && this.renderComponentNav(options);
});
},
renderGlobalNav(options) {
const el = document.getElementById('global-navigation');
React.render(<GlobalNav {...options}/>, el);
+ },
+
+ renderComponentNav(options) {
+ const el = document.getElementById('context-navigation');
+ React.render(<ComponentNav {...options}/>, el);
}
};
diff --git a/server/sonar-web/src/main/js/apps/nav/component/component-nav-breadcrumbs.jsx b/server/sonar-web/src/main/js/apps/nav/component/component-nav-breadcrumbs.jsx
new file mode 100644
index 00000000000..1bb643c11ac
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/nav/component/component-nav-breadcrumbs.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import QualifierIcon from 'components/shared/qualifier-icon';
+
+export default React.createClass({
+ render() {
+ if (!this.props.breadcrumbs) {
+ return null;
+ }
+ const items = this.props.breadcrumbs.map((item, index) => {
+ const url = `${window.baseUrl}/dashboard/index?id=${encodeURIComponent(item.key)}`;
+ return (
+ <li key={index}>
+ <a href={url}>
+ <QualifierIcon qualifier={item.qualifier}/>&nbsp;{item.name}
+ </a>
+ </li>
+ );
+ });
+ return (
+ <ul className="nav navbar-nav nav-crumbs">{items}</ul>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/component/component-nav-favorite.jsx b/server/sonar-web/src/main/js/apps/nav/component/component-nav-favorite.jsx
new file mode 100644
index 00000000000..8e2b8624abd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/nav/component/component-nav-favorite.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import Favorite from 'components/shared/favorite';
+
+export default React.createClass({
+ render() {
+ if (!this.props.canBeFavorite) {
+ return null;
+ }
+ return (
+ <div className="navbar-context-favorite">
+ <Favorite component={this.props.component} favorite={this.props.favorite}/>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/component/component-nav-menu.jsx b/server/sonar-web/src/main/js/apps/nav/component/component-nav-menu.jsx
new file mode 100644
index 00000000000..03e1c28f7ee
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/nav/component/component-nav-menu.jsx
@@ -0,0 +1,226 @@
+import React from 'react';
+import DashboardNameMixin from '../dashboard-name-mixin';
+
+const SETTINGS_URLS = [
+ '/project/settings', '/project/profile', '/project/qualitygate', '/manual_measures/index',
+ '/action_plans/index', '/project/links', '/project_roles/index', '/project/history', '/project/key',
+ '/project/deletion'
+];
+
+const MORE_URLS = ['/dashboards', '/dashboard', '/plugins/resource'];
+
+export default React.createClass({
+ mixins: [DashboardNameMixin],
+
+ activeLink(url) {
+ return window.location.pathname.indexOf(window.baseUrl + url) === 0 ? 'active' : null;
+ },
+
+ renderLink(url, title, highlightUrl = url) {
+ let fullUrl = window.baseUrl + url;
+ return (
+ <li key={highlightUrl} className={this.activeLink(highlightUrl)}>
+ <a href={fullUrl}>{title}</a>
+ </li>
+ );
+ },
+
+ renderOverviewLink() {
+ const url = `/overview/index?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('overview.page'), '/overview');
+ },
+
+ renderComponentsLink() {
+ const url = `/components/index?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('components.page'), '/components');
+ },
+
+ renderComponentIssuesLink() {
+ const url = `/component_issues/index?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('issues.page'), '/component_issues');
+ },
+
+ renderAdministration() {
+ if (!this.props.conf.showSettings) {
+ return null;
+ }
+ let isSettingsActive = SETTINGS_URLS.some(url => {
+ return window.location.href.indexOf(url) !== -1;
+ }),
+ className = 'dropdown' + (isSettingsActive ? ' active' : '');
+ return (
+ <li className={className}>
+ <a className="dropdown-toggle navbar-admin-link" data-toggle="dropdown" href="#">
+ {window.t('layout.settings')}&nbsp;<i className="icon-dropdown"/></a>
+ <ul className="dropdown-menu">
+ {this.renderSettingsLink()}
+ {this.renderProfilesLink()}
+ {this.renderQualityGatesLink()}
+ {this.renderCustomMeasuresLink()}
+ {this.renderActionPlansLink()}
+ {this.renderLinksLink()}
+ {this.renderPermissionsLink()}
+ {this.renderHistoryLink()}
+ {this.renderUpdateKeyLink()}
+ {this.renderDeletionLink()}
+ {this.renderExtensions()}
+ </ul>
+ </li>
+ );
+ },
+
+ renderSettingsLink() {
+ const url = `/project/settings?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('project_settings.page'), '/project/settings');
+ },
+
+ renderProfilesLink() {
+ if (!this.props.conf.showQualityProfiles) {
+ return null;
+ }
+ const url = `/project/profile?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('project_quality_profiles.page'), '/project/profile');
+ },
+
+ renderQualityGatesLink() {
+ if (!this.props.conf.showQualityGates) {
+ return null;
+ }
+ const url = `/project/qualitygate?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('project_quality_gate.page'), '/project/qualitygate');
+ },
+
+ renderCustomMeasuresLink() {
+ if (!this.props.conf.showManualMeasures) {
+ return null;
+ }
+ const url = `/custom_measures?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('custom_measures.page'), '/custom_measures');
+ },
+
+ renderActionPlansLink() {
+ if (!this.props.conf.showActionPlans) {
+ return null;
+ }
+ const url = `/action_plans?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('action_plans.page'), '/action_plans');
+ },
+
+ renderLinksLink() {
+ if (!this.props.conf.showLinks) {
+ return null;
+ }
+ const url = `/project/links?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('project_links.page'), '/project/links');
+ },
+
+ renderPermissionsLink() {
+ if (!this.props.conf.showPermissions) {
+ return null;
+ }
+ const url = `/project_roles?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('permissions.page'), '/project_roles');
+ },
+
+ renderHistoryLink() {
+ if (!this.props.conf.showHistory) {
+ return null;
+ }
+ const url = `/project/history?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('project_history.page'), '/project/history');
+ },
+
+ renderUpdateKeyLink() {
+ if (!this.props.conf.showUpdateKey) {
+ return null;
+ }
+ const url = `/project/key?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('update_key.page'), '/project/key');
+ },
+
+ renderDeletionLink() {
+ if (!this.props.conf.showDeletion) {
+ return null;
+ }
+ const url = `/project/deletion?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, window.t('deletion.page'), '/project/deletion');
+ },
+
+ renderExtensions() {
+ let extensions = this.props.conf.extensions || [];
+ return extensions.map(e => {
+ return this.renderLink(e.url, e.name, e.url);
+ });
+ },
+
+ renderMore() {
+ let isActive = MORE_URLS.some(url => {
+ return window.location.href.indexOf(url) !== -1;
+ }),
+ className = 'dropdown' + (isActive ? ' active' : '');
+ return (
+ <li className={className}>
+ <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+ {window.t('more')}&nbsp;<i className="icon-dropdown"></i>
+ </a>
+ <ul className="dropdown-menu">
+ {this.renderDashboards()}
+ {this.renderDashboardManagementLink()}
+ {this.renderTools()}
+ </ul>
+ </li>
+ );
+ },
+
+ renderDashboards() {
+ let dashboards = (this.props.component.dashboards || []).map(d => {
+ let url = `${window.baseUrl}/dashboard?id=${encodeURIComponent(this.props.component.key)}&did=${d.key}`;
+ let name = this.getLocalizedDashboardName(d.name);
+ return this.renderLink(url, name);
+ });
+ return [<li key="0" className="dropdown-header">{window.t('layout.dashboards')}</li>].concat(dashboards);
+ },
+
+ renderDashboardManagementLink() {
+ if (!window.SS.user) {
+ return null;
+ }
+ let url = `${window.baseUrl}/dashboards?resource=${encodeURIComponent(this.props.component.key)}`;
+ let name = window.t('dashboard.manage_dashboards');
+ return [
+ <li key="dashboard-divider" className="small-divider"></li>,
+ this.renderLink(url, name, '/dashboards')
+ ];
+ },
+
+ renderTools() {
+ let component = this.props.component;
+ if (!component.isComparable && !_.size(component.extensions)) {
+ return null;
+ }
+ let tools = [
+ <li key="tools-divider" className="divider"></li>,
+ <li key="tools" className="dropdown-header">Tools</li>
+ ];
+ if (component.isComparable) {
+ let compareUrl = `/comparison/index?resource=${component.key}`;
+ tools.push(this.renderLink(compareUrl, window.t('comparison.page')));
+ }
+ (component.extensions || []).forEach(e => {
+ tools.push(this.renderLink(e.url, e.name));
+ });
+ return tools;
+ },
+
+ render() {
+ return (
+ <ul className="nav navbar-nav nav-tabs">
+ {this.renderOverviewLink()}
+ {this.renderComponentsLink()}
+ {this.renderComponentIssuesLink()}
+ {this.renderAdministration()}
+ {this.renderMore()}
+ </ul>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/component/component-nav-meta.jsx b/server/sonar-web/src/main/js/apps/nav/component/component-nav-meta.jsx
new file mode 100644
index 00000000000..8ebb8e91a42
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/nav/component/component-nav-meta.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ const version = this.props.version ? `Version ${this.props.version}` : null;
+ const snapshotDate = this.props.snapshotDate ? moment(this.props.snapshotDate).format('LLL') : null;
+ return (
+ <div className="navbar-right navbar-context-meta">
+ {version} {snapshotDate}
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/component/component-nav.jsx b/server/sonar-web/src/main/js/apps/nav/component/component-nav.jsx
new file mode 100644
index 00000000000..855482981d9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/nav/component/component-nav.jsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import ComponentNavFavorite from './component-nav-favorite';
+import ComponentNavBreadcrumbs from './component-nav-breadcrumbs';
+import ComponentNavMeta from './component-nav-meta';
+import ComponentNavMenu from './component-nav-menu';
+
+let $ = jQuery;
+
+export default React.createClass({
+ getInitialState() {
+ return { component: {}, conf: {} };
+ },
+
+ componentDidMount() {
+ this.loadDetails();
+ },
+
+ loadDetails() {
+ const url = `${window.baseUrl}/api/navigation/component`;
+ const data = { componentKey: this.props.componentKey };
+ $.get(url, data).done(r => {
+ this.setState({
+ component: r,
+ conf: r.configuration || {}
+ });
+ });
+ },
+
+ render() {
+ return (
+ <div className="container">
+ <ComponentNavFavorite
+ component={this.state.component.key}
+ favorite={this.state.component.isFavorite}
+ canBeFavorite={this.state.component.canBeFavorite}/>
+
+ <ComponentNavBreadcrumbs
+ breadcrumbs={this.state.component.breadcrumbs}/>
+
+ <ComponentNavMeta
+ version={this.state.component.version}
+ snapshotDate={this.state.component.snapshotDate}/>
+
+ <ComponentNavMenu
+ component={this.state.component}
+ conf={this.state.conf}/>
+ </div>
+ );
+ }
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/dashboard-name-mixin.jsx b/server/sonar-web/src/main/js/apps/nav/dashboard-name-mixin.jsx
new file mode 100644
index 00000000000..e8366f137c7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/nav/dashboard-name-mixin.jsx
@@ -0,0 +1,11 @@
+export default {
+ getLocalizedDashboardName(baseName) {
+ var l10nKey = 'dashboard.' + baseName + '.name';
+ var l10nLabel = window.t(l10nKey);
+ if (l10nLabel !== l10nKey) {
+ return l10nLabel;
+ } else {
+ return baseName;
+ }
+ }
+};
diff --git a/server/sonar-web/src/main/js/apps/nav/global-nav-branding.jsx b/server/sonar-web/src/main/js/apps/nav/global/global-nav-branding.jsx
index dccd40f4a36..dccd40f4a36 100644
--- a/server/sonar-web/src/main/js/apps/nav/global-nav-branding.jsx
+++ b/server/sonar-web/src/main/js/apps/nav/global/global-nav-branding.jsx
diff --git a/server/sonar-web/src/main/js/apps/nav/global-nav-menu.jsx b/server/sonar-web/src/main/js/apps/nav/global/global-nav-menu.jsx
index 9aeb24baa77..037b0231210 100644
--- a/server/sonar-web/src/main/js/apps/nav/global-nav-menu.jsx
+++ b/server/sonar-web/src/main/js/apps/nav/global/global-nav-menu.jsx
@@ -1,6 +1,9 @@
import React from 'react';
+import DashboardNameMixin from '../dashboard-name-mixin';
export default React.createClass({
+ mixins: [DashboardNameMixin],
+
getDefaultProps: function () {
return { globalDashboards: [], globalPages: [] };
},
@@ -9,16 +12,6 @@ export default React.createClass({
return window.location.pathname.indexOf(window.baseUrl + url) === 0 ? 'active' : null;
},
- getLocalizedDashboardName(baseName) {
- var l10nKey = 'dashboard.' + baseName + '.name';
- var l10nLabel = window.t(l10nKey);
- if (l10nLabel !== l10nKey) {
- return l10nLabel;
- } else {
- return baseName;
- }
- },
-
renderDashboardLink(dashboard) {
const url = `${window.baseUrl}/dashboard/index?did=${encodeURIComponent(dashboard.key)}`;
const name = this.getLocalizedDashboardName(dashboard.name);
diff --git a/server/sonar-web/src/main/js/apps/nav/global-nav-search.jsx b/server/sonar-web/src/main/js/apps/nav/global/global-nav-search.jsx
index 157ba7b25f7..157ba7b25f7 100644
--- a/server/sonar-web/src/main/js/apps/nav/global-nav-search.jsx
+++ b/server/sonar-web/src/main/js/apps/nav/global/global-nav-search.jsx
diff --git a/server/sonar-web/src/main/js/apps/nav/global-nav-user.jsx b/server/sonar-web/src/main/js/apps/nav/global/global-nav-user.jsx
index d07c9a4d201..d07c9a4d201 100644
--- a/server/sonar-web/src/main/js/apps/nav/global-nav-user.jsx
+++ b/server/sonar-web/src/main/js/apps/nav/global/global-nav-user.jsx
diff --git a/server/sonar-web/src/main/js/apps/nav/global-nav.jsx b/server/sonar-web/src/main/js/apps/nav/global/global-nav.jsx
index b7d8782268a..b7d8782268a 100644
--- a/server/sonar-web/src/main/js/apps/nav/global-nav.jsx
+++ b/server/sonar-web/src/main/js/apps/nav/global/global-nav.jsx
diff --git a/server/sonar-web/src/main/js/apps/nav/search-view.js b/server/sonar-web/src/main/js/apps/nav/global/search-view.js
index d66a26508ff..233a1dc5f29 100644
--- a/server/sonar-web/src/main/js/apps/nav/search-view.js
+++ b/server/sonar-web/src/main/js/apps/nav/global/search-view.js
@@ -1,6 +1,6 @@
define([
'components/common/selectable-collection-view',
- './templates'
+ '../templates'
], function (SelectableCollectionView) {
var $ = jQuery,
diff --git a/server/sonar-web/src/main/js/apps/nav/shortcuts-help-view.js b/server/sonar-web/src/main/js/apps/nav/global/shortcuts-help-view.js
index 39024617f7a..b016a734d8c 100644
--- a/server/sonar-web/src/main/js/apps/nav/shortcuts-help-view.js
+++ b/server/sonar-web/src/main/js/apps/nav/global/shortcuts-help-view.js
@@ -1,6 +1,6 @@
define([
'components/common/modals',
- './templates'
+ '../templates'
], function (ModalView) {
return ModalView.extend({
diff --git a/server/sonar-web/src/main/js/components/shared/favorite.jsx b/server/sonar-web/src/main/js/components/shared/favorite.jsx
new file mode 100644
index 00000000000..09601d31cd1
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/favorite.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+
+let $ = jQuery;
+
+export default React.createClass({
+ propTypes: {
+ component: React.PropTypes.string.isRequired,
+ favorite: React.PropTypes.bool.isRequired
+ },
+
+ getInitialState() {
+ return { favorite: this.props.favorite };
+ },
+
+ toggleFavorite(e) {
+ e.preventDefault();
+ this.state.favorite ? this.removeFavorite() : this.addFavorite();
+ },
+
+ addFavorite() {
+ const url = `${window.baseUrl}/api/favourites`;
+ const data = { key: this.props.component };
+ $.ajax({ type: 'POST', url, data }).done(() => this.setState({ favorite: true }));
+ },
+
+ removeFavorite() {
+ const url = `${window.baseUrl}/api/favourites/${encodeURIComponent(this.props.component)}`;
+ $.ajax({ type: 'DELETE', url }).done(() => this.setState({ favorite: false }));
+ },
+
+ renderSVG() {
+ return (
+ <svg width="16" height="16" style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 1.41421 }}>
+ <path d="M15.4275,5.77678C15.4275,5.90773 15.3501,6.05059 15.1953,6.20536L11.9542,9.36608L12.7221,13.8304C12.728,13.872 12.731,13.9316 12.731,14.0089C12.731,14.1339 12.6998,14.2396 12.6373,14.3259C12.5748,14.4122 12.484,14.4554 12.3649,14.4554C12.2518,14.4554 12.1328,14.4197 12.0078,14.3482L7.99888,12.2411L3.98995,14.3482C3.85901,14.4197 3.73996,14.4554 3.63281,14.4554C3.50781,14.4554 3.41406,14.4122 3.35156,14.3259C3.28906,14.2396 3.25781,14.1339 3.25781,14.0089C3.25781,13.9732 3.26377,13.9137 3.27567,13.8304L4.04353,9.36608L0.793531,6.20536C0.644719,6.04464 0.570313,5.90178 0.570313,5.77678C0.570313,5.55654 0.736979,5.41964 1.07031,5.36606L5.55245,4.71428L7.56138,0.651781C7.67447,0.407729 7.8203,0.285703 7.99888,0.285703C8.17745,0.285703 8.32328,0.407729 8.43638,0.651781L10.4453,4.71428L14.9274,5.36606C15.2608,5.41964 15.4274,5.55654 15.4274,5.77678L15.4275,5.77678Z"
+ style={{ fillRule: 'nonzero' }}/>
+ </svg>
+ )
+ },
+
+ render() {
+ const className = this.state.favorite ? 'icon-star icon-star-favorite' : 'icon-star';
+ return <a onClick={this.toggleFavorite} className={className} href="#">{this.renderSVG()}</a>;
+ }
+});
diff --git a/server/sonar-web/src/main/js/components/shared/qualifier-icon.jsx b/server/sonar-web/src/main/js/components/shared/qualifier-icon.jsx
new file mode 100644
index 00000000000..e0f6e5a342d
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/qualifier-icon.jsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default React.createClass({
+ render() {
+ if (!this.props.qualifier) {
+ return null;
+ }
+ var className = 'icon-qualifier-' + this.props.qualifier.toLowerCase();
+ return <i className={className}/>;
+ }
+});
diff --git a/server/sonar-web/src/main/less/init/icons.less b/server/sonar-web/src/main/less/init/icons.less
index f0d20455d5e..19d7e0cbaae 100644
--- a/server/sonar-web/src/main/less/init/icons.less
+++ b/server/sonar-web/src/main/less/init/icons.less
@@ -316,14 +316,47 @@ a[class^="icon-"], a[class*=" icon-"] {
.square(16px);
background-size: 16px 16px;
background: no-repeat center center;
+ .trans !important;
}
.icon-favorite {
background-image: url('data:image/svg+xml,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M15.428%205.777c0%20.13-.078.274-.233.428l-3.24%203.16.767%204.465c.006.042.01.102.01.18%200%20.124-.032.23-.095.316-.062.086-.153.13-.272.13-.113%200-.232-.036-.357-.108l-4.01-2.107L3.99%2014.35c-.13.072-.25.107-.357.107-.125%200-.22-.043-.28-.13-.064-.085-.095-.19-.095-.316%200-.037.006-.096.018-.18l.768-4.464-3.25-3.16C.644%206.045.57%205.9.57%205.775c0-.22.167-.356.5-.41l4.482-.652L7.562.652c.112-.244.258-.366.437-.366.177%200%20.323.122.436.366l2.01%204.062%204.48.652c.335.054.5.19.5.41h.002z%22%20fill%3D%22%23F90%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E');
+ .rotate(72deg);
}
.icon-not-favorite {
background-image: url('data:image/svg+xml,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M15.428%205.777c0%20.13-.078.274-.233.428l-3.24%203.16.767%204.465c.006.042.01.102.01.18%200%20.124-.032.23-.095.316-.062.086-.153.13-.272.13-.113%200-.232-.036-.357-.108l-4.01-2.107L3.99%2014.35c-.13.072-.25.107-.357.107-.125%200-.22-.043-.28-.13-.064-.085-.095-.19-.095-.316%200-.037.006-.096.018-.18l.768-4.464-3.25-3.16C.644%206.045.57%205.9.57%205.775c0-.22.167-.356.5-.41l4.482-.652L7.562.652c.112-.244.258-.366.437-.366.177%200%20.323.122.436.366l2.01%204.062%204.48.652c.335.054.5.19.5.41h.002z%22%20fill%3D%22%23CDCDCD%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E');
}
+.icon-star {
+ .trans !important;
+}
+
+.icon-star path {
+ stroke: #777;
+ stroke-width: sqrt(2);
+ stroke-opacity: 1;
+ fill-opacity: 0;
+ .trans;
+}
+
+.icon-star-favorite {
+ animation: spin .6s forwards;
+}
+
+.icon-star-favorite path {
+ fill: rgb(255, 153, 0);
+ stroke-opacity: 0;
+ fill-opacity: 1;
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(144deg);
+ }
+}
+
.icon-help:before {
content: "\f059";
color: @blue;
diff --git a/server/sonar-web/test/intern.js b/server/sonar-web/test/intern.js
index 74b2a6bd9ef..ee712c33f18 100644
--- a/server/sonar-web/test/intern.js
+++ b/server/sonar-web/test/intern.js
@@ -18,7 +18,8 @@ define(['intern'], function (intern) {
'test/unit/application.spec',
'test/unit/issue.spec',
'test/unit/overview/card.spec',
- 'test/unit/code-with-issue-locations-helper.spec'
+ 'test/unit/code-with-issue-locations-helper.spec',
+ 'test/unit/nav/component/component-nav-breadcrumbs.spec'
],
functionalSuites: [
@@ -40,7 +41,12 @@ define(['intern'], function (intern) {
loaderOptions: {
paths: {
- 'react': 'build/js/libs/third-party/react-with-addons'
+ 'react': '../../build/js/libs/third-party/react-with-addons'
+ },
+ map: {
+ '*': {
+ 'components/shared/qualifier-icon': '../../build/js/components/shared/qualifier-icon'
+ }
}
}
};
diff --git a/server/sonar-web/test/unit/nav/component/component-nav-breadcrumbs.spec.js b/server/sonar-web/test/unit/nav/component/component-nav-breadcrumbs.spec.js
new file mode 100644
index 00000000000..4b2a5fecaee
--- /dev/null
+++ b/server/sonar-web/test/unit/nav/component/component-nav-breadcrumbs.spec.js
@@ -0,0 +1,27 @@
+define(function (require) {
+ var bdd = require('intern!bdd');
+ var assert = require('intern/chai!assert');
+
+ var React = require('react');
+ var TestUtils = React.addons.TestUtils;
+
+ var ComponentNavBreadcrumbs = require('build/js/apps/nav/component/component-nav-breadcrumbs');
+
+ bdd.describe('ComponentNavBreadcrumbs', function () {
+ bdd.it('should not render unless `props.breadcrumbs`', function () {
+ var result = React.renderToStaticMarkup(React.createElement(ComponentNavBreadcrumbs, null));
+ assert.equal(result, '<noscript></noscript>');
+ });
+
+ bdd.it('should not render breadcrumbs with one element', function () {
+ var breadcrumbs = [
+ { key: 'my-project', name: 'My Project', qualifier: 'TRK' }
+ ];
+ var result = TestUtils.renderIntoDocument(
+ React.createElement(ComponentNavBreadcrumbs, { breadcrumbs: breadcrumbs })
+ );
+ assert.equal(TestUtils.scryRenderedDOMComponentsWithTag(result, 'li').length, 1);
+ assert.equal(TestUtils.scryRenderedDOMComponentsWithTag(result, 'a').length, 1);
+ });
+ });
+});