瀏覽代碼

SONAR-10187 Provide more options to populate empty "My Projects" page

tags/7.0-RC1
Stas Vilchik 6 年之前
父節點
當前提交
82d363f3bc
共有 42 個檔案被更改,包括 1082 行新增805 行删除
  1. 78
    0
      server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
  2. 0
    83
      server/sonar-web/src/main/js/app/components/GlobalFooter.js
  3. 97
    0
      server/sonar-web/src/main/js/app/components/GlobalFooter.tsx
  4. 7
    1
      server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx
  5. 21
    15
      server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.tsx
  6. 1
    1
      server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx
  7. 1
    1
      server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.tsx
  8. 0
    173
      server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.js.snap
  9. 242
    0
      server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap
  10. 0
    57
      server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.js.snap
  11. 78
    0
      server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.tsx.snap
  12. 30
    0
      server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts
  13. 42
    40
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
  14. 14
    22
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
  15. 45
    103
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
  16. 3
    3
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
  17. 0
    2
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
  18. 27
    100
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
  19. 28
    0
      server/sonar-web/src/main/js/app/components/search/Search.d.ts
  20. 11
    9
      server/sonar-web/src/main/js/app/styles/components/page.css
  21. 5
    1
      server/sonar-web/src/main/js/app/styles/init/forms.css
  22. 10
    0
      server/sonar-web/src/main/js/app/types.ts
  23. 21
    38
      server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.tsx
  24. 7
    10
      server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx
  25. 1
    1
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
  26. 2
    13
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
  27. 3
    1
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
  28. 18
    69
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
  29. 81
    13
      server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
  30. 13
    2
      server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
  31. 54
    15
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
  32. 1
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap
  33. 5
    0
      server/sonar-web/src/main/js/apps/projects/styles.css
  34. 26
    0
      server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts
  35. 3
    3
      server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx
  36. 1
    0
      server/sonar-web/src/main/js/components/nav/NavBar.tsx
  37. 19
    16
      server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx
  38. 38
    0
      server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx
  39. 41
    0
      server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap
  40. 3
    10
      server/sonar-web/src/main/js/store/appState/duck.ts
  41. 1
    1
      server/sonar-web/src/main/js/store/organizations/duck.js
  42. 4
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 78
- 0
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx 查看文件

@@ -0,0 +1,78 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import GlobalNav from './nav/global/GlobalNav';
import GlobalFooterContainer from './GlobalFooterContainer';
import GlobalMessagesContainer from './GlobalMessagesContainer';

interface Props {
children: React.ReactNode;
location: { pathname: string };
}

interface State {
isOnboardingTutorialOpen: boolean;
}

export default class GlobalContainer extends React.PureComponent<Props, State> {
static childContextTypes = {
closeOnboardingTutorial: PropTypes.func,
openOnboardingTutorial: PropTypes.func
};

constructor(props: Props) {
super(props);
this.state = { isOnboardingTutorialOpen: false };
}

getChildContext() {
return {
closeOnboardingTutorial: this.closeOnboardingTutorial,
openOnboardingTutorial: this.openOnboardingTutorial
};
}

openOnboardingTutorial = () => this.setState({ isOnboardingTutorialOpen: true });

closeOnboardingTutorial = () => this.setState({ isOnboardingTutorialOpen: false });

render() {
// it is important to pass `location` down to `GlobalNav` to trigger render on url change

return (
<div className="global-container">
<div className="page-wrapper" id="container">
<div className="page-container">
<GlobalNav
closeOnboardingTutorial={this.closeOnboardingTutorial}
isOnboardingTutorialOpen={this.state.isOnboardingTutorialOpen}
location={this.props.location}
openOnboardingTutorial={this.openOnboardingTutorial}
/>
<GlobalMessagesContainer />
{this.props.children}
</div>
</div>
<GlobalFooterContainer />
</div>
);
}
}

+ 0
- 83
server/sonar-web/src/main/js/app/components/GlobalFooter.js 查看文件

@@ -1,83 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { Link } from 'react-router';
import GlobalFooterSonarCloud from './GlobalFooterSonarCloud';
import GlobalFooterBranding from './GlobalFooterBranding';
import { translate, translateWithParameters } from '../../helpers/l10n';

/*::
type Props = {
hideLoggedInInfo?: boolean,
productionDatabase: boolean,
onSonarCloud?: { value: string },
sonarqubeVersion?: string
};
*/

export default function GlobalFooter(
{ hideLoggedInInfo, productionDatabase, onSonarCloud, sonarqubeVersion } /*: Props */
) {
if (onSonarCloud && onSonarCloud.value === 'true') {
return <GlobalFooterSonarCloud hideLoggedInInfo={hideLoggedInInfo} />;
}

return (
<div id="footer" className="page-footer page-container">
{productionDatabase === false && (
<div className="alert alert-danger">
<p className="big" id="evaluation_warning">
{translate('footer.production_database_warning')}
</p>
<p>{translate('footer.production_database_explanation')}</p>
</div>
)}

<GlobalFooterBranding />

<div>
{!hideLoggedInInfo &&
sonarqubeVersion &&
translateWithParameters('footer.version_x', sonarqubeVersion)}
{!hideLoggedInInfo && sonarqubeVersion && ' - '}
<a href="http://www.gnu.org/licenses/lgpl-3.0.txt">{translate('footer.licence')}</a>
{' - '}
<a href="http://www.sonarqube.org">{translate('footer.community')}</a>
{' - '}
<a href="https://redirect.sonarsource.com/doc/home.html">
{translate('footer.documentation')}
</a>
{' - '}
<a href="https://redirect.sonarsource.com/doc/community.html">
{translate('footer.support')}
</a>
{' - '}
<a href="https://redirect.sonarsource.com/doc/plugin-library.html">
{translate('footer.plugins')}
</a>
{!hideLoggedInInfo && ' - '}
{!hideLoggedInInfo && <Link to="/web_api">{translate('footer.web_api')}</Link>}
{!hideLoggedInInfo && ' - '}
{!hideLoggedInInfo && <Link to="/about">{translate('footer.about')}</Link>}
</div>
</div>
);
}

+ 97
- 0
server/sonar-web/src/main/js/app/components/GlobalFooter.tsx 查看文件

@@ -0,0 +1,97 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Link } from 'react-router';
import GlobalFooterSonarCloud from './GlobalFooterSonarCloud';
import GlobalFooterBranding from './GlobalFooterBranding';
import { translate, translateWithParameters } from '../../helpers/l10n';

interface Props {
hideLoggedInInfo?: boolean;
productionDatabase: boolean;
onSonarCloud?: { value: string };
sonarqubeVersion?: string;
}

