From: Stas Vilchik Date: Thu, 20 Aug 2015 09:38:54 +0000 (+0200) Subject: rewrite component navigation X-Git-Tag: 5.2-RC1~709 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=bdbaf099bbc788427899504472319cd8c42e6085;p=sonarqube.git rewrite component navigation --- 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(, el); + }, + + renderComponentNav(options) { + const el = document.getElementById('context-navigation'); + React.render(, 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 ( +
  • + +  {item.name} + +
  • + ); + }); + return ( +
      {items}
    + ); + } +}); 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 ( +
    + +
    + ); + } +}); 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 ( +
  • + {title} +
  • + ); + }, + + 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 ( +
  • + + {window.t('layout.settings')}  +
      + {this.renderSettingsLink()} + {this.renderProfilesLink()} + {this.renderQualityGatesLink()} + {this.renderCustomMeasuresLink()} + {this.renderActionPlansLink()} + {this.renderLinksLink()} + {this.renderPermissionsLink()} + {this.renderHistoryLink()} + {this.renderUpdateKeyLink()} + {this.renderDeletionLink()} + {this.renderExtensions()} +
    +
  • + ); + }, + + 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 ( +
  • + + {window.t('more')}  + +
      + {this.renderDashboards()} + {this.renderDashboardManagementLink()} + {this.renderTools()} +
    +
  • + ); + }, + + 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 [
  • {window.t('layout.dashboards')}
  • ].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 [ +
  • , + this.renderLink(url, name, '/dashboards') + ]; + }, + + renderTools() { + let component = this.props.component; + if (!component.isComparable && !_.size(component.extensions)) { + return null; + } + let tools = [ +
  • , +
  • Tools
  • + ]; + 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 ( +
      + {this.renderOverviewLink()} + {this.renderComponentsLink()} + {this.renderComponentIssuesLink()} + {this.renderAdministration()} + {this.renderMore()} +
    + ); + } +}); 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 ( +
    + {version} {snapshotDate} +
    + ); + } +}); 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 ( +
    + + + + + + + +
    + ); + } +}); 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-nav-branding.jsx deleted file mode 100644 index dccd40f4a36..00000000000 --- a/server/sonar-web/src/main/js/apps/nav/global-nav-branding.jsx +++ /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 {title} - }, - - render() { - const homeUrl = window.baseUrl + '/'; - const homeLinkClassName = 'navbar-brand' + (this.props.logoUrl ? ' navbar-brand-custom' : ''); - return ( -
    - {this.renderLogo()} -
    - ); - } -}); 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 index 9aeb24baa77..00000000000 --- a/server/sonar-web/src/main/js/apps/nav/global-nav-menu.jsx +++ /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 ( -
  • - {name} -
  • - ); - }, - - renderDashboardsManagementLink() { - const url = `${window.baseUrl}/dashboards`; - return ( -
  • - {window.t('dashboard.manage_dashboards')} -
  • - ); - }, - - renderDashboards() { - const dashboards = this.props.globalDashboards.map(this.renderDashboardLink); - const canManageDashboards = !!window.SS.user; - return ( -
  • - - {window.t('layout.dashboards')}  - -
      - {dashboards} - {canManageDashboards ?
    • : null} - {canManageDashboards ? this.renderDashboardsManagementLink() : null} -
    -
  • - ); - }, - - renderIssuesLink() { - const url = `${window.baseUrl}/issues/search`; - return ( -
  • - {window.t('issues.page')} -
  • - ); - }, - - renderMeasuresLink() { - const url = `${window.baseUrl}/measures/search?qualifiers[]=TRK`; - return ( -
  • - {window.t('layout.measures')} -
  • - ); - }, - - renderRulesLink() { - const url = `${window.baseUrl}/coding_rules`; - return ( -
  • - {window.t('coding_rules.page')} -
  • - ); - }, - - renderProfilesLink() { - const url = `${window.baseUrl}/profiles`; - return ( -
  • - {window.t('quality_profiles.page')} -
  • - ); - }, - - renderQualityGatesLink() { - const url = `${window.baseUrl}/quality_gates`; - return ( -
  • - {window.t('quality_gates.page')} -
  • - ); - }, - - renderAdministrationLink() { - if (!window.SS.isUserAdmin) { - return null; - } - const url = `${window.baseUrl}/settings`; - return ( -
  • - {window.t('layout.settings')} -
  • - ); - }, - - renderComparisonLink() { - const url = `${window.baseUrl}/comparison`; - return ( -
  • - {window.t('comparison_global.page')} -
  • - ); - }, - - renderGlobalPageLink(globalPage, index) { - const url = window.baseUrl + globalPage.url; - return ( -
  • - {globalPage.name} -
  • - ); - }, - - renderMore() { - const globalPages = this.props.globalPages.map(this.renderGlobalPageLink); - return ( -
  • - - {window.t('more')}  - -
      - {this.renderComparisonLink()} - {globalPages} -
    -
  • - ); - }, - - render() { - return ( -
      - {this.renderDashboards()} - {this.renderIssuesLink()} - {this.renderMeasuresLink()} - {this.renderRulesLink()} - {this.renderProfilesLink()} - {this.renderQualityGatesLink()} - {this.renderAdministrationLink()} - {this.renderMore()} -
    - ); - } -}); 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 index 157ba7b25f7..00000000000 --- a/server/sonar-web/src/main/js/apps/nav/global-nav-search.jsx +++ /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 ( -
  • - -   - -
    -
  • - ); - } -}); 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 index d07c9a4d201..00000000000 --- a/server/sonar-web/src/main/js/apps/nav/global-nav-user.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import Avatar from 'components/shared/avatar'; - -export default React.createClass({ - renderAuthenticated() { - return ( -
  • - -   - {window.SS.userName}  - - -
  • - ); - }, - - renderAnonymous() { - return ( -
  • - {window.t('layout.login')} -
  • - ); - }, - - 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 index b7d8782268a..00000000000 --- a/server/sonar-web/src/main/js/apps/nav/global-nav.jsx +++ /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 ( -
    - - - - - -
    - ); - } -}); 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 index 00000000000..dccd40f4a36 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/nav/global/global-nav-branding.jsx @@ -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 {title} + }, + + render() { + const homeUrl = window.baseUrl + '/'; + const homeLinkClassName = 'navbar-brand' + (this.props.logoUrl ? ' navbar-brand-custom' : ''); + return ( + + ); + } +}); 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 index 00000000000..037b0231210 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/nav/global/global-nav-menu.jsx @@ -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 ( +
  • + {name} +
  • + ); + }, + + renderDashboardsManagementLink() { + const url = `${window.baseUrl}/dashboards`; + return ( +
  • + {window.t('dashboard.manage_dashboards')} +
  • + ); + }, + + renderDashboards() { + const dashboards = this.props.globalDashboards.map(this.renderDashboardLink); + const canManageDashboards = !!window.SS.user; + return ( +
  • + + {window.t('layout.dashboards')}  + +
      + {dashboards} + {canManageDashboards ?
    • : null} + {canManageDashboards ? this.renderDashboardsManagementLink() : null} +
    +
  • + ); + }, + + renderIssuesLink() { + const url = `${window.baseUrl}/issues/search`; + return ( +
  • + {window.t('issues.page')} +
  • + ); + }, + + renderMeasuresLink() { + const url = `${window.baseUrl}/measures/search?qualifiers[]=TRK`; + return ( +
  • + {window.t('layout.measures')} +
  • + ); + }, + + renderRulesLink() { + const url = `${window.baseUrl}/coding_rules`; + return ( +
  • + {window.t('coding_rules.page')} +
  • + ); + }, + + renderProfilesLink() { + const url = `${window.baseUrl}/profiles`; + return ( +
  • + {window.t('quality_profiles.page')} +
  • + ); + }, + + renderQualityGatesLink() { + const url = `${window.baseUrl}/quality_gates`; + return ( +
  • + {window.t('quality_gates.page')} +
  • + ); + }, + + renderAdministrationLink() { + if (!window.SS.isUserAdmin) { + return null; + } + const url = `${window.baseUrl}/settings`; + return ( +
  • + {window.t('layout.settings')} +
  • + ); + }, + + renderComparisonLink() { + const url = `${window.baseUrl}/comparison`; + return ( +
  • + {window.t('comparison_global.page')} +
  • + ); + }, + + renderGlobalPageLink(globalPage, index) { + const url = window.baseUrl + globalPage.url; + return ( +
  • + {globalPage.name} +
  • + ); + }, + + renderMore() { + const globalPages = this.props.globalPages.map(this.renderGlobalPageLink); + return ( +
  • + + {window.t('more')}  + +
      + {this.renderComparisonLink()} + {globalPages} +
    +
  • + ); + }, + + render() { + return ( +
      + {this.renderDashboards()} + {this.renderIssuesLink()} + {this.renderMeasuresLink()} + {this.renderRulesLink()} + {this.renderProfilesLink()} + {this.renderQualityGatesLink()} + {this.renderAdministrationLink()} + {this.renderMore()} +
    + ); + } +}); 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 index 00000000000..157ba7b25f7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/nav/global/global-nav-search.jsx @@ -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 ( +
  • + +   + +
    +
  • + ); + } +}); 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 index 00000000000..d07c9a4d201 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/nav/global/global-nav-user.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import Avatar from 'components/shared/avatar'; + +export default React.createClass({ + renderAuthenticated() { + return ( +
  • + +   + {window.SS.userName}  + + +
  • + ); + }, + + renderAnonymous() { + return ( +
  • + {window.t('layout.login')} +
  • + ); + }, + + 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 index 00000000000..b7d8782268a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/nav/global/global-nav.jsx @@ -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 ( +
    + + + + + +
    + ); + } +}); 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 index 00000000000..233a1dc5f29 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/nav/global/search-view.js @@ -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 index 00000000000..b016a734d8c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/nav/global/shortcuts-help-view.js @@ -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 index d66a26508ff..00000000000 --- a/server/sonar-web/src/main/js/apps/nav/search-view.js +++ /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 index 39024617f7a..00000000000 --- a/server/sonar-web/src/main/js/apps/nav/shortcuts-help-view.js +++ /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 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 ( + + + + ) + }, + + render() { + const className = this.state.favorite ? 'icon-star icon-star-favorite' : 'icon-star'; + return {this.renderSVG()}; + } +}); 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 ; + } +}); 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, ''); + }); + + 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); + }); + }); +});