]> source.dussan.org Git - sonarqube.git/commitdiff
rewrite component navigation
authorStas Vilchik <vilchiks@gmail.com>
Thu, 20 Aug 2015 09:38:54 +0000 (11:38 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Thu, 20 Aug 2015 11:17:18 +0000 (13:17 +0200)
26 files changed:
server/sonar-web/src/main/js/apps/nav/app.jsx
server/sonar-web/src/main/js/apps/nav/component/component-nav-breadcrumbs.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/component/component-nav-favorite.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/component/component-nav-menu.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/component/component-nav-meta.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/component/component-nav.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/dashboard-name-mixin.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/global-nav-branding.jsx [deleted file]
server/sonar-web/src/main/js/apps/nav/global-nav-menu.jsx [deleted file]
server/sonar-web/src/main/js/apps/nav/global-nav-search.jsx [deleted file]
server/sonar-web/src/main/js/apps/nav/global-nav-user.jsx [deleted file]
server/sonar-web/src/main/js/apps/nav/global-nav.jsx [deleted file]
server/sonar-web/src/main/js/apps/nav/global/global-nav-branding.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/global/global-nav-menu.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/global/global-nav-search.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/global/global-nav-user.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/global/global-nav.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/global/search-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/global/shortcuts-help-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/nav/search-view.js [deleted file]
server/sonar-web/src/main/js/apps/nav/shortcuts-help-view.js [deleted file]
server/sonar-web/src/main/js/components/shared/favorite.jsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/shared/qualifier-icon.jsx [new file with mode: 0644]
server/sonar-web/src/main/less/init/icons.less
server/sonar-web/test/intern.js
server/sonar-web/test/unit/nav/component/component-nav-breadcrumbs.spec.js [new file with mode: 0644]

index d7bcc337d81a30172d371343fca23d4c2bea2873..48a708e321b427f27f042210a7bbfa1d894a36ea 100644 (file)
@@ -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 (file)
index 0000000..1bb643c
--- /dev/null
@@ -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 (file)
index 0000000..8e2b862
--- /dev/null
@@ -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 (file)
index 0000000..03e1c28
--- /dev/null
@@ -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 (file)
index 0000000..8ebb8e9
--- /dev/null
@@ -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 (file)
index 0000000..8554829
--- /dev/null
@@ -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 (file)
index 0000000..e8366f1
--- /dev/null
@@ -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-nav-branding.jsx
deleted file mode 100644 (file)
index dccd40f..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  renderLogo() {
-    const url = this.props.logoUrl || `${window.baseUrl}/images/logo.svg`;
-    const width = this.props.logoWidth || 30;
-    const title = window.t('layout.sonar.slogan');
-    return <img src={url} width={width} height="30" alt={title} title={title}/>
-  },
-
-  render() {
-    const homeUrl = window.baseUrl + '/';
-    const homeLinkClassName = 'navbar-brand' + (this.props.logoUrl ? ' navbar-brand-custom' : '');
-    return (
-        <div className="navbar-header">
-          <a className={homeLinkClassName} href={homeUrl}>{this.renderLogo()}</a>
-        </div>
-    );
-  }
-});
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-nav-menu.jsx
deleted file mode 100644 (file)
index 9aeb24b..0000000
+++ /dev/null
@@ -1,162 +0,0 @@
-import React from 'react';
-
-export default React.createClass({
-  getDefaultProps: function () {
-    return { globalDashboards: [], globalPages: [] };
-  },
-
-  activeLink(url) {
-    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);
-    return (
-        <li key={dashboard.name}>
-          <a href={url}>{name}</a>
-        </li>
-    );
-  },
-
-  renderDashboardsManagementLink() {
-    const url = `${window.baseUrl}/dashboards`;
-    return (
-        <li>
-          <a href={url}>{window.t('dashboard.manage_dashboards')}</a>
-        </li>
-    );
-  },
-
-  renderDashboards() {
-    const dashboards = this.props.globalDashboards.map(this.renderDashboardLink);
-    const canManageDashboards = !!window.SS.user;
-    return (
-        <li className="dropdown">
-          <a className="dropdown-toggle" data-toggle="dropdown" href="#">
-            {window.t('layout.dashboards')}&nbsp;<span className="icon-dropdown"/>
-          </a>
-          <ul className="dropdown-menu">
-            {dashboards}
-            {canManageDashboards ? <li className="divider"/> : null}
-            {canManageDashboards ? this.renderDashboardsManagementLink() : null}
-          </ul>
-        </li>
-    );
-  },
-
-  renderIssuesLink() {
-    const url = `${window.baseUrl}/issues/search`;
-    return (
-        <li className={this.activeLink('/issues')}>
-          <a href={url}>{window.t('issues.page')}</a>
-        </li>
-    );
-  },
-
-  renderMeasuresLink() {
-    const url = `${window.baseUrl}/measures/search?qualifiers[]=TRK`;
-    return (
-        <li className={this.activeLink('/measures')}>
-          <a href={url}>{window.t('layout.measures')}</a>
-        </li>
-    );
-  },
-
-  renderRulesLink() {
-    const url = `${window.baseUrl}/coding_rules`;
-    return (
-        <li className={this.activeLink('/coding_rules')}>
-          <a href={url}>{window.t('coding_rules.page')}</a>
-        </li>
-    );
-  },
-
-  renderProfilesLink() {
-    const url = `${window.baseUrl}/profiles`;
-    return (
-        <li className={this.activeLink('/profiles')}>
-          <a href={url}>{window.t('quality_profiles.page')}</a>
-        </li>
-    );
-  },
-
-  renderQualityGatesLink() {
-    const url = `${window.baseUrl}/quality_gates`;
-    return (
-        <li className={this.activeLink('/quality_gates')}>
-          <a href={url}>{window.t('quality_gates.page')}</a>
-        </li>
-    );
-  },
-
-  renderAdministrationLink() {
-    if (!window.SS.isUserAdmin) {
-      return null;
-    }
-    const url = `${window.baseUrl}/settings`;
-    return (
-        <li className={this.activeLink('/settings')}>
-          <a className="navbar-admin-link" href={url}>{window.t('layout.settings')}</a>
-        </li>
-    );
-  },
-
-  renderComparisonLink() {
-    const url = `${window.baseUrl}/comparison`;
-    return (
-        <li className={this.activeLink('/comparison')}>
-          <a href={url}>{window.t('comparison_global.page')}</a>
-        </li>
-    );
-  },
-
-  renderGlobalPageLink(globalPage, index) {
-    const url = window.baseUrl + globalPage.url;
-    return (
-        <li key={index}>
-          <a href={url}>{globalPage.name}</a>
-        </li>
-    );
-  },
-
-  renderMore() {
-    const globalPages = this.props.globalPages.map(this.renderGlobalPageLink);
-    return (
-        <li className="dropdown">
-          <a className="dropdown-toggle" data-toggle="dropdown" href="#">
-            {window.t('more')}&nbsp;<span className="icon-dropdown"/>
-          </a>
-          <ul className="dropdown-menu">
-            {this.renderComparisonLink()}
-            {globalPages}
-          </ul>
-        </li>
-    );
-  },
-
-  render() {
-    return (
-        <ul className="nav navbar-nav">
-          {this.renderDashboards()}
-          {this.renderIssuesLink()}
-          {this.renderMeasuresLink()}
-          {this.renderRulesLink()}
-          {this.renderProfilesLink()}
-          {this.renderQualityGatesLink()}
-          {this.renderAdministrationLink()}
-          {this.renderMore()}
-        </ul>
-    );
-  }
-});
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-nav-search.jsx
deleted file mode 100644 (file)
index 157ba7b..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-import React from 'react';
-import SearchView from './search-view';
-
-let $ = jQuery;
-
-function contains (root, node) {
-  while (node) {
-    if (node === root) {
-      return true;
-    }
-    node = node.parentNode;
-  }
-  return false;
-}
-
-export default React.createClass({
-  getInitialState() {
-    return { open: false };
-  },
-
-  componentDidMount() {
-    key('s', () => {
-      this.openSearch();
-      return false;
-    });
-  },
-
-  componentWillUnmount() {
-    this.closeSearch();
-    key.unbind('s');
-  },
-
-  openSearch() {
-    window.addEventListener('click', this.onClickOutside);
-    this.setState({ open: true }, this.renderSearchView);
-  },
-
-  closeSearch() {
-    window.removeEventListener('click', this.onClickOutside);
-    this.resetSearchView();
-    this.setState({ open: false });
-  },
-
-  renderSearchView() {
-    let searchContainer = React.findDOMNode(this.refs.container);
-    this.searchView = new SearchView({
-      model: new Backbone.Model(this.props),
-      hide: this.closeSearch
-    });
-    this.searchView.render().$el.appendTo(searchContainer);
-  },
-
-  resetSearchView() {
-    this.searchView && this.searchView.destroy();
-  },
-
-  onClick(e) {
-    e.preventDefault();
-    this.state.open ? this.closeSearch() : this.openSearch();
-  },
-
-  onClickOutside(e) {
-    if (!contains(React.findDOMNode(this.refs.dropdown), e.target)) {
-      this.closeSearch();
-    }
-  },
-
-  render() {
-    const dropdownClassName = 'dropdown' + (this.state.open ? ' open' : '');
-    return (
-        <li ref="dropdown" className={dropdownClassName}>
-          <a className="navbar-search-dropdown" href="#" onClick={this.onClick}>
-            <i className="icon-search navbar-icon"/>&nbsp;<i className="icon-dropdown"/>
-          </a>
-          <div ref="container" className="dropdown-menu dropdown-menu-right"></div>
-        </li>
-    );
-  }
-});
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-nav-user.jsx
deleted file mode 100644 (file)
index d07c9a4..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from 'react';
-import Avatar from 'components/shared/avatar';
-
-export default React.createClass({
-  renderAuthenticated() {
-    return (
-        <li className="dropdown">
-          <a className="dropdown-toggle" data-toggle="dropdown" href="#">
-            <Avatar email={window.SS.userEmail} size={20}/>&nbsp;
-            {window.SS.userName}&nbsp;<i className="icon-dropdown"/>
-          </a>
-          <ul className="dropdown-menu dropdown-menu-right">
-            <li>
-              <a href={`${window.baseUrl}/account/index`}>{window.t('layout.user_panel.my_profile')}</a>
-            </li>
-            <li>
-              <a onClick={this.handleLogout} href="#">{window.t('layout.logout')}</a>
-            </li>
-          </ul>
-        </li>
-    );
-  },
-
-  renderAnonymous() {
-    return (
-        <li>
-          <a onClick={this.handleLogin}>{window.t('layout.login')}</a>
-        </li>
-    );
-  },
-
-  handleLogin(e) {
-    e.preventDefault();
-    const returnTo = window.location.pathname + window.location.search;
-    const loginUrl = `${window.baseUrl}/sessions/new?return_to=${encodeURIComponent(returnTo)}${window.location.hash}`;
-    window.location = loginUrl;
-  },
-
-  handleLogout(e) {
-    e.preventDefault();
-    if (window.sonarRecentHistory) {
-      window.sonarRecentHistory.clear();
-    }
-    const logoutUrl = `${window.baseUrl}/sessions/logout`;
-    window.location = logoutUrl;
-  },
-
-  render() {
-    const isUserAuthenticated = !!window.SS.user;
-    return isUserAuthenticated ? this.renderAuthenticated() : this.renderAnonymous();
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/nav/global-nav.jsx b/server/sonar-web/src/main/js/apps/nav/global-nav.jsx
deleted file mode 100644 (file)
index b7d8782..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import GlobalNavBranding from './global-nav-branding';
-import GlobalNavMenu from './global-nav-menu';
-import GlobalNavUser from './global-nav-user';
-import GlobalNavSearch from './global-nav-search';
-import ShortcutsHelpView from './shortcuts-help-view';
-
-let $ = jQuery;
-
-export default React.createClass({
-  getInitialState() {
-    return this.props;
-  },
-
-  componentDidMount() {
-    this.loadGlobalNavDetails();
-  },
-
-  loadGlobalNavDetails() {
-    $.get(`${window.baseUrl}/api/navigation/global`).done(r => {
-      this.setState(r);
-    });
-  },
-
-  openHelp(e) {
-    e.preventDefault();
-    new ShortcutsHelpView().render();
-  },
-
-  render() {
-    return (
-        <div className="container">
-          <GlobalNavBranding {...this.state}/>
-
-          <GlobalNavMenu {...this.state}/>
-
-          <ul className="nav navbar-nav navbar-right">
-            <GlobalNavUser {...this.state}/>
-            <GlobalNavSearch {...this.state}/>
-            <li>
-              <a onClick={this.openHelp} href="#">
-                <i className="icon-help navbar-icon"/>
-              </a>
-            </li>
-          </ul>
-        </div>
-    );
-  }
-});
diff --git a/server/sonar-web/src/main/js/apps/nav/global/global-nav-branding.jsx b/server/sonar-web/src/main/js/apps/nav/global/global-nav-branding.jsx
new file mode 100644 (file)
index 0000000..dccd40f
--- /dev/null
@@ -0,0 +1,20 @@
+import React from 'react';
+
+export default React.createClass({
+  renderLogo() {
+    const url = this.props.logoUrl || `${window.baseUrl}/images/logo.svg`;
+    const width = this.props.logoWidth || 30;
+    const title = window.t('layout.sonar.slogan');
+    return <img src={url} width={width} height="30" alt={title} title={title}/>
+  },
+
+  render() {
+    const homeUrl = window.baseUrl + '/';
+    const homeLinkClassName = 'navbar-brand' + (this.props.logoUrl ? ' navbar-brand-custom' : '');
+    return (
+        <div className="navbar-header">
+          <a className={homeLinkClassName} href={homeUrl}>{this.renderLogo()}</a>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/global/global-nav-menu.jsx b/server/sonar-web/src/main/js/apps/nav/global/global-nav-menu.jsx
new file mode 100644 (file)
index 0000000..037b023
--- /dev/null
@@ -0,0 +1,155 @@
+import React from 'react';
+import DashboardNameMixin from '../dashboard-name-mixin';
+
+export default React.createClass({
+  mixins: [DashboardNameMixin],
+
+  getDefaultProps: function () {
+    return { globalDashboards: [], globalPages: [] };
+  },
+
+  activeLink(url) {
+    return window.location.pathname.indexOf(window.baseUrl + url) === 0 ? 'active' : null;
+  },
+
+  renderDashboardLink(dashboard) {
+    const url = `${window.baseUrl}/dashboard/index?did=${encodeURIComponent(dashboard.key)}`;
+    const name = this.getLocalizedDashboardName(dashboard.name);
+    return (
+        <li key={dashboard.name}>
+          <a href={url}>{name}</a>
+        </li>
+    );
+  },
+
+  renderDashboardsManagementLink() {
+    const url = `${window.baseUrl}/dashboards`;
+    return (
+        <li>
+          <a href={url}>{window.t('dashboard.manage_dashboards')}</a>
+        </li>
+    );
+  },
+
+  renderDashboards() {
+    const dashboards = this.props.globalDashboards.map(this.renderDashboardLink);
+    const canManageDashboards = !!window.SS.user;
+    return (
+        <li className="dropdown">
+          <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+            {window.t('layout.dashboards')}&nbsp;<span className="icon-dropdown"/>
+          </a>
+          <ul className="dropdown-menu">
+            {dashboards}
+            {canManageDashboards ? <li className="divider"/> : null}
+            {canManageDashboards ? this.renderDashboardsManagementLink() : null}
+          </ul>
+        </li>
+    );
+  },
+
+  renderIssuesLink() {
+    const url = `${window.baseUrl}/issues/search`;
+    return (
+        <li className={this.activeLink('/issues')}>
+          <a href={url}>{window.t('issues.page')}</a>
+        </li>
+    );
+  },
+
+  renderMeasuresLink() {
+    const url = `${window.baseUrl}/measures/search?qualifiers[]=TRK`;
+    return (
+        <li className={this.activeLink('/measures')}>
+          <a href={url}>{window.t('layout.measures')}</a>
+        </li>
+    );
+  },
+
+  renderRulesLink() {
+    const url = `${window.baseUrl}/coding_rules`;
+    return (
+        <li className={this.activeLink('/coding_rules')}>
+          <a href={url}>{window.t('coding_rules.page')}</a>
+        </li>
+    );
+  },
+
+  renderProfilesLink() {
+    const url = `${window.baseUrl}/profiles`;
+    return (
+        <li className={this.activeLink('/profiles')}>
+          <a href={url}>{window.t('quality_profiles.page')}</a>
+        </li>
+    );
+  },
+
+  renderQualityGatesLink() {
+    const url = `${window.baseUrl}/quality_gates`;
+    return (
+        <li className={this.activeLink('/quality_gates')}>
+          <a href={url}>{window.t('quality_gates.page')}</a>
+        </li>
+    );
+  },
+
+  renderAdministrationLink() {
+    if (!window.SS.isUserAdmin) {
+      return null;
+    }
+    const url = `${window.baseUrl}/settings`;
+    return (
+        <li className={this.activeLink('/settings')}>
+          <a className="navbar-admin-link" href={url}>{window.t('layout.settings')}</a>
+        </li>
+    );
+  },
+
+  renderComparisonLink() {
+    const url = `${window.baseUrl}/comparison`;
+    return (
+        <li className={this.activeLink('/comparison')}>
+          <a href={url}>{window.t('comparison_global.page')}</a>
+        </li>
+    );
+  },
+
+  renderGlobalPageLink(globalPage, index) {
+    const url = window.baseUrl + globalPage.url;
+    return (
+        <li key={index}>
+          <a href={url}>{globalPage.name}</a>
+        </li>
+    );
+  },
+
+  renderMore() {
+    const globalPages = this.props.globalPages.map(this.renderGlobalPageLink);
+    return (
+        <li className="dropdown">
+          <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+            {window.t('more')}&nbsp;<span className="icon-dropdown"/>
+          </a>
+          <ul className="dropdown-menu">
+            {this.renderComparisonLink()}
+            {globalPages}
+          </ul>
+        </li>
+    );
+  },
+
+  render() {
+    return (
+        <ul className="nav navbar-nav">
+          {this.renderDashboards()}
+          {this.renderIssuesLink()}
+          {this.renderMeasuresLink()}
+          {this.renderRulesLink()}
+          {this.renderProfilesLink()}
+          {this.renderQualityGatesLink()}
+          {this.renderAdministrationLink()}
+          {this.renderMore()}
+        </ul>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/global/global-nav-search.jsx b/server/sonar-web/src/main/js/apps/nav/global/global-nav-search.jsx
new file mode 100644 (file)
index 0000000..157ba7b
--- /dev/null
@@ -0,0 +1,79 @@
+import React from 'react';
+import SearchView from './search-view';
+
+let $ = jQuery;
+
+function contains (root, node) {
+  while (node) {
+    if (node === root) {
+      return true;
+    }
+    node = node.parentNode;
+  }
+  return false;
+}
+
+export default React.createClass({
+  getInitialState() {
+    return { open: false };
+  },
+
+  componentDidMount() {
+    key('s', () => {
+      this.openSearch();
+      return false;
+    });
+  },
+
+  componentWillUnmount() {
+    this.closeSearch();
+    key.unbind('s');
+  },
+
+  openSearch() {
+    window.addEventListener('click', this.onClickOutside);
+    this.setState({ open: true }, this.renderSearchView);
+  },
+
+  closeSearch() {
+    window.removeEventListener('click', this.onClickOutside);
+    this.resetSearchView();
+    this.setState({ open: false });
+  },
+
+  renderSearchView() {
+    let searchContainer = React.findDOMNode(this.refs.container);
+    this.searchView = new SearchView({
+      model: new Backbone.Model(this.props),
+      hide: this.closeSearch
+    });
+    this.searchView.render().$el.appendTo(searchContainer);
+  },
+
+  resetSearchView() {
+    this.searchView && this.searchView.destroy();
+  },
+
+  onClick(e) {
+    e.preventDefault();
+    this.state.open ? this.closeSearch() : this.openSearch();
+  },
+
+  onClickOutside(e) {
+    if (!contains(React.findDOMNode(this.refs.dropdown), e.target)) {
+      this.closeSearch();
+    }
+  },
+
+  render() {
+    const dropdownClassName = 'dropdown' + (this.state.open ? ' open' : '');
+    return (
+        <li ref="dropdown" className={dropdownClassName}>
+          <a className="navbar-search-dropdown" href="#" onClick={this.onClick}>
+            <i className="icon-search navbar-icon"/>&nbsp;<i className="icon-dropdown"/>
+          </a>
+          <div ref="container" className="dropdown-menu dropdown-menu-right"></div>
+        </li>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/global/global-nav-user.jsx b/server/sonar-web/src/main/js/apps/nav/global/global-nav-user.jsx
new file mode 100644 (file)
index 0000000..d07c9a4
--- /dev/null
@@ -0,0 +1,52 @@
+import React from 'react';
+import Avatar from 'components/shared/avatar';
+
+export default React.createClass({
+  renderAuthenticated() {
+    return (
+        <li className="dropdown">
+          <a className="dropdown-toggle" data-toggle="dropdown" href="#">
+            <Avatar email={window.SS.userEmail} size={20}/>&nbsp;
+            {window.SS.userName}&nbsp;<i className="icon-dropdown"/>
+          </a>
+          <ul className="dropdown-menu dropdown-menu-right">
+            <li>
+              <a href={`${window.baseUrl}/account/index`}>{window.t('layout.user_panel.my_profile')}</a>
+            </li>
+            <li>
+              <a onClick={this.handleLogout} href="#">{window.t('layout.logout')}</a>
+            </li>
+          </ul>
+        </li>
+    );
+  },
+
+  renderAnonymous() {
+    return (
+        <li>
+          <a onClick={this.handleLogin}>{window.t('layout.login')}</a>
+        </li>
+    );
+  },
+
+  handleLogin(e) {
+    e.preventDefault();
+    const returnTo = window.location.pathname + window.location.search;
+    const loginUrl = `${window.baseUrl}/sessions/new?return_to=${encodeURIComponent(returnTo)}${window.location.hash}`;
+    window.location = loginUrl;
+  },
+
+  handleLogout(e) {
+    e.preventDefault();
+    if (window.sonarRecentHistory) {
+      window.sonarRecentHistory.clear();
+    }
+    const logoutUrl = `${window.baseUrl}/sessions/logout`;
+    window.location = logoutUrl;
+  },
+
+  render() {
+    const isUserAuthenticated = !!window.SS.user;
+    return isUserAuthenticated ? this.renderAuthenticated() : this.renderAnonymous();
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/global/global-nav.jsx b/server/sonar-web/src/main/js/apps/nav/global/global-nav.jsx
new file mode 100644 (file)
index 0000000..b7d8782
--- /dev/null
@@ -0,0 +1,49 @@
+import React from 'react';
+import GlobalNavBranding from './global-nav-branding';
+import GlobalNavMenu from './global-nav-menu';
+import GlobalNavUser from './global-nav-user';
+import GlobalNavSearch from './global-nav-search';
+import ShortcutsHelpView from './shortcuts-help-view';
+
+let $ = jQuery;
+
+export default React.createClass({
+  getInitialState() {
+    return this.props;
+  },
+
+  componentDidMount() {
+    this.loadGlobalNavDetails();
+  },
+
+  loadGlobalNavDetails() {
+    $.get(`${window.baseUrl}/api/navigation/global`).done(r => {
+      this.setState(r);
+    });
+  },
+
+  openHelp(e) {
+    e.preventDefault();
+    new ShortcutsHelpView().render();
+  },
+
+  render() {
+    return (
+        <div className="container">
+          <GlobalNavBranding {...this.state}/>
+
+          <GlobalNavMenu {...this.state}/>
+
+          <ul className="nav navbar-nav navbar-right">
+            <GlobalNavUser {...this.state}/>
+            <GlobalNavSearch {...this.state}/>
+            <li>
+              <a onClick={this.openHelp} href="#">
+                <i className="icon-help navbar-icon"/>
+              </a>
+            </li>
+          </ul>
+        </div>
+    );
+  }
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/global/search-view.js b/server/sonar-web/src/main/js/apps/nav/global/search-view.js
new file mode 100644 (file)
index 0000000..233a1dc
--- /dev/null
@@ -0,0 +1,230 @@
+define([
+  'components/common/selectable-collection-view',
+  '../templates'
+], function (SelectableCollectionView) {
+
+  var $ = jQuery,
+
+      SearchItemView = Marionette.ItemView.extend({
+        tagName: 'li',
+        template: Templates['nav-search-item'],
+
+        select: function () {
+          this.$el.addClass('active');
+        },
+
+        deselect: function () {
+          this.$el.removeClass('active');
+        },
+
+        submit: function () {
+          this.$('a')[0].click();
+        },
+
+        serializeData: function () {
+          return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
+            index: this.options.index
+          });
+        }
+      }),
+
+      SearchEmptyView = Marionette.ItemView.extend({
+        tagName: 'li',
+        template: Templates['nav-search-empty']
+      }),
+
+      SearchResultsView = SelectableCollectionView.extend({
+        className: 'menu',
+        tagName: 'ul',
+        childView: SearchItemView,
+        emptyView: SearchEmptyView
+      });
+
+  return Marionette.LayoutView.extend({
+    className: 'navbar-search',
+    tagName: 'form',
+    template: Templates['nav-search'],
+
+    regions: {
+      resultsRegion: '.js-search-results'
+    },
+
+    events: {
+      'submit': 'onSubmit',
+      'keydown .js-search-input': 'onKeyDown',
+      'keyup .js-search-input': 'debouncedOnKeyUp'
+    },
+
+    initialize: function () {
+      var that = this;
+      this.results = new Backbone.Collection();
+      this.favorite = [];
+      if (window.SS.user) {
+        this.fetchFavorite().always(function () {
+          that.resetResultsToDefault();
+        });
+      } else {
+        this.resetResultsToDefault();
+      }
+      this.resultsView = new SearchResultsView({ collection: this.results });
+      this.debouncedOnKeyUp = _.debounce(this.onKeyUp, 400);
+      this._bufferedValue = '';
+    },
+
+    onRender: function () {
+      var that = this;
+      this.resultsRegion.show(this.resultsView);
+      setTimeout(function () {
+        that.$('.js-search-input').focus();
+      }, 0);
+    },
+
+    onKeyDown: function (e) {
+      if (e.keyCode === 38) {
+        this.resultsView.selectPrev();
+        return false;
+      }
+      if (e.keyCode === 40) {
+        this.resultsView.selectNext();
+        return false;
+      }
+      if (e.keyCode === 13) {
+        this.resultsView.submitCurrent();
+        return false;
+      }
+      if (e.keyCode === 27) {
+        this.options.hide();
+        return false;
+      }
+    },
+
+    onKeyUp: function () {
+      var value = this.$('.js-search-input').val();
+      if (value === this._bufferedValue) {
+        return;
+      }
+      this._bufferedValue = this.$('.js-search-input').val();
+      if (this.searchRequest != null) {
+        this.searchRequest.abort();
+      }
+      this.searchRequest = this.search(value);
+    },
+
+    onSubmit: function () {
+      return false;
+    },
+
+    fetchFavorite: function () {
+      var that = this;
+      return $.get(baseUrl + '/api/favourites').done(function (r) {
+        that.favorite = r.map(function (f) {
+          var isFile = ['FIL', 'UTS'].indexOf(f.qualifier) !== -1;
+          return {
+            url: baseUrl + '/dashboard/index?id=' + encodeURIComponent(f.key) + dashboardParameters(true),
+            name: isFile ? window.collapsedDirFromPath(f.lname) + window.fileFromPath(f.lname) : f.name,
+            icon: 'favorite'
+          };
+        });
+        that.favorite = _.sortBy(that.favorite, 'name');
+      });
+    },
+
+    resetResultsToDefault: function () {
+      var recentHistory = JSON.parse(localStorage.getItem('sonar_recent_history')),
+          history = (recentHistory || []).map(function (historyItem, index) {
+            return {
+              url: baseUrl + '/dashboard/index?id=' + encodeURIComponent(historyItem.key) + dashboardParameters(true),
+              name: historyItem.name,
+              q: historyItem.icon,
+              extra: index === 0 ? t('browsed_recently') : null
+            };
+          }),
+          favorite = _.first(this.favorite, 6).map(function (f, index) {
+            return _.extend(f, { extra: index === 0 ? t('favorite') : null });
+          }),
+          qualifiers = this.model.get('qualifiers').map(function (q, index) {
+            return {
+              url: baseUrl + '/all_projects?qualifier=' + encodeURIComponent(q),
+              name: t('qualifiers.all', q),
+              extra: index === 0 ? '' : null
+            };
+          });
+      this.results.reset([].concat(history, favorite, qualifiers));
+    },
+
+    search: function (q) {
+      if (q.length < 2) {
+        this.resetResultsToDefault();
+        return;
+      }
+      var that = this,
+          url = baseUrl + '/api/components/suggestions',
+          options = { s: q };
+      return $.get(url, options).done(function (r) {
+        var collection = [];
+        r.results.forEach(function (domain) {
+          domain.items.forEach(function (item, index) {
+            collection.push(_.extend(item, {
+              q: domain.q,
+              extra: index === 0 ? domain.name : null,
+              url: baseUrl + '/dashboard/index?id=' + encodeURIComponent(item.key) + dashboardParameters(true)
+            }));
+          });
+        });
+        that.results.reset([].concat(
+            that.getNavigationFindings(q),
+            that.getGlobalDashboardFindings(q),
+            that.getFavoriteFindings(q),
+            collection
+        ));
+      });
+    },
+
+    getNavigationFindings: function (q) {
+      var DEFAULT_ITEMS = [
+            { name: t('issues.page'), url: baseUrl + '/issues/search' },
+            { name: t('layout.measures'), url: baseUrl + '/measures/search?qualifiers[]=TRK' },
+            { name: t('coding_rules.page'), url: baseUrl + '/coding_rules' },
+            { name: t('quality_profiles.page'), url: baseUrl + '/profiles' },
+            { name: t('quality_gates.page'), url: baseUrl + '/quality_gates' },
+            { name: t('comparison_global.page'), url: baseUrl + '/comparison' }
+          ],
+          customItems = [];
+      if (window.SS.isUserAdmin) {
+        customItems.push({ name: t('layout.settings'), url: baseUrl + '/settings' });
+      }
+      var findings = [].concat(DEFAULT_ITEMS, customItems).filter(function (f) {
+        return f.name.match(new RegExp(q, 'i'));
+      });
+      if (findings.length > 0) {
+        findings[0].extra = t('navigation');
+      }
+      return _.first(findings, 6);
+    },
+
+    getGlobalDashboardFindings: function (q) {
+      var dashboards = this.model.get('globalDashboards') || [],
+          items = dashboards.map(function (d) {
+            return { name: d.name, url: baseUrl + '/dashboard/index?did=' + encodeURIComponent(d.key) };
+          });
+      var findings = items.filter(function (f) {
+        return f.name.match(new RegExp(q, 'i'));
+      });
+      if (findings.length > 0) {
+        findings[0].extra = t('dashboard.global_dashboards');
+      }
+      return _.first(findings, 6);
+    },
+
+    getFavoriteFindings: function (q) {
+      var findings = this.favorite.filter(function (f) {
+        return f.name.match(new RegExp(q, 'i'));
+      });
+      if (findings.length > 0) {
+        findings[0].extra = t('favorite');
+      }
+      return _.first(findings, 6);
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/global/shortcuts-help-view.js b/server/sonar-web/src/main/js/apps/nav/global/shortcuts-help-view.js
new file mode 100644 (file)
index 0000000..b016a73
--- /dev/null
@@ -0,0 +1,11 @@
+define([
+  'components/common/modals',
+  '../templates'
+], function (ModalView) {
+
+  return ModalView.extend({
+    className: 'modal modal-large',
+    template: Templates['nav-shortcuts-help']
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/nav/search-view.js b/server/sonar-web/src/main/js/apps/nav/search-view.js
deleted file mode 100644 (file)
index d66a265..0000000
+++ /dev/null
@@ -1,230 +0,0 @@
-define([
-  'components/common/selectable-collection-view',
-  './templates'
-], function (SelectableCollectionView) {
-
-  var $ = jQuery,
-
-      SearchItemView = Marionette.ItemView.extend({
-        tagName: 'li',
-        template: Templates['nav-search-item'],
-
-        select: function () {
-          this.$el.addClass('active');
-        },
-
-        deselect: function () {
-          this.$el.removeClass('active');
-        },
-
-        submit: function () {
-          this.$('a')[0].click();
-        },
-
-        serializeData: function () {
-          return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
-            index: this.options.index
-          });
-        }
-      }),
-
-      SearchEmptyView = Marionette.ItemView.extend({
-        tagName: 'li',
-        template: Templates['nav-search-empty']
-      }),
-
-      SearchResultsView = SelectableCollectionView.extend({
-        className: 'menu',
-        tagName: 'ul',
-        childView: SearchItemView,
-        emptyView: SearchEmptyView
-      });
-
-  return Marionette.LayoutView.extend({
-    className: 'navbar-search',
-    tagName: 'form',
-    template: Templates['nav-search'],
-
-    regions: {
-      resultsRegion: '.js-search-results'
-    },
-
-    events: {
-      'submit': 'onSubmit',
-      'keydown .js-search-input': 'onKeyDown',
-      'keyup .js-search-input': 'debouncedOnKeyUp'
-    },
-
-    initialize: function () {
-      var that = this;
-      this.results = new Backbone.Collection();
-      this.favorite = [];
-      if (window.SS.user) {
-        this.fetchFavorite().always(function () {
-          that.resetResultsToDefault();
-        });
-      } else {
-        this.resetResultsToDefault();
-      }
-      this.resultsView = new SearchResultsView({ collection: this.results });
-      this.debouncedOnKeyUp = _.debounce(this.onKeyUp, 400);
-      this._bufferedValue = '';
-    },
-
-    onRender: function () {
-      var that = this;
-      this.resultsRegion.show(this.resultsView);
-      setTimeout(function () {
-        that.$('.js-search-input').focus();
-      }, 0);
-    },
-
-    onKeyDown: function (e) {
-      if (e.keyCode === 38) {
-        this.resultsView.selectPrev();
-        return false;
-      }
-      if (e.keyCode === 40) {
-        this.resultsView.selectNext();
-        return false;
-      }
-      if (e.keyCode === 13) {
-        this.resultsView.submitCurrent();
-        return false;
-      }
-      if (e.keyCode === 27) {
-        this.options.hide();
-        return false;
-      }
-    },
-
-    onKeyUp: function () {
-      var value = this.$('.js-search-input').val();
-      if (value === this._bufferedValue) {
-        return;
-      }
-      this._bufferedValue = this.$('.js-search-input').val();
-      if (this.searchRequest != null) {
-        this.searchRequest.abort();
-      }
-      this.searchRequest = this.search(value);
-    },
-
-    onSubmit: function () {
-      return false;
-    },
-
-    fetchFavorite: function () {
-      var that = this;
-      return $.get(baseUrl + '/api/favourites').done(function (r) {
-        that.favorite = r.map(function (f) {
-          var isFile = ['FIL', 'UTS'].indexOf(f.qualifier) !== -1;
-          return {
-            url: baseUrl + '/dashboard/index?id=' + encodeURIComponent(f.key) + dashboardParameters(true),
-            name: isFile ? window.collapsedDirFromPath(f.lname) + window.fileFromPath(f.lname) : f.name,
-            icon: 'favorite'
-          };
-        });
-        that.favorite = _.sortBy(that.favorite, 'name');
-      });
-    },
-
-    resetResultsToDefault: function () {
-      var recentHistory = JSON.parse(localStorage.getItem('sonar_recent_history')),
-          history = (recentHistory || []).map(function (historyItem, index) {
-            return {
-              url: baseUrl + '/dashboard/index?id=' + encodeURIComponent(historyItem.key) + dashboardParameters(true),
-              name: historyItem.name,
-              q: historyItem.icon,
-              extra: index === 0 ? t('browsed_recently') : null
-            };
-          }),
-          favorite = _.first(this.favorite, 6).map(function (f, index) {
-            return _.extend(f, { extra: index === 0 ? t('favorite') : null });
-          }),
-          qualifiers = this.model.get('qualifiers').map(function (q, index) {
-            return {
-              url: baseUrl + '/all_projects?qualifier=' + encodeURIComponent(q),
-              name: t('qualifiers.all', q),
-              extra: index === 0 ? '' : null
-            };
-          });
-      this.results.reset([].concat(history, favorite, qualifiers));
-    },
-
-    search: function (q) {
-      if (q.length < 2) {
-        this.resetResultsToDefault();
-        return;
-      }
-      var that = this,
-          url = baseUrl + '/api/components/suggestions',
-          options = { s: q };
-      return $.get(url, options).done(function (r) {
-        var collection = [];
-        r.results.forEach(function (domain) {
-          domain.items.forEach(function (item, index) {
-            collection.push(_.extend(item, {
-              q: domain.q,
-              extra: index === 0 ? domain.name : null,
-              url: baseUrl + '/dashboard/index?id=' + encodeURIComponent(item.key) + dashboardParameters(true)
-            }));
-          });
-        });
-        that.results.reset([].concat(
-            that.getNavigationFindings(q),
-            that.getGlobalDashboardFindings(q),
-            that.getFavoriteFindings(q),
-            collection
-        ));
-      });
-    },
-
-    getNavigationFindings: function (q) {
-      var DEFAULT_ITEMS = [
-            { name: t('issues.page'), url: baseUrl + '/issues/search' },
-            { name: t('layout.measures'), url: baseUrl + '/measures/search?qualifiers[]=TRK' },
-            { name: t('coding_rules.page'), url: baseUrl + '/coding_rules' },
-            { name: t('quality_profiles.page'), url: baseUrl + '/profiles' },
-            { name: t('quality_gates.page'), url: baseUrl + '/quality_gates' },
-            { name: t('comparison_global.page'), url: baseUrl + '/comparison' }
-          ],
-          customItems = [];
-      if (window.SS.isUserAdmin) {
-        customItems.push({ name: t('layout.settings'), url: baseUrl + '/settings' });
-      }
-      var findings = [].concat(DEFAULT_ITEMS, customItems).filter(function (f) {
-        return f.name.match(new RegExp(q, 'i'));
-      });
-      if (findings.length > 0) {
-        findings[0].extra = t('navigation');
-      }
-      return _.first(findings, 6);
-    },
-
-    getGlobalDashboardFindings: function (q) {
-      var dashboards = this.model.get('globalDashboards') || [],
-          items = dashboards.map(function (d) {
-            return { name: d.name, url: baseUrl + '/dashboard/index?did=' + encodeURIComponent(d.key) };
-          });
-      var findings = items.filter(function (f) {
-        return f.name.match(new RegExp(q, 'i'));
-      });
-      if (findings.length > 0) {
-        findings[0].extra = t('dashboard.global_dashboards');
-      }
-      return _.first(findings, 6);
-    },
-
-    getFavoriteFindings: function (q) {
-      var findings = this.favorite.filter(function (f) {
-        return f.name.match(new RegExp(q, 'i'));
-      });
-      if (findings.length > 0) {
-        findings[0].extra = t('favorite');
-      }
-      return _.first(findings, 6);
-    }
-  });
-
-});
diff --git a/server/sonar-web/src/main/js/apps/nav/shortcuts-help-view.js b/server/sonar-web/src/main/js/apps/nav/shortcuts-help-view.js
deleted file mode 100644 (file)
index 3902461..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-define([
-  'components/common/modals',
-  './templates'
-], function (ModalView) {
-
-  return ModalView.extend({
-    className: 'modal modal-large',
-    template: Templates['nav-shortcuts-help']
-  });
-
-});
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 (file)
index 0000000..09601d3
--- /dev/null
@@ -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 (file)
index 0000000..e0f6e5a
--- /dev/null
@@ -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}/>;
+  }
+});
index f0d20455d5e10b9d30ba7f9d77ee70e6ed2b9908..19d7e0cbaae0242d9ef1183c8017d67b54cd7868 100644 (file)
@@ -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;
index 74b2a6bd9ef8f2eca16da5a573aea52ec976cdac..ee712c33f1845fa4689b093f1aadb5ab008cc1f6 100644 (file)
@@ -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 (file)
index 0000000..4b2a5fe
--- /dev/null
@@ -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);
+    });
+  });
+});