export default function GlobalFooter({
hideLoggedInInfo,
productionDatabase,
onSonarCloud,
sonarqubeVersion
}: Props) {
if (onSonarCloud && onSonarCloud.value === 'true') {
return <GlobalFooterSonarCloud />;
}

return (
<div id="footer" className="page-footer page-container">
{productionDatabase === false && (
<div className="alert alert-danger">
<p className="big" id="evaluation_warning">
{translate('footer.production_database_warning')}
</p>
<p>{translate('footer.production_database_explanation')}</p>
</div>
)}

<GlobalFooterBranding />

<ul className="page-footer-menu">
{!hideLoggedInInfo &&
sonarqubeVersion && (
<li className="page-footer-menu-item">
{translateWithParameters('footer.version_x', sonarqubeVersion)}
</li>
)}
<li className="page-footer-menu-item">
<a href="http://www.gnu.org/licenses/lgpl-3.0.txt">{translate('footer.license')}</a>
</li>
<li className="page-footer-menu-item">
<a href="http://www.sonarqube.org">{translate('footer.community')}</a>
</li>
<li className="page-footer-menu-item">
<a href="https://redirect.sonarsource.com/doc/home.html">
{translate('footer.documentation')}
</a>
</li>
<li className="page-footer-menu-item">
<a href="https://redirect.sonarsource.com/doc/community.html">
{translate('footer.support')}
</a>
</li>
<li className="page-footer-menu-item">
<a href="https://redirect.sonarsource.com/doc/plugin-library.html">
{translate('footer.plugins')}
</a>
</li>
{!hideLoggedInInfo && (
<li className="page-footer-menu-item">
<Link to="/web_api">{translate('footer.web_api')}</Link>
</li>
)}
{!hideLoggedInInfo && (
<li className="page-footer-menu-item">
<Link to="/about">{translate('footer.about')}</Link>
</li>
)}
</ul>
</div>
);
}

+ 7
- 1
server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx 查看文件

@@ -21,7 +21,13 @@ import { connect } from 'react-redux';
import { getAppState, getGlobalSettingValue } from '../../store/rootReducer';
import GlobalFooter from './GlobalFooter';

const mapStateToProps = (state: any) => ({
interface StateProps {
onSonarCloud?: { value: string };
productionDatabase: boolean;
sonarqubeVersion?: string;
}

const mapStateToProps = (state: any): StateProps => ({
sonarqubeVersion: getAppState(state).version,
productionDatabase: getAppState(state).productionDatabase,
onSonarCloud: getGlobalSettingValue(state, 'sonar.sonarcloud.enabled')

server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.js → server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.tsx 查看文件

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { translate } from '../../helpers/l10n';

export default function GlobalFooterSonarCloud() {
@@ -32,19 +31,26 @@ export default function GlobalFooterSonarCloud() {
. All rights reserved.
</div>

<div>
<a href="https://about.sonarcloud.io/news/">{translate('footer.news')}</a>
{' - '}
<a href="https://about.sonarcloud.io/terms.pdf">{translate('footer.terms')}</a>
{' - '}
<a href="https://twitter.com/sonarqube">{translate('footer.twitter')}</a>
{' - '}
<a href="https://about.sonarcloud.io/get-started/">{translate('footer.get_started')}</a>
{' - '}
<a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a>
{' - '}
<a href="https://about.sonarcloud.io/">{translate('footer.about')}</a>
</div>
<ul className="page-footer-menu">
<li className="page-footer-menu-item">
<a href="https://about.sonarcloud.io/news/">{translate('footer.news')}</a>
</li>
<li className="page-footer-menu-item">
<a href="https://about.sonarcloud.io/terms.pdf">{translate('footer.terms')}</a>
</li>
<li className="page-footer-menu-item">
<a href="https://twitter.com/sonarqube">{translate('footer.twitter')}</a>
</li>
<li className="page-footer-menu-item">
<a href="https://about.sonarcloud.io/get-started/">{translate('footer.get_started')}</a>
</li>
<li className="page-footer-menu-item">
<a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a>
</li>
<li className="page-footer-menu-item">
<a href="https://about.sonarcloud.io/">{translate('footer.about')}</a>
</li>
</ul>
</div>
);
}

server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.js → server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx 查看文件

@@ -17,8 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import React from 'react';
import GlobalFooter from '../GlobalFooter';

it('should render the only logged in information', () => {

server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.js → server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.tsx 查看文件

@@ -17,8 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import React from 'react';
import GlobalFooterSonarCloud from '../GlobalFooterSonarCloud';

it('should render correctly', () => {

+ 0
- 173
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.js.snap 查看文件

@@ -1,173 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display the sq version 1`] = `
<div
className="page-footer page-container"
id="footer"
>
<GlobalFooterBranding />
<div>
footer.version_x.6.4-SNAPSHOT
-
<a
href="http://www.gnu.org/licenses/lgpl-3.0.txt"
>
footer.licence
</a>
-
<a
href="http://www.sonarqube.org"
>
footer.community
</a>
-
<a
href="https://redirect.sonarsource.com/doc/home.html"
>
footer.documentation
</a>
-
<a
href="https://redirect.sonarsource.com/doc/community.html"
>
footer.support
</a>
-
<a
href="https://redirect.sonarsource.com/doc/plugin-library.html"
>
footer.plugins
</a>
-
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/web_api"
>
footer.web_api
</Link>
-
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/about"
>
footer.about
</Link>
</div>
</div>
`;

exports[`should not render the only logged in information 1`] = `
<div
className="page-footer page-container"
id="footer"
>
<GlobalFooterBranding />
<div>
<a
href="http://www.gnu.org/licenses/lgpl-3.0.txt"
>
footer.licence
</a>
-
<a
href="http://www.sonarqube.org"
>
footer.community
</a>
-
<a
href="https://redirect.sonarsource.com/doc/home.html"
>
footer.documentation
</a>
-
<a
href="https://redirect.sonarsource.com/doc/community.html"
>
footer.support
</a>
-
<a
href="https://redirect.sonarsource.com/doc/plugin-library.html"
>
footer.plugins
</a>
</div>
</div>
`;

exports[`should render SonarCloud footer 1`] = `<GlobalFooterSonarCloud />`;

exports[`should render the only logged in information 1`] = `
<div
className="page-footer page-container"
id="footer"
>
<GlobalFooterBranding />
<div>
<a
href="http://www.gnu.org/licenses/lgpl-3.0.txt"
>
footer.licence
</a>
-
<a
href="http://www.sonarqube.org"
>
footer.community
</a>
-
<a
href="https://redirect.sonarsource.com/doc/home.html"
>
footer.documentation
</a>
-
<a
href="https://redirect.sonarsource.com/doc/community.html"
>
footer.support
</a>
-
<a
href="https://redirect.sonarsource.com/doc/plugin-library.html"
>
footer.plugins
</a>
-
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/web_api"
>
footer.web_api
</Link>
-
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/about"
>
footer.about
</Link>
</div>
</div>
`;

exports[`should show the db warning message 1`] = `
<div
className="alert alert-danger"
>
<p
className="big"
id="evaluation_warning"
>
footer.production_database_warning
</p>
<p>
footer.production_database_explanation
</p>
</div>
`;

+ 242
- 0
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap 查看文件

@@ -0,0 +1,242 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display the sq version 1`] = `
<div
className="page-footer page-container"
id="footer"
>
<GlobalFooterBranding />
<ul
className="page-footer-menu"
>
<li
className="page-footer-menu-item"
>
footer.version_x.6.4-SNAPSHOT
</li>
<li
className="page-footer-menu-item"
>
<a
href="http://www.gnu.org/licenses/lgpl-3.0.txt"
>
footer.license
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="http://www.sonarqube.org"
>
footer.community
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://redirect.sonarsource.com/doc/home.html"
>
footer.documentation
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://redirect.sonarsource.com/doc/community.html"
>
footer.support
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://redirect.sonarsource.com/doc/plugin-library.html"
>
footer.plugins
</a>
</li>
<li
className="page-footer-menu-item"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/web_api"
>
footer.web_api
</Link>
</li>
<li
className="page-footer-menu-item"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/about"
>
footer.about
</Link>
</li>
</ul>
</div>
`;

exports[`should not render the only logged in information 1`] = `
<div
className="page-footer page-container"
id="footer"
>
<GlobalFooterBranding />
<ul
className="page-footer-menu"
>
<li
className="page-footer-menu-item"
>
<a
href="http://www.gnu.org/licenses/lgpl-3.0.txt"
>
footer.license
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="http://www.sonarqube.org"
>
footer.community
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://redirect.sonarsource.com/doc/home.html"
>
footer.documentation
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://redirect.sonarsource.com/doc/community.html"
>
footer.support
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://redirect.sonarsource.com/doc/plugin-library.html"
>
footer.plugins
</a>
</li>
</ul>
</div>
`;

exports[`should render SonarCloud footer 1`] = `<GlobalFooterSonarCloud />`;

exports[`should render the only logged in information 1`] = `
<div
className="page-footer page-container"
id="footer"
>
<GlobalFooterBranding />
<ul
className="page-footer-menu"
>
<li
className="page-footer-menu-item"
>
<a
href="http://www.gnu.org/licenses/lgpl-3.0.txt"
>
footer.license
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="http://www.sonarqube.org"
>
footer.community
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://redirect.sonarsource.com/doc/home.html"
>
footer.documentation
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://redirect.sonarsource.com/doc/community.html"
>
footer.support
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://redirect.sonarsource.com/doc/plugin-library.html"
>
footer.plugins
</a>
</li>
<li
className="page-footer-menu-item"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/web_api"
>
footer.web_api
</Link>
</li>
<li
className="page-footer-menu-item"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/about"
>
footer.about
</Link>
</li>
</ul>
</div>
`;

exports[`should show the db warning message 1`] = `
<div
className="alert alert-danger"
>
<p
className="big"
id="evaluation_warning"
>
footer.production_database_warning
</p>
<p>
footer.production_database_explanation
</p>
</div>
`;

+ 0
- 57
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.js.snap 查看文件

@@ -1,57 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<div
className="page-footer page-container"
id="footer"
>
<div>
© 2008-2017, SonarCloud.io by
<a
href="http://www.sonarsource.com"
title="SonarSource SA"
>
SonarSource SA
</a>
. All rights reserved.
</div>
<div>
<a
href="https://about.sonarcloud.io/news/"
>
footer.news
</a>
-
<a
href="https://about.sonarcloud.io/terms.pdf"
>
footer.terms
</a>
-
<a
href="https://twitter.com/sonarqube"
>
footer.twitter
</a>
-
<a
href="https://about.sonarcloud.io/get-started/"
>
footer.get_started
</a>
-
<a
href="https://about.sonarcloud.io/contact/"
>
footer.help
</a>
-
<a
href="https://about.sonarcloud.io/"
>
footer.about
</a>
</div>
</div>
`;

+ 78
- 0
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.tsx.snap 查看文件

@@ -0,0 +1,78 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<div
className="page-footer page-container"
id="footer"
>
<div>
© 2008-2017, SonarCloud.io by
<a
href="http://www.sonarsource.com"
title="SonarSource SA"
>
SonarSource SA
</a>
. All rights reserved.
</div>
<ul
className="page-footer-menu"
>
<li
className="page-footer-menu-item"
>
<a
href="https://about.sonarcloud.io/news/"
>
footer.news
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://about.sonarcloud.io/terms.pdf"
>
footer.terms
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://twitter.com/sonarqube"
>
footer.twitter
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://about.sonarcloud.io/get-started/"
>
footer.get_started
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://about.sonarcloud.io/contact/"
>
footer.help
</a>
</li>
<li
className="page-footer-menu-item"
>
<a
href="https://about.sonarcloud.io/"
>
footer.about
</a>
</li>
</ul>
</div>
`;

+ 30
- 0
server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts 查看文件

@@ -0,0 +1,30 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { CurrentUser, AppState } from '../../types';

export interface Props {
currentUser: CurrentUser;
onClose: () => void;
onSonarCloud?: boolean;
onTutorialSelect: () => void;
}

export default class GlobalHelp extends React.PureComponent<Props> {}

server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js → server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx 查看文件

@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { connect } from 'react-redux';
import GlobalNavBranding from './GlobalNavBranding';
import GlobalNavMenu from './GlobalNavMenu';
@@ -28,42 +27,41 @@ import GlobalNavPlus from './GlobalNavPlus';
import Search from '../../search/Search';
import GlobalHelp from '../../help/GlobalHelp';
import * as theme from '../../../theme';
import { isLoggedIn } from '../../../types';
import { isLoggedIn, CurrentUser, AppState } from '../../../types';
import OnboardingModal from '../../../../apps/tutorials/onboarding/OnboardingModal';
import NavBar from '../../../../components/nav/NavBar';
import Tooltip from '../../../../components/controls/Tooltip';
import HelpIcon from '../../../../components/icons-components/HelpIcon';
import OnboardingModal from '../../../../apps/tutorials/onboarding/OnboardingModal';
import { translate } from '../../../../helpers/l10n';
import { getCurrentUser, getAppState, getGlobalSettingValue } from '../../../../store/rootReducer';
import { skipOnboarding } from '../../../../store/users/actions';
import { translate } from '../../../../helpers/l10n';
import './GlobalNav.css';

/*::
type Props = {
appState: { organizationsEnabled: boolean },
currentUser: { isLoggedIn: boolean, showOnboardingTutorial: boolean },
location: { pathname: string },
skipOnboarding: () => void,
onSonarCloud: boolean
};
*/
interface StateProps {
appState: AppState;
currentUser: CurrentUser;
onSonarCloud: boolean;
}

/*::
type State = {
helpOpen: boolean,
onboardingTutorialOpen: boolean,
onboardingTutorialTooltip: boolean
};
*/

class GlobalNav extends React.PureComponent {
/*:: interval: ?number; */
/*:: props: Props; */
state /*: State */ = {
helpOpen: false,
onboardingTutorialOpen: false,
onboardingTutorialTooltip: false
};
interface DispatchProps {
skipOnboarding: () => void;
}

interface Props extends StateProps, DispatchProps {
closeOnboardingTutorial: () => void;
isOnboardingTutorialOpen: boolean;
location: { pathname: string };
openOnboardingTutorial: () => void;
}

interface State {
helpOpen: boolean;
onboardingTutorialTooltip: boolean;
}

class GlobalNav extends React.PureComponent<Props, State> {
interval?: number;
state: State = { helpOpen: false, onboardingTutorialTooltip: false };

componentDidMount() {
window.addEventListener('keypress', this.onKeyPress);
@@ -79,9 +77,9 @@ class GlobalNav extends React.PureComponent {
window.removeEventListener('keypress', this.onKeyPress);
}

onKeyPress = e => {
const tagName = e.target.tagName;
const code = e.keyCode || e.which;
onKeyPress = (event: KeyboardEvent) => {
const { tagName } = event.target as HTMLElement;
const code = event.keyCode || event.which;
const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
const isTriggerKey = code === 63;
if (!isInput && isTriggerKey) {
@@ -89,7 +87,7 @@ class GlobalNav extends React.PureComponent {
}
};

handleHelpClick = event => {
handleHelpClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.openHelp();
};
@@ -98,12 +96,16 @@ class GlobalNav extends React.PureComponent {

closeHelp = () => this.setState({ helpOpen: false });

openOnboardingTutorial = () => this.setState({ helpOpen: false, onboardingTutorialOpen: true });
openOnboardingTutorial = () => {
this.setState({ helpOpen: false });
this.props.openOnboardingTutorial();
};

closeOnboardingTutorial = () => {
this.setState({ onboardingTutorialOpen: false, onboardingTutorialTooltip: true });
this.setState({ onboardingTutorialTooltip: true });
this.props.skipOnboarding();
this.interval = setInterval(() => {
this.props.closeOnboardingTutorial();
this.interval = window.setInterval(() => {
this.setState({ onboardingTutorialTooltip: false });
}, 3000);
};
@@ -148,7 +150,7 @@ class GlobalNav extends React.PureComponent {
/>
)}

{this.state.onboardingTutorialOpen && (
{this.props.isOnboardingTutorialOpen && (
<OnboardingModal onFinish={this.closeOnboardingTutorial} />
)}
</NavBar>
@@ -156,7 +158,7 @@ class GlobalNav extends React.PureComponent {
}
}

const mapStateToProps = state => {
const mapStateToProps = (state: any): StateProps => {
const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled');

return {
@@ -166,6 +168,6 @@ const mapStateToProps = state => {
};
};

const mapDispatchToProps = { skipOnboarding };
const mapDispatchToProps: DispatchProps = { skipOnboarding };

export default connect(mapStateToProps, mapDispatchToProps)(GlobalNav);

server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js → server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx 查看文件

@@ -17,31 +17,23 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as React from 'react';
import { Link } from 'react-router';
import { isLoggedIn } from '../../../../app/types';
import { isLoggedIn, CurrentUser, AppState, Extension } from '../../../../app/types';
import { translate } from '../../../../helpers/l10n';
import { getQualityGatesUrl } from '../../../../helpers/urls';
import { getQualityGatesUrl, getBaseUrl } from '../../../../helpers/urls';
import { isMySet } from '../../../../apps/issues/utils';

export default class GlobalNavMenu extends React.PureComponent {
static propTypes = {
appState: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
location: PropTypes.shape({
pathname: PropTypes.string.isRequired
}).isRequired,
onSonarCloud: PropTypes.bool
};

static defaultProps = {
globalDashboards: [],
globalPages: []
};
interface Props {
appState: AppState;
currentUser: CurrentUser;
location: { pathname: string };
onSonarCloud: boolean;
}

activeLink(url) {
return window.location.pathname.indexOf(window.baseUrl + url) === 0 ? 'active' : null;
export default class GlobalNavMenu extends React.PureComponent<Props> {
activeLink(url: string) {
return window.location.pathname.indexOf(getBaseUrl() + url) === 0 ? 'active' : undefined;
}

renderProjects() {
@@ -144,7 +136,7 @@ export default class GlobalNavMenu extends React.PureComponent {
);
}

renderGlobalPageLink = ({ key, name }) => {
renderGlobalPageLink = ({ key, name }: Extension) => {
return (
<li key={key}>
<Link to={`/extension/${key}`}>{name}</Link>
@@ -153,7 +145,7 @@ export default class GlobalNavMenu extends React.PureComponent {
};

renderMore() {
const { globalPages } = this.props.appState;
const { globalPages = [] } = this.props.appState;
const withoutPortfolios = globalPages.filter(page => page.key !== 'governance/portfolios');
if (withoutPortfolios.length === 0) {
return null;

+ 45
- 103
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx 查看文件

@@ -25,10 +25,10 @@ import { Link } from 'react-router';
import * as theme from '../../../theme';
import { CurrentUser, LoggedInUser, isLoggedIn, Organization } from '../../../types';
import Avatar from '../../../../components/ui/Avatar';
import OrganizationLink from '../../../../components/ui/OrganizationLink';
import OrganizationListItem from '../../../../components/ui/OrganizationListItem';
import { translate } from '../../../../helpers/l10n';
import { getBaseUrl } from '../../../../helpers/urls';
import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
import Dropdown from '../../../../components/controls/Dropdown';

interface Props {
appState: { organizationsEnabled: boolean };
@@ -36,32 +36,11 @@ interface Props {
organizations: Organization[];
}

interface State {
open: boolean;
}

export default class GlobalNavUser extends React.PureComponent<Props, State> {
node?: HTMLElement | null;

export default class GlobalNavUser extends React.PureComponent<Props> {
static contextTypes = {
router: PropTypes.object
};

constructor(props: Props) {
super(props);
this.state = { open: false };
}

componentWillUnmount() {
window.removeEventListener('click', this.handleClickOutside);
}

handleClickOutside = (event: MouseEvent) => {
if (!this.node || !this.node.contains(event.target as Node)) {
this.closeDropdown();
}
};

handleLogin = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
const shouldReturnToCurrentPage = window.location.pathname !== `${getBaseUrl()}/about`;
@@ -76,98 +55,61 @@ export default class GlobalNavUser extends React.PureComponent<Props, State> {

handleLogout = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.closeDropdown();
this.context.router.push('/sessions/logout');
};

toggleDropdown = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
if (this.state.open) {
this.closeDropdown();
} else {
this.openDropdown();
}
};

openDropdown = () => {
window.addEventListener('click', this.handleClickOutside, true);
this.setState({ open: true });
};

closeDropdown = () => {
window.removeEventListener('click', this.handleClickOutside);
this.setState({ open: false });
};

renderAuthenticated() {
const { organizations } = this.props;
const currentUser = this.props.currentUser as LoggedInUser;
const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0;
return (
<li
className={classNames('dropdown js-user-authenticated', { open: this.state.open })}
ref={node => (this.node = node)}>
<a className="dropdown-toggle navbar-avatar" href="#" onClick={this.toggleDropdown}>
<Avatar
hash={currentUser.avatar}
name={currentUser.name}
size={theme.globalNavContentHeightRaw}
/>
</a>
{this.state.open && (
<ul className="dropdown-menu dropdown-menu-right">
<li className="dropdown-item">
<div className="text-ellipsis text-muted" title={currentUser.name}>
<strong>{currentUser.name}</strong>
</div>
{currentUser.email != null && (
<div
className="little-spacer-top text-ellipsis text-muted"
title={currentUser.email}>
{currentUser.email}
<Dropdown>
{({ onToggleClick, open }) => (
<li className={classNames('dropdown', 'js-user-authenticated', { open })}>
<a className="dropdown-toggle navbar-avatar" href="#" onClick={onToggleClick}>
<Avatar
hash={currentUser.avatar}
name={currentUser.name}
size={theme.globalNavContentHeightRaw}
/>
</a>
<ul className="dropdown-menu dropdown-menu-right">
<li className="dropdown-item">
<div className="text-ellipsis text-muted" title={currentUser.name}>
<strong>{currentUser.name}</strong>
</div>
)}
</li>
<li className="divider" />
<li>
<Link to="/account" onClick={this.closeDropdown}>
{translate('my_account.page')}
</Link>
</li>
{hasOrganizations && <li role="separator" className="divider" />}
{hasOrganizations && (
{currentUser.email != null && (
<div
className="little-spacer-top text-ellipsis text-muted"
title={currentUser.email}>
{currentUser.email}
</div>
)}
</li>
<li className="divider" />
<li>
<Link to="/account/organizations" onClick={this.closeDropdown}>
{translate('my_organizations')}
</Link>
<Link to="/account">{translate('my_account.page')}</Link>
</li>
)}
{hasOrganizations &&
sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
<li key={organization.key}>
<OrganizationLink
className="dropdown-item-flex"
organization={organization}
onClick={this.closeDropdown}>
<div>
<OrganizationAvatar organization={organization} small={true} />
<span className="spacer-left">{organization.name}</span>
</div>
{organization.isAdmin && (
<span className="outline-badge spacer-left">{translate('admin')}</span>
)}
</OrganizationLink>
{hasOrganizations && <li role="separator" className="divider" />}
{hasOrganizations && (
<li>
<Link to="/account/organizations">{translate('my_organizations')}</Link>
</li>
))}
{hasOrganizations && <li role="separator" className="divider" />}
<li>
<a onClick={this.handleLogout} href="#">
{translate('layout.logout')}
</a>
</li>
</ul>
)}
{hasOrganizations &&
sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
<OrganizationListItem key={organization.key} organization={organization} />
))}
{hasOrganizations && <li role="separator" className="divider" />}
<li>
<a onClick={this.handleLogout} href="#">
{translate('layout.logout')}
</a>
</li>
</ul>
</li>
)}
</li>
</Dropdown>
);
}


+ 3
- 3
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx 查看文件

@@ -43,7 +43,7 @@ it('should render the right interface for logged in user', () => {
<GlobalNavUser appState={appState} currentUser={currentUser} organizations={[]} />
);
wrapper.setState({ open: true });
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
});

it('should render user organizations', () => {
@@ -51,7 +51,7 @@ it('should render user organizations', () => {
<GlobalNavUser appState={appState} currentUser={currentUser} organizations={organizations} />
);
wrapper.setState({ open: true });
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
});

it('should not render user organizations when they are not activated', () => {
@@ -63,5 +63,5 @@ it('should not render user organizations when they are not activated', () => {
/>
);
wrapper.setState({ open: true });
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
});

+ 0
- 2
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap 查看文件

@@ -32,7 +32,6 @@ exports[`should show administration menu if the user has the rights 1`] = `
</li>
<li>
<Link
className={null}
onlyActiveOnIndex={false}
style={Object {}}
to="/coding_rules"
@@ -109,7 +108,6 @@ exports[`should work with extensions 1`] = `
</li>
<li>
<Link
className={null}
onlyActiveOnIndex={false}
style={Object {}}
to="/coding_rules"

+ 27
- 100
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap 查看文件

@@ -2,7 +2,7 @@

exports[`should not render user organizations when they are not activated 1`] = `
<li
className="dropdown js-user-authenticated open"
className="dropdown js-user-authenticated"
>
<a
className="dropdown-toggle navbar-avatar"
@@ -41,7 +41,6 @@ exports[`should not render user organizations when they are not activated 1`] =
/>
<li>
<Link
onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
to="/account"
@@ -75,7 +74,7 @@ exports[`should render the right interface for anonymous user 1`] = `

exports[`should render the right interface for logged in user 1`] = `
<li
className="dropdown js-user-authenticated open"
className="dropdown js-user-authenticated"
>
<a
className="dropdown-toggle navbar-avatar"
@@ -114,7 +113,6 @@ exports[`should render the right interface for logged in user 1`] = `
/>
<li>
<Link
onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
to="/account"
@@ -136,7 +134,7 @@ exports[`should render the right interface for logged in user 1`] = `

exports[`should render user organizations 1`] = `
<li
className="dropdown js-user-authenticated open"
className="dropdown js-user-authenticated"
>
<a
className="dropdown-toggle navbar-avatar"
@@ -175,7 +173,6 @@ exports[`should render user organizations 1`] = `
/>
<li>
<Link
onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
to="/account"
@@ -189,7 +186,6 @@ exports[`should render user organizations 1`] = `
/>
<li>
<Link
onClick={[Function]}
onlyActiveOnIndex={false}
style={Object {}}
to="/account/organizations"
@@ -197,105 +193,36 @@ exports[`should render user organizations 1`] = `
my_organizations
</Link>
</li>
<li
<OrganizationListItem
key="bar"
>
<OrganizationLink
className="dropdown-item-flex"
onClick={[Function]}
organization={
Object {
"key": "bar",
"name": "bar",
"projectVisibility": "public",
}
organization={
Object {
"key": "bar",
"name": "bar",
"projectVisibility": "public",
}
>
<div>
<OrganizationAvatar
organization={
Object {
"key": "bar",
"name": "bar",
"projectVisibility": "public",
}
}
small={true}
/>
<span
className="spacer-left"
>
bar
</span>
</div>
</OrganizationLink>
</li>
<li
}
/>
<OrganizationListItem
key="foo"
>
<OrganizationLink
className="dropdown-item-flex"
onClick={[Function]}
organization={
Object {
"key": "foo",
"name": "Foo",
"projectVisibility": "public",
}
organization={
Object {
"key": "foo",
"name": "Foo",
"projectVisibility": "public",
}
>
<div>
<OrganizationAvatar
organization={
Object {
"key": "foo",
"name": "Foo",
"projectVisibility": "public",
}
}
small={true}
/>
<span
className="spacer-left"
>
Foo
</span>
</div>
</OrganizationLink>
</li>
<li
}
/>
<OrganizationListItem
key="myorg"
>
<OrganizationLink
className="dropdown-item-flex"
onClick={[Function]}
organization={
Object {
"key": "myorg",
"name": "MyOrg",
"projectVisibility": "public",
}
organization={
Object {
"key": "myorg",
"name": "MyOrg",
"projectVisibility": "public",
}
>
<div>
<OrganizationAvatar
organization={
Object {
"key": "myorg",
"name": "MyOrg",
"projectVisibility": "public",
}
}
small={true}
/>
<span
className="spacer-left"
>
MyOrg
</span>
</div>
</OrganizationLink>
</li>
}
/>
<li
className="divider"
role="separator"

+ 28
- 0
server/sonar-web/src/main/js/app/components/search/Search.d.ts 查看文件

@@ -0,0 +1,28 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { CurrentUser, AppState } from '../../types';

export interface Props {
appState: AppState;
currentUser: CurrentUser;
}

export default class Search extends React.PureComponent<Props> {}

+ 11
- 9
server/sonar-web/src/main/js/app/styles/components/page.css 查看文件

@@ -140,18 +140,10 @@
}

.page-footer a:hover,
.page-footer a:active,
.page-footer a:focus {
color: var(--blue);
}

.page-footer a:hover {
border-bottom-color: var(--lightBlue);
}

.page-footer a:active,
.page-footer a:focus {
border-bottom-color: var(--lightBlue);
color: var(--blue);
}

.page-footer-with-sidebar {
@@ -162,6 +154,16 @@
max-width: 980px;
}

.page-footer-menu-item {
display: inline-block;
}

.page-footer-menu-item + .page-footer-menu-item::before {
content: '-';
padding: 0 calc(0.5 * var(--gridSize));
user-select: none;
}

.page-with-sidebar {
display: flex;
}

+ 5
- 1
server/sonar-web/src/main/js/app/styles/init/forms.css 查看文件

@@ -118,7 +118,7 @@ input[type='button'] {
display: inline-block;
vertical-align: baseline;
height: var(--controlHeight);
line-height: 22px;
line-height: calc(var(--controlHeight) - 2px);
padding: 0 12px;
border: 1px solid var(--darkBlue);
border-radius: 2px;
@@ -184,6 +184,10 @@ input[type='button']:disabled:focus {
box-shadow: none;
}

.button svg {
padding-top: calc((var(--controlHeight) - 16px - 2px) / 2);
}

.button-red,
input[type='submit'].button-red {
border-color: var(--red);

+ 10
- 0
server/sonar-web/src/main/js/app/types.ts 查看文件

@@ -150,3 +150,13 @@ export interface LoggedInUser extends CurrentUser {
export function isLoggedIn(user: CurrentUser): user is LoggedInUser {
return user.isLoggedIn;
}

export interface AppState {
adminPages?: Extension[];
authenticationError: boolean;
authorizationError: boolean;
canAdmin?: boolean;
globalPages?: Extension[];
organizationsEnabled: boolean;
qualifiers: string[];
}

server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js → server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.tsx 查看文件

@@ -17,61 +17,44 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
import OrganizationLink from '../../../components/ui/OrganizationLink';
/*:: import type { Organization } from '../../../store/organizations/duck'; */
import { translate } from '../../../helpers/l10n';
import { Organization } from '../../../app/types';

/*::
type Props = {
organization: Organization
};
*/

export default function OrganizationCard(props /*: Props */) {
const { organization } = props;
interface Props {
organization: Organization;
}

export default function OrganizationCard({ organization }: Props) {
return (
<div className="account-project-card clearfix">
<aside className="account-project-side">
{!!organization.avatar && (
<div className="spacer-bottom">
<img src={organization.avatar} height={30} alt={organization.name} />
</div>
)}
{!!organization.url && (
<div className="text-limited text-top spacer-bottom">
<a className="small" href={organization.url} title={organization.url} rel="nofollow">
{organization.url}
</a>
</div>
)}
<aside className="account-project-side note">
<strong>{translate('organization.key')}:</strong> {organization.key}
</aside>

<h3 className="account-project-name">
<OrganizationLink organization={organization}>{organization.name}</OrganizationLink>
<OrganizationAvatar organization={organization} />
<OrganizationLink className="spacer-left text-middle" organization={organization}>
{organization.name}
</OrganizationLink>
{organization.isAdmin && (
<span className="outline-badge spacer-left">{translate('admin')}</span>
)}
</h3>

{!!organization.description && (
<div className="account-project-description">{organization.description}</div>
<div className="markdown spacer-top">{organization.description}</div>
)}

<div className="account-project-key">
<span className="little-spacer-right">
{translate('key')}
{':'}
</span>
<input
onClick={event => event.currentTarget.select()}
readOnly={true}
type="text"
value={organization.key}
/>
</div>
{!!organization.url && (
<div className="markdown spacer-top">
<a href={organization.url} title={organization.url} rel="nofollow">
{organization.url}
</a>
</div>
)}
</div>
);
}

server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js → server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx 查看文件

@@ -17,22 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';
import { sortBy } from 'lodash';
import OrganizationCard from './OrganizationCard';
/*:: import type { Organization } from '../../../store/organizations/duck'; */
import { Organization } from '../../../app/types';

/*::
type Props = {
organizations: Array<Organization>
};
*/
interface Props {
organizations: Organization[];
}

export default function OrganizationsList(props /*: Props */) {
export default function OrganizationsList({ organizations }: Props) {
return (
<ul className="account-projects-list">
{sortBy(props.organizations, organization => organization.name.toLocaleLowerCase()).map(
{sortBy(organizations, organization => organization.name.toLocaleLowerCase()).map(
organization => (
<li key={organization.key}>
<OrganizationCard organization={organization} />

+ 1
- 1
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx 查看文件

@@ -56,7 +56,7 @@ export default function OrganizationNavigationAdministration({ location, organiz
href="#"
onClick={onToggleClick}>
{translate('layout.settings')}
<DropdownIcon />
<DropdownIcon className="little-spacer-left" />
</a>
<ul className="dropdown-menu">
{extensions.map(extension => (

+ 2
- 13
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx 查看文件

@@ -24,8 +24,7 @@ import { Organization } from '../../../app/types';
import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
import Dropdown from '../../../components/controls/Dropdown';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
import OrganizationLink from '../../../components/ui/OrganizationLink';
import { translate } from '../../../helpers/l10n';
import OrganizationListItem from '../../../components/ui/OrganizationListItem';

interface Props {
organization: Organization;
@@ -49,17 +48,7 @@ export default function OrganizationNavigationHeader({ organization, organizatio
</a>
<ul className="dropdown-menu">
{sortBy(other, org => org.name.toLowerCase()).map(organization => (
<li key={organization.key}>
<OrganizationLink className="dropdown-item-flex" organization={organization}>
<div>
<OrganizationAvatar organization={organization} small={true} />
<span className="spacer-left">{organization.name}</span>
</div>
{organization.isAdmin && (
<span className="outline-badge spacer-left">{translate('admin')}</span>
)}
</OrganizationLink>
</li>
<OrganizationListItem key={organization.key} organization={organization} />
))}
</ul>
</div>

+ 3
- 1
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap 查看文件

@@ -11,7 +11,9 @@ exports[`renders 1`] = `
onClick={[Function]}
>
layout.settings
<DropdownIcon />
<DropdownIcon
className="little-spacer-left"
/>
</a>
<ul
className="dropdown-menu"

+ 18
- 69
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap 查看文件

@@ -42,79 +42,28 @@ exports[`renders dropdown 1`] = `
<ul
className="dropdown-menu"
>
<li
<OrganizationListItem
key="org1"
>
<OrganizationLink
className="dropdown-item-flex"
organization={
Object {
"isAdmin": true,
"key": "org1",
"name": "org1",
"projectVisibility": "public",
}
organization={
Object {
"isAdmin": true,
"key": "org1",
"name": "org1",
"projectVisibility": "public",
}
>
<div>
<OrganizationAvatar
organization={
Object {
"isAdmin": true,
"key": "org1",
"name": "org1",
"projectVisibility": "public",
}
}
small={true}
/>
<span
className="spacer-left"
>
org1
</span>
</div>
<span
className="outline-badge spacer-left"
>
admin
</span>
</OrganizationLink>
</li>
<li
}
/>
<OrganizationListItem
key="org2"
>
<OrganizationLink
className="dropdown-item-flex"
organization={
Object {
"isAdmin": false,
"key": "org2",
"name": "org2",
"projectVisibility": "public",
}
organization={
Object {
"isAdmin": false,
"key": "org2",
"name": "org2",
"projectVisibility": "public",
}
>
<div>
<OrganizationAvatar
organization={
Object {
"isAdmin": false,
"key": "org2",
"name": "org2",
"projectVisibility": "public",
}
}
small={true}
/>
<span
className="spacer-left"
>
org2
</span>
</div>
</OrganizationLink>
</li>
}
/>
</ul>
</div>
`;

+ 81
- 13
server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx 查看文件

@@ -19,22 +19,90 @@
*/
import * as React from 'react';
import { Link } from 'react-router';
import * as classNames from 'classnames';
import { connect } from 'react-redux';
import * as PropTypes from 'prop-types';
import { sortBy } from 'lodash';
import { Organization } from '../../../app/types';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
import Dropdown from '../../../components/controls/Dropdown';
import { getMyOrganizations } from '../../../store/rootReducer';
import OrganizationListItem from '../../../components/ui/OrganizationListItem';
import { translate } from '../../../helpers/l10n';

interface Props {
interface StateProps {
organizations: Organization[];
}

interface Props extends StateProps {
onSonarCloud: boolean;
}

export default function NoFavoriteProjects({ onSonarCloud }: Props) {
return (
<div className="projects-empty-list">
<h3>{translate('projects.no_favorite_projects')}</h3>
<p className="big-spacer-top">{translate('projects.no_favorite_projects.engagement')}</p>
<p className="big-spacer-top">
<Link to={onSonarCloud ? '/explore/projects' : '/projects/all'} className="button">
{translate('projects.explore_projects')}
</Link>
</p>
</div>
);
export class NoFavoriteProjects extends React.PureComponent<Props> {
static contextTypes = {
openOnboardingTutorial: PropTypes.func
};

onAnalyzeProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.context.openOnboardingTutorial();
};

render() {
const { onSonarCloud, organizations } = this.props;
return (
<div className="projects-empty-list">
<h3>{translate('projects.no_favorite_projects')}</h3>
{onSonarCloud ? (
<div className="spacer-top">
<p>{translate('projects.no_favorite_projects.how_to_add_projects')}</p>
<div className="huge-spacer-top">
<a className="button" href="#" onClick={this.onAnalyzeProjectClick}>
{translate('my_account.analyze_new_project')}
</a>
<Dropdown>
{({ onToggleClick, open }) => (
<div
className={classNames('display-inline-block', 'big-spacer-left', 'dropdown', {
open
})}>
<a className="button" href="#" onClick={onToggleClick}>
{translate('projects.no_favorite_projects.favorite_projects_from_orgs')}
<DropdownIcon className="little-spacer-left" />
</a>
<ul className="dropdown-menu">
{sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
<OrganizationListItem key={organization.key} organization={organization} />
))}
</ul>
</div>
)}
</Dropdown>
<Link className="button big-spacer-left" to="/explore/projects">
{translate('projects.no_favorite_projects.favorite_public_projects')}
</Link>
</div>
</div>
) : (
<div>
<p className="big-spacer-top">
{translate('projects.no_favorite_projects.engagement')}
</p>
<p className="big-spacer-top">
<Link to="/projects/all" className="button">
{translate('projects.explore_projects')}
</Link>
</p>
</div>
)}
</div>
);
}
}

const mapStateToProps = (state: any): StateProps => ({
organizations: getMyOrganizations(state)
});

export default connect(mapStateToProps)(NoFavoriteProjects);

+ 13
- 2
server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx 查看文件

@@ -19,8 +19,19 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import NoFavoriteProjects from '../NoFavoriteProjects';
import { NoFavoriteProjects } from '../NoFavoriteProjects';
import { Visibility } from '../../../../app/types';

it('renders', () => {
expect(shallow(<NoFavoriteProjects onSonarCloud={false} />)).toMatchSnapshot();
expect(shallow(<NoFavoriteProjects onSonarCloud={false} organizations={[]} />)).toMatchSnapshot();
});

it('renders for SonarCloud', () => {
const organizations = [
{ isAdmin: true, key: 'org1', name: 'org1', projectVisibility: Visibility.Public },
{ isAdmin: false, key: 'org2', name: 'org2', projectVisibility: Visibility.Public }
];
expect(
shallow(<NoFavoriteProjects onSonarCloud={true} organizations={organizations} />)
).toMatchSnapshot();
});

+ 54
- 15
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap 查看文件

@@ -7,22 +7,61 @@ exports[`renders 1`] = `
<h3>
projects.no_favorite_projects
</h3>
<p
className="big-spacer-top"
>
projects.no_favorite_projects.engagement
</p>
<p
className="big-spacer-top"
<div>
<p
className="big-spacer-top"
>
projects.no_favorite_projects.engagement
</p>
<p
className="big-spacer-top"
>
<Link
className="button"
onlyActiveOnIndex={false}
style={Object {}}
to="/projects/all"
>
projects.explore_projects
</Link>
</p>
</div>
</div>
`;

exports[`renders for SonarCloud 1`] = `
<div
className="projects-empty-list"
>
<h3>
projects.no_favorite_projects
</h3>
<div
className="spacer-top"
>
<Link
className="button"
onlyActiveOnIndex={false}
style={Object {}}
to="/projects/all"
<p>
projects.no_favorite_projects.how_to_add_projects
</p>
<div
className="huge-spacer-top"
>
projects.explore_projects
</Link>
</p>
<a
className="button"
href="#"
onClick={[Function]}
>
my_account.analyze_new_project
</a>
<Dropdown />
<Link
className="button big-spacer-left"
onlyActiveOnIndex={false}
style={Object {}}
to="/explore/projects"
>
projects.no_favorite_projects.favorite_public_projects
</Link>
</div>
</div>
</div>
`;

+ 1
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap 查看文件

@@ -47,6 +47,6 @@ exports[`renders different types of "no projects" 3`] = `
<div
className="projects-list"
>
<NoFavoriteProjects />
<Connect(NoFavoriteProjects) />
</div>
`;

+ 5
- 0
server/sonar-web/src/main/js/apps/projects/styles.css 查看文件

@@ -267,3 +267,8 @@
margin-left: -250px;
text-align: center;
}

.projects-empty-list {
padding: calc(4 * var(--gridSize)) 0;
text-align: center;
}

+ 26
- 0
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts 查看文件

@@ -0,0 +1,26 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';

export interface Props {
onFinish: () => void;
}

export default class OnboardingModal extends React.PureComponent<Props> {}

+ 3
- 3
server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx 查看文件

@@ -24,13 +24,13 @@ export default function DropdownIcon({ className, fill = 'currentColor', size =
return (
<svg
className={className}
width={size}
width={size / 16 * 7}
height={size}
viewBox="0 0 16 16"
viewBox="0 0 7 16"
version="1.1"
xmlnsXlink="http://www.w3.org/1999/xlink"
xmlSpace="preserve">
<g transform="matrix(0.0273438,0,0,0.0273438,4.5,2.65625)">
<g transform="matrix(0.0273438,0,0,0.0273438,-6.4e-06,2.65625)">
<path
style={{ fill }}
d="M256,176C256,180.333 254.417,184.083 251.25,187.25L139.25,299.25C136.083,302.417 132.333,304 128,304C123.667,304 119.917,302.417 116.75,299.25L4.75,187.25C1.583,184.083 0,180.333 0,176C0,171.667 1.583,167.917 4.75,164.75C7.917,161.583 11.667,160 16,160L240,160C244.333,160 248.083,161.583 251.25,164.75C254.417,167.917 256,171.667 256,176Z"

+ 1
- 0
server/sonar-web/src/main/js/components/nav/NavBar.tsx 查看文件

@@ -26,6 +26,7 @@ interface Props {
className?: string;
height: number;
notif?: React.ReactNode;
[prop: string]: any;
}

export default function NavBar({ children, className, height, notif, ...other }: Props) {

server/sonar-web/src/main/js/app/components/GlobalContainer.js → server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx 查看文件

@@ -17,25 +17,28 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import GlobalNav from './nav/global/GlobalNav';
import GlobalFooterContainer from './GlobalFooterContainer';
import GlobalMessagesContainer from './GlobalMessagesContainer';
import * as React from 'react';
import { Organization } from '../../app/types';
import OrganizationLink from './OrganizationLink';
import OrganizationAvatar from '../common/OrganizationAvatar';
import { translate } from '../../helpers/l10n';

export default function GlobalContainer(props /*: Object */) {
// it is important to pass `location` down to `GlobalNav` to trigger render on url change
interface Props {
organization: Organization;
}

export default function OrganizationListItem({ organization }: Props) {
return (
<div className="global-container">
<div className="page-wrapper" id="container">
<div className="page-container">
<GlobalNav location={props.location} />
<GlobalMessagesContainer />
{props.children}
<li>
<OrganizationLink className="dropdown-item-flex" organization={organization}>
<div>
<OrganizationAvatar organization={organization} small={true} />
<span className="spacer-left">{organization.name}</span>
</div>
</div>
<GlobalFooterContainer />
</div>
{organization.isAdmin && (
<span className="outline-badge spacer-left">{translate('admin')}</span>
)}
</OrganizationLink>
</li>
);
}

+ 38
- 0
server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx 查看文件

@@ -0,0 +1,38 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationListItem from '../OrganizationListItem';
import { Visibility } from '../../../app/types';

it('renders', () => {
expect(
shallow(
<OrganizationListItem
organization={{
isAdmin: true,
key: 'org',
name: 'org',
projectVisibility: Visibility.Public
}}
/>
)
).toMatchSnapshot();
});

+ 41
- 0
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap 查看文件

@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<li>
<OrganizationLink
className="dropdown-item-flex"
organization={
Object {
"isAdmin": true,
"key": "org",
"name": "org",
"projectVisibility": "public",
}
}
>
<div>
<OrganizationAvatar
organization={
Object {
"isAdmin": true,
"key": "org",
"name": "org",
"projectVisibility": "public",
}
}
small={true}
/>
<span
className="spacer-left"
>
org
</span>
</div>
<span
className="outline-badge spacer-left"
>
admin
</span>
</OrganizationLink>
</li>
`;

+ 3
- 10
server/sonar-web/src/main/js/store/appState/duck.ts 查看文件

@@ -18,15 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { Extension } from '../../app/types';

interface AppState {
adminPages?: Extension[];
authenticationError: boolean;
authorizationError: boolean;
organizationsEnabled: boolean;
qualifiers?: string[];
}
import { Extension, AppState } from '../../app/types';

interface SetAppStateAction {
type: 'SET_APP_STATE';
@@ -62,7 +54,8 @@ export function requireAuthorization(): RequireAuthorizationAction {
const defaultValue: AppState = {
authenticationError: false,
authorizationError: false,
organizationsEnabled: false
organizationsEnabled: false,
qualifiers: []
};

export default function(state: AppState = defaultValue, action: Action): AppState {

+ 1
- 1
server/sonar-web/src/main/js/store/organizations/duck.js 查看文件

@@ -199,7 +199,7 @@ function byKey(state /*: ByKeyState */ = {}, action /*: Action */) {
case 'RECEIVE_MY_ORGANIZATIONS':
return onReceiveOrganizations(state, action);
case 'CREATE_ORGANIZATION':
return { ...state, [action.organization.key]: action.organization };
return { ...state, [action.organization.key]: { ...action.organization, isAdmin: true } };
case 'UPDATE_ORGANIZATION':
return {
...state,

+ 4
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties 查看文件

@@ -685,6 +685,9 @@ projects._projects=projects
projects.no_projects.empty_instance=Once you analyze some projects, they will show up here.
projects.no_favorite_projects=You don't have any favorite projects yet.
projects.no_favorite_projects.engagement=Discover and mark as favorites projects you are interested in to have a quick access to them.
projects.no_favorite_projects.how_to_add_projects=Here is how to add projects to this page
projects.no_favorite_projects.favorite_projects_from_orgs=Favorite projects from your orgs
projects.no_favorite_projects.favorite_public_projects=Favorite public projects
projects.explore_projects=Explore Projects
projects.not_analyzed=Project is not analyzed yet.
projects.no_leak_period=Project has no leak data yet.
@@ -2472,7 +2475,7 @@ footer.community=Community
footer.documentation=Documentation
footer.get_started=Get Started
footer.help=Help
footer.licence=LGPL v3
footer.license=LGPL v3
footer.news=News
footer.plugins=Plugins
footer.production_database_explanation=The embedded database will not scale, it will not support upgrading to newer versions of SonarQube, and there is no support for migrating your data out of it into a different database engine.

Loading…
取消
儲存