Browse Source

SONAR-10188 Update project/organization header

tags/7.0-RC1
Stas Vilchik 6 years ago
parent
commit
7b757f9053
95 changed files with 1789 additions and 1723 deletions
  1. 11
    27
      server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx
  2. 11
    27
      server/sonar-web/src/main/js/app/components/SimpleContainer.tsx
  3. 13
    29
      server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.tsx
  4. 4
    4
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
  5. 8
    12
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  6. 47
    45
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
  7. 12
    4
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
  8. 2
    2
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
  9. 5
    4
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
  10. 3
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
  11. 0
    98
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap
  12. 104
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap
  13. 18
    14
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
  14. 6
    1
      server/sonar-web/src/main/js/app/styles/components/page.css
  15. 1
    1
      server/sonar-web/src/main/js/app/styles/init/base.css
  16. 1
    0
      server/sonar-web/src/main/js/app/styles/init/icons.css
  17. 3
    4
      server/sonar-web/src/main/js/app/styles/print.css
  18. 0
    5
      server/sonar-web/src/main/js/app/styles/style.css
  19. 17
    8
      server/sonar-web/src/main/js/app/types.ts
  20. 4
    0
      server/sonar-web/src/main/js/apps/about/components/AboutApp.js
  21. 1
    1
      server/sonar-web/src/main/js/apps/account/account.css
  22. 2
    2
      server/sonar-web/src/main/js/apps/account/components/Password.js
  23. 0
    4
      server/sonar-web/src/main/js/apps/account/components/Security.js
  24. 24
    22
      server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js
  25. 1
    6
      server/sonar-web/src/main/js/apps/account/notifications/Notifications.js
  26. 26
    24
      server/sonar-web/src/main/js/apps/account/notifications/Projects.js
  27. 65
    61
      server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.js.snap
  28. 1
    4
      server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Notifications-test.js.snap
  29. 150
    138
      server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.js.snap
  30. 5
    0
      server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx
  31. 13
    20
      server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx
  32. 19
    17
      server/sonar-web/src/main/js/apps/account/profile/Profile.js
  33. 73
    69
      server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs
  34. 30
    28
      server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.js
  35. 3
    1
      server/sonar-web/src/main/js/apps/code/components/App.tsx
  36. 8
    6
      server/sonar-web/src/main/js/apps/code/components/Search.tsx
  37. 6
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js
  38. 4
    0
      server/sonar-web/src/main/js/apps/component-measures/components/App.js
  39. 14
    12
      server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list.hbs
  40. 1
    1
      server/sonar-web/src/main/js/apps/groups/templates/groups-list.hbs
  41. 1
    1
      server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs
  42. 6
    0
      server/sonar-web/src/main/js/apps/issues/components/App.js
  43. 5
    1
      server/sonar-web/src/main/js/apps/issues/components/PageActions.js
  44. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx
  45. 1
    1
      server/sonar-web/src/main/js/apps/marketplace/Search.tsx
  46. 0
    1
      server/sonar-web/src/main/js/apps/marketplace/style.css
  47. 1
    1
      server/sonar-web/src/main/js/apps/metrics/templates/metrics-layout.hbs
  48. 16
    14
      server/sonar-web/src/main/js/apps/organizations/components/MembersList.js
  49. 78
    76
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.js
  50. 39
    35
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersList-test.js.snap
  51. 325
    313
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.js.snap
  52. 26
    6
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
  53. 2
    1
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx
  54. 1
    1
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap
  55. 2
    2
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap
  56. 0
    8
      server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
  57. 6
    4
      server/sonar-web/src/main/js/apps/permission-templates/components/List.js
  58. 10
    8
      server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx
  59. 0
    8
      server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
  60. 1
    1
      server/sonar-web/src/main/js/apps/project-admin/key/Key.js
  61. 6
    4
      server/sonar-web/src/main/js/apps/project-admin/links/Table.js
  62. 0
    4
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
  63. 24
    20
      server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
  64. 64
    60
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
  65. 13
    11
      server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx
  66. 82
    78
      server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap
  67. 0
    10
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
  68. 7
    7
      server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
  69. 1
    0
      server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
  70. 1
    0
      server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
  71. 28
    26
      server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
  72. 62
    58
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap
  73. 4
    0
      server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js
  74. 0
    11
      server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx
  75. 0
    11
      server/sonar-web/src/main/js/apps/settings/components/App.js
  76. 28
    26
      server/sonar-web/src/main/js/apps/users/UsersList.tsx
  77. 61
    57
      server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList.tsx.snap
  78. 21
    19
      server/sonar-web/src/main/js/apps/web-api/components/Action.tsx
  79. 43
    39
      server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap
  80. 1
    4
      server/sonar-web/src/main/js/apps/web-api/styles/web-api.css
  81. 1
    1
      server/sonar-web/src/main/js/components/SourceViewer/styles.css
  82. 3
    0
      server/sonar-web/src/main/js/components/common/BranchStatus.css
  83. 5
    5
      server/sonar-web/src/main/js/components/common/BranchStatus.tsx
  84. 30
    12
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap
  85. 2
    2
      server/sonar-web/src/main/js/components/controls/Favorite.tsx
  86. 5
    4
      server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx
  87. 0
    42
      server/sonar-web/src/main/js/components/controls/FavoriteIssueFilter.js
  88. 13
    7
      server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
  89. 1
    1
      server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx
  90. 9
    3
      server/sonar-web/src/main/js/components/controls/__tests__/FavoriteBase-test.tsx
  91. 1
    0
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Favorite-test.tsx.snap
  92. 4
    4
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap
  93. 3
    2
      server/sonar-web/src/main/js/components/nav/ContextNavBar.css
  94. 7
    7
      server/sonar-web/src/main/js/helpers/urls.ts
  95. 22
    3
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 11
- 27
server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx View File

@@ -21,32 +21,16 @@ import * as React from 'react';
import { Link } from 'react-router';
import { translate } from '../../helpers/l10n';

export default class ComponentContainerNotFound extends React.PureComponent {
componentDidMount() {
const html = document.querySelector('html');
if (html) {
html.classList.add('dashboard-page');
}
}

componentWillUnmount() {
const html = document.querySelector('html');
if (html) {
html.classList.remove('dashboard-page');
}
}

render() {
return (
<div id="bd" className="page-wrapper-simple">
<div id="nonav" className="page-simple">
<h2 className="big-spacer-bottom">{translate('dashboard.project_not_found')}</h2>
<p className="spacer-bottom">{translate('dashboard.project_not_found.2')}</p>
<p>
<Link to="/">Go back to the homepage</Link>
</p>
</div>
export default function ComponentContainerNotFound() {
return (
<div id="bd" className="page-wrapper-simple">
<div id="nonav" className="page-simple">
<h2 className="big-spacer-bottom">{translate('dashboard.project_not_found')}</h2>
<p className="spacer-bottom">{translate('dashboard.project_not_found.2')}</p>
<p>
<Link to="/">Go back to the homepage</Link>
</p>
</div>
);
}
</div>
);
}

+ 11
- 27
server/sonar-web/src/main/js/app/components/SimpleContainer.tsx View File

@@ -27,35 +27,19 @@ interface Props {
hideLoggedInInfo?: boolean;
}

export default class SimpleContainer extends React.PureComponent<Props> {
componentDidMount() {
const html = document.querySelector('html');
if (html) {
html.classList.add('dashboard-page');
}
}
export default function SimpleContainer(props: Props) {
return (
<div className="global-container">
<div className="page-wrapper" id="container">
<NavBar className="navbar-global" height={theme.globalNavHeightRaw} />

componentWillUnmount() {
const html = document.querySelector('html');
if (html) {
html.classList.remove('dashboard-page');
}
}

render() {
return (
<div className="global-container">
<div className="page-wrapper" id="container">
<NavBar className="navbar-global" height={theme.globalNavHeightRaw} />

<div id="bd" className="page-wrapper-simple">
<div id="nonav" className="page-simple">
{this.props.children}
</div>
<div id="bd" className="page-wrapper-simple">
<div id="nonav" className="page-simple">
{props.children}
</div>
</div>
<GlobalFooterContainer hideLoggedInInfo={this.props.hideLoggedInInfo} />
</div>
);
}
<GlobalFooterContainer hideLoggedInInfo={props.hideLoggedInInfo} />
</div>
);
}

+ 13
- 29
server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.tsx View File

@@ -20,34 +20,18 @@
import * as React from 'react';
import { Link } from 'react-router';

export default class ExtensionNotFound extends React.PureComponent {
componentDidMount() {
const html = document.querySelector('html');
if (html) {
html.classList.add('dashboard-page');
}
}

componentWillUnmount() {
const html = document.querySelector('html');
if (html) {
html.classList.remove('dashboard-page');
}
}

render() {
return (
<div id="bd" className="page-wrapper-simple">
<div id="nonav" className="page-simple">
<h2 className="big-spacer-bottom">The page you were looking for does not exist.</h2>
<p className="spacer-bottom">
You may have mistyped the address or the page may have moved.
</p>
<p>
<Link to="/">Go back to the homepage</Link>
</p>
</div>
export default function ExtensionNotFound() {
return (
<div id="bd" className="page-wrapper-simple">
<div id="nonav" className="page-simple">
<h2 className="big-spacer-bottom">The page you were looking for does not exist.</h2>
<p className="spacer-bottom">
You may have mistyped the address or the page may have moved.
</p>
<p>
<Link to="/">Go back to the homepage</Link>
</p>
</div>
);
}
</div>
);
}

+ 4
- 4
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css View File

@@ -1,9 +1,9 @@
.navbar-context-branches {
display: inline-block;
vertical-align: top;
padding: var(--gridSize) 0;
display: inline-flex;
justify-content: center;
line-height: calc(2 * var(--gridSize));
margin-left: calc(2 * var(--gridSize));
line-height: 16px;
font-size: var(--baseFontSize);
}

.navbar-context-meta-branch-menu-item {

+ 8
- 12
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx View File

@@ -18,8 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import ComponentNavBranch from './ComponentNavBranch';
import ComponentNavBreadcrumbs from './ComponentNavBreadcrumbs';
import ComponentNavHeader from './ComponentNavHeader';
import ComponentNavMeta from './ComponentNavMeta';
import ComponentNavMenu from './ComponentNavMenu';
import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif';
@@ -111,16 +110,13 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
id="context-navigation"
height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw}
notif={notifComponent}>
<ComponentNavBreadcrumbs component={this.props.component} />
{this.props.currentBranch && (
<ComponentNavBranch
branches={this.props.branches}
component={this.props.component}
currentBranch={this.props.currentBranch}
// to close dropdown on any location change
location={this.props.location}
/>
)}
<ComponentNavHeader
branches={this.props.branches}
component={this.props.component}
currentBranch={this.props.currentBranch}
// to close dropdown on any location change
location={this.props.location}
/>
<ComponentNavMeta branch={this.props.currentBranch} component={this.props.component} />
<ComponentNavMenu
branch={this.props.currentBranch}

server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx → server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx View File

@@ -20,7 +20,8 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import { Component, Organization } from '../../../types';
import ComponentNavBranch from './ComponentNavBranch';
import { Component, Organization, Branch, Breadcrumb } from '../../../types';
import QualifierIcon from '../../../../components/shared/QualifierIcon';
import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer';
import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
@@ -36,46 +37,16 @@ interface StateProps {
}

interface OwnProps {
branches: Branch[];
component: Component;
currentBranch?: Branch;
location?: any;
}

interface Props extends StateProps, OwnProps {}

export function ComponentNavBreadcrumbs(props: Props) {
export function ComponentNavHeader(props: Props) {
const { component, organization, shouldOrganizationBeDisplayed } = props;
const { breadcrumbs } = component;

const lastItem = breadcrumbs[breadcrumbs.length - 1];

const items: JSX.Element[] = [];
breadcrumbs.forEach((item, index) => {
const isPath = item.qualifier === 'DIR';
const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name);

if (index === 0) {
items.push(
<QualifierIcon
className="spacer-right"
key={`qualifier-${item.key}`}
qualifier={lastItem.qualifier}
/>
);
}

items.push(
<Link
className="link-base-color link-no-underline"
key={`name-${item.key}`}
title={item.name}
to={getProjectUrl(item.key)}>
{itemName}
</Link>
);

if (index < breadcrumbs.length - 1) {
items.push(<span className="slash-separator" key={`separator-${item.key}`} />);
}
});

return (
<header className="navbar-context-header">
@@ -83,29 +54,60 @@ export function ComponentNavBreadcrumbs(props: Props) {
title={component.name}
organization={organization && shouldOrganizationBeDisplayed ? organization : undefined}
/>
{organization &&
shouldOrganizationBeDisplayed && <OrganizationAvatar organization={organization} />}
{organization &&
shouldOrganizationBeDisplayed && (
<OrganizationLink
organization={organization}
className="link-base-color link-no-underline spacer-left">
{organization.name}
</OrganizationLink>
<>
<OrganizationAvatar organization={organization} />
<OrganizationLink
organization={organization}
className="link-base-color link-no-underline spacer-left">
{organization.name}
</OrganizationLink>
<span className="slash-separator" />
</>
)}
{organization && shouldOrganizationBeDisplayed && <span className="slash-separator" />}
{items}
{renderBreadcrumbs(component.breadcrumbs)}
{component.visibility === 'private' && (
<PrivateBadge className="spacer-left" qualifier={component.qualifier} />
)}
{props.currentBranch && (
<ComponentNavBranch
branches={props.branches}
component={component}
currentBranch={props.currentBranch}
// to close dropdown on any location change
location={props.location}
/>
)}
</header>
);
}

function renderBreadcrumbs(breadcrumbs: Breadcrumb[]) {
const lastItem = breadcrumbs[breadcrumbs.length - 1];
return breadcrumbs.map((item, index) => {
const isPath = item.qualifier === 'DIR';
const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name);

return (
<React.Fragment key={item.key}>
{index === 0 && <QualifierIcon className="spacer-right" qualifier={lastItem.qualifier} />}
<Link
className="link-base-color link-no-underline"
title={item.name}
to={getProjectUrl(item.key)}>
{itemName}
</Link>
{index < breadcrumbs.length - 1 && <span className="slash-separator" />}
</React.Fragment>
);
});
}

const mapStateToProps = (state: any, ownProps: OwnProps): StateProps => ({
organization:
ownProps.component.organization && getOrganizationByKey(state, ownProps.component.organization),
shouldOrganizationBeDisplayed: areThereCustomOrganizations(state)
});

export default connect(mapStateToProps)(ComponentNavBreadcrumbs);
export default connect(mapStateToProps)(ComponentNavHeader);

+ 12
- 4
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { connect } from 'react-redux';
import { Branch, Component, CurrentUser, isLoggedIn } from '../../../types';
import { Branch, Component, CurrentUser, isLoggedIn, HomePageType } from '../../../types';
import BranchStatus from '../../../../components/common/BranchStatus';
import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
import Favorite from '../../../../components/controls/Favorite';
@@ -60,14 +60,22 @@ export function ComponentNavMeta({ branch, component, currentUser }: Props) {
{isLoggedIn(currentUser) &&
mainBranch && (
<div className="navbar-context-meta-secondary">
<Favorite component={component.key} favorite={Boolean(component.isFavorite)} />
<Favorite
component={component.key}
favorite={Boolean(component.isFavorite)}
qualifier={component.qualifier}
/>
<HomePageSelect
className="spacer-left"
currentPage={{ type: 'project', key: component.key }}
currentPage={{ type: HomePageType.Project, parameter: component.key }}
/>
</div>
)}
{shortBranch && <BranchStatus branch={branch!} />}
{shortBranch && (
<div className="navbar-context-meta-secondary">
<BranchStatus branch={branch!} />
</div>
)}
</div>
);
}

+ 2
- 2
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx View File

@@ -29,9 +29,9 @@ jest.mock('../ComponentNavMeta', () => ({
}
}));

jest.mock('../ComponentNavBreadcrumbs', () => ({
jest.mock('../ComponentNavHeader', () => ({
// eslint-disable-next-line
default: function ComponentNavBreadcrumbs() {
default: function ComponentNavHeader() {
return null;
}
}));

server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx → server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import { ComponentNavBreadcrumbs } from '../ComponentNavBreadcrumbs';
import { ComponentNavHeader } from '../ComponentNavHeader';
import { Visibility } from '../../../../types';

it('should not render breadcrumbs with one element', () => {
@@ -32,7 +32,7 @@ it('should not render breadcrumbs with one element', () => {
visibility: 'public'
};
const result = shallow(
<ComponentNavBreadcrumbs component={component} shouldOrganizationBeDisplayed={false} />
<ComponentNavHeader branches={[]} component={component} shouldOrganizationBeDisplayed={false} />
);
expect(result).toMatchSnapshot();
});
@@ -52,7 +52,8 @@ it('should render organization', () => {
projectVisibility: Visibility.Public
};
const result = shallow(
<ComponentNavBreadcrumbs
<ComponentNavHeader
branches={[]}
component={component}
organization={organization}
shouldOrganizationBeDisplayed={true}
@@ -71,7 +72,7 @@ it('renders private badge', () => {
visibility: 'private'
};
const result = shallow(
<ComponentNavBreadcrumbs component={component} shouldOrganizationBeDisplayed={false} />
<ComponentNavHeader branches={[]} component={component} shouldOrganizationBeDisplayed={false} />
);
expect(result.find('PrivateBadge')).toHaveLength(1);
});

+ 3
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap View File

@@ -27,7 +27,8 @@ exports[`renders 1`] = `
/>
}
>
<ComponentNavBreadcrumbs
<ComponentNavHeader
branches={Array []}
component={
Object {
"breadcrumbs": Array [
@@ -43,6 +44,7 @@ exports[`renders 1`] = `
"qualifier": "TRK",
}
}
location={Object {}}
/>
<ComponentNavMeta
component={

+ 0
- 98
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap View File

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

exports[`should not render breadcrumbs with one element 1`] = `
<header
className="navbar-context-header"
>
<OrganizationHelmet
title="My Project"
/>
<QualifierIcon
className="spacer-right"
key="qualifier-my-project"
qualifier="TRK"
/>
<Link
className="link-base-color link-no-underline"
key="name-my-project"
onlyActiveOnIndex={false}
style={Object {}}
title="My Project"
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "my-project",
},
}
}
>
My Project
</Link>
</header>
`;

exports[`should render organization 1`] = `
<header
className="navbar-context-header"
>
<OrganizationHelmet
organization={
Object {
"key": "foo",
"name": "The Foo Organization",
"projectVisibility": "public",
}
}
title="My Project"
/>
<OrganizationAvatar
organization={
Object {
"key": "foo",
"name": "The Foo Organization",
"projectVisibility": "public",
}
}
/>
<OrganizationLink
className="link-base-color link-no-underline spacer-left"
organization={
Object {
"key": "foo",
"name": "The Foo Organization",
"projectVisibility": "public",
}
}
>
The Foo Organization
</OrganizationLink>
<span
className="slash-separator"
/>
<QualifierIcon
className="spacer-right"
key="qualifier-my-project"
qualifier="TRK"
/>
<Link
className="link-base-color link-no-underline"
key="name-my-project"
onlyActiveOnIndex={false}
style={Object {}}
title="My Project"
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "my-project",
},
}
}
>
My Project
</Link>
</header>
`;

+ 104
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap View File

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

exports[`should not render breadcrumbs with one element 1`] = `
<header
className="navbar-context-header"
>
<OrganizationHelmet
title="My Project"
/>
<React.Fragment
key="my-project"
>
<QualifierIcon
className="spacer-right"
qualifier="TRK"
/>
<Link
className="link-base-color link-no-underline"
onlyActiveOnIndex={false}
style={Object {}}
title="My Project"
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "my-project",
},
}
}
>
My Project
</Link>
</React.Fragment>
</header>
`;

exports[`should render organization 1`] = `
<header
className="navbar-context-header"
>
<OrganizationHelmet
organization={
Object {
"key": "foo",
"name": "The Foo Organization",
"projectVisibility": "public",
}
}
title="My Project"
/>
<React.Fragment>
<OrganizationAvatar
organization={
Object {
"key": "foo",
"name": "The Foo Organization",
"projectVisibility": "public",
}
}
/>
<OrganizationLink
className="link-base-color link-no-underline spacer-left"
organization={
Object {
"key": "foo",
"name": "The Foo Organization",
"projectVisibility": "public",
}
}
>
The Foo Organization
</OrganizationLink>
<span
className="slash-separator"
/>
</React.Fragment>
<React.Fragment
key="my-project"
>
<QualifierIcon
className="spacer-right"
qualifier="TRK"
/>
<Link
className="link-base-color link-no-underline"
onlyActiveOnIndex={false}
style={Object {}}
title="My Project"
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "my-project",
},
}
}
>
My Project
</Link>
</React.Fragment>
</header>
`;

+ 18
- 14
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap View File

@@ -38,20 +38,24 @@ exports[`renders status of short-living branch 1`] = `
date="2017-01-02T00:00:00.000Z"
/>
</div>
<BranchStatus
branch={
Object {
"isMain": false,
"mergeBranch": "master",
"name": "feature",
"status": Object {
"bugs": 0,
"codeSmells": 2,
"vulnerabilities": 3,
},
"type": "SHORT",
<div
className="navbar-context-meta-secondary"
>
<BranchStatus
branch={
Object {
"isMain": false,
"mergeBranch": "master",
"name": "feature",
"status": Object {
"bugs": 0,
"codeSmells": 2,
"vulnerabilities": 3,
},
"type": "SHORT",
}
}
}
/>
/>
</div>
</div>
`;

+ 6
- 1
server/sonar-web/src/main/js/app/styles/components/page.css View File

@@ -17,6 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.white-page {
background-color: #fff !important;
}

.global-container {
display: flex;
flex-direction: column;
@@ -150,7 +154,8 @@
padding-left: calc(50vw - 370px + 10px) !important;
}

.page-footer-with-sidebar div {
.page-footer-with-sidebar div,
.page-footer-with-sidebar .page-footer-menu {
max-width: 980px;
}


+ 1
- 1
server/sonar-web/src/main/js/app/styles/init/base.css View File

@@ -26,7 +26,7 @@

html,
body {
background-color: #fff;
background-color: var(--barBackgroundColor);
}

body {

+ 1
- 0
server/sonar-web/src/main/js/app/styles/init/icons.css View File

@@ -485,6 +485,7 @@ a:hover > .icon-radio {
stroke-width: 1.41421356;
stroke-opacity: 1;
fill-opacity: 0;
vector-effect: non-scaling-stroke;
transition: all 0.2s ease;
}


+ 3
- 4
server/sonar-web/src/main/js/app/styles/print.css View File

@@ -31,11 +31,10 @@
display: none !important;
}

.dashboard-page,
.dashboard-page body {
background-color: #fff;
html,
body {
background-color: #fff !important;
}

.widget thead,
.widget tfoot {
display: table-row-group;

+ 0
- 5
server/sonar-web/src/main/js/app/styles/style.css View File

@@ -17,11 +17,6 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.dashboard-page,
.dashboard-page body {
background-color: var(--barBackgroundColor);
}

.tabs {
height: 20px;
border-bottom: 1px solid #ddd;

+ 17
- 8
server/sonar-web/src/main/js/app/types.ts View File

@@ -62,13 +62,15 @@ export interface Extension {
name: string;
}

export interface Breadcrumb {
key: string;
name: string;
qualifier: string;
}

export interface Component {
analysisDate?: string;
breadcrumbs: Array<{
key: string;
name: string;
qualifier: string;
}>;
breadcrumbs: Breadcrumb[];
configuration?: ComponentConfiguration;
description?: string;
extensions?: Extension[];
@@ -141,13 +143,20 @@ export interface CurrentUser {
showOnboardingTutorial?: boolean;
}

export enum HomePageType {
Project = 'PROJECT',
Organization = 'ORGANIZATION',
MyProjects = 'MY_PROJECTS',
MyIssues = 'MY_ISSUES'
}

export interface HomePage {
key?: string;
type: string;
parameter?: string;
type: HomePageType;
}

export function isSameHomePage(a: HomePage, b: HomePage) {
return a.type === b.type && a.key === b.key;
return a.type === b.type && a.parameter === b.parameter;
}

export interface LoggedInUser extends CurrentUser {

+ 4
- 0
server/sonar-web/src/main/js/apps/about/components/AboutApp.js View File

@@ -76,11 +76,15 @@ class AboutApp extends React.PureComponent {
window.location = 'https://about.sonarcloud.io';
} else {
this.loadData();
// $FlowFixMe
document.body.classList.add('white-page');
}
}

componentWillUnmount() {
this.mounted = false;
// $FlowFixMe
document.body.classList.remove('white-page');
}

loadProjects() {

+ 1
- 1
server/sonar-web/src/main/js/apps/account/account.css View File

@@ -8,7 +8,7 @@
padding-top: 20px;
padding-bottom: 20px;
border-bottom: 1px solid var(--barBorderColor);
background-color: var(--barBackgroundColor);
background-color: #fff;
}

.account-nav {

+ 2
- 2
server/sonar-web/src/main/js/apps/account/components/Password.js View File

@@ -70,10 +70,10 @@ export default class Password extends Component {
const { success, errors } = this.state;

return (
<section>
<section className="boxed-group">
<h2 className="spacer-bottom">{translate('my_profile.password.title')}</h2>

<form onSubmit={this.handleChangePassword}>
<form className="boxed-group-inner" onSubmit={this.handleChangePassword}>
{success && (
<div className="alert alert-success">{translate('my_profile.password.changed')}</div>
)}

+ 0
- 4
server/sonar-web/src/main/js/apps/account/components/Security.js View File

@@ -31,11 +31,7 @@ function Security(props) {
return (
<div className="account-body account-container">
<Helmet title={translate('my_account.security')} />

<Tokens user={user} />

{user.local && <hr className="account-separator" />}

{user.local && <Password user={user} />}
</div>
);

+ 24
- 22
server/sonar-web/src/main/js/apps/account/notifications/GlobalNotifications.js View File

@@ -46,30 +46,32 @@ type Props = {

function GlobalNotifications(props /*: Props */) {
return (
<section>
<h2 className="spacer-bottom">{translate('my_profile.overall_notifications.title')}</h2>
<section className="boxed-group">
<h2>{translate('my_profile.overall_notifications.title')}</h2>

<table className="form">
<thead>
<tr>
<th />
{props.channels.map(channel => (
<th key={channel} className="text-center">
<h4>{translate('notification.channel', channel)}</h4>
</th>
))}
</tr>
</thead>
<div className="boxed-group-inner">
<table className="form">
<thead>
<tr>
<th />
{props.channels.map(channel => (
<th key={channel} className="text-center">
<h4>{translate('notification.channel', channel)}</h4>
</th>
))}
</tr>
</thead>

<NotificationsList
notifications={props.notifications}
channels={props.channels}
types={props.types}
checkboxId={(d, c) => `global-notification-${d}-${c}`}
onAdd={props.addNotification}
onRemove={props.removeNotification}
/>
</table>
<NotificationsList
notifications={props.notifications}
channels={props.channels}
types={props.types}
checkboxId={(d, c) => `global-notification-${d}-${c}`}
onAdd={props.addNotification}
onRemove={props.removeNotification}
/>
</table>
</div>
</section>
);
}

+ 1
- 6
server/sonar-web/src/main/js/apps/account/notifications/Notifications.js View File

@@ -40,13 +40,8 @@ class Notifications extends React.PureComponent {
return (
<div className="account-body account-container">
<Helmet title={translate('my_account.notifications')} />

<p className="big-spacer-bottom">{translate('notification.dispatcher.information')}</p>

<p className="alert alert-info">{translate('notification.dispatcher.information')}</p>
<GlobalNotifications />

<hr className="account-separator" />

<Projects />
</div>
);

+ 26
- 24
server/sonar-web/src/main/js/apps/account/notifications/Projects.js View File

@@ -113,30 +113,32 @@ class Projects extends React.PureComponent {
const allProjects = [...this.props.projects, ...this.state.addedProjects];

return (
<section>
<h2 className="spacer-bottom">{translate('my_profile.per_project_notifications.title')}</h2>

{allProjects.length === 0 && (
<div className="note">{translate('my_account.no_project_notifications')}</div>
)}

{allProjects.map(project => <ProjectNotifications key={project.key} project={project} />)}

<div className="spacer-top panel bg-muted">
<span className="text-middle spacer-right">
{translate('my_account.set_notifications_for')}:
</span>
<AsyncSelect
autoload={false}
cache={false}
name="new_project"
style={{ width: '300px' }}
loadOptions={this.loadOptions}
minimumInput={2}
optionRenderer={this.renderOption}
onChange={this.handleAddProject}
placeholder={translate('my_account.search_project')}
/>
<section className="boxed-group">
<h2>{translate('my_profile.per_project_notifications.title')}</h2>

<div className="boxed-group-inner">
{allProjects.length === 0 && (
<div className="note">{translate('my_account.no_project_notifications')}</div>
)}

{allProjects.map(project => <ProjectNotifications key={project.key} project={project} />)}

<div className="spacer-top panel bg-muted">
<span className="text-middle spacer-right">
{translate('my_account.set_notifications_for')}:
</span>
<AsyncSelect
autoload={false}
cache={false}
name="new_project"
style={{ width: '300px' }}
loadOptions={this.loadOptions}
minimumInput={2}
optionRenderer={this.renderOption}
onChange={this.handleAddProject}
placeholder={translate('my_account.search_project')}
/>
</div>
</div>
</section>
);

+ 65
- 61
server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/GlobalNotifications-test.js.snap View File

@@ -1,69 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should match snapshot 1`] = `
<section>
<h2
className="spacer-bottom"
>
<section
className="boxed-group"
>
<h2>
my_profile.overall_notifications.title
</h2>
<table
className="form"
<div
className="boxed-group-inner"
>
<thead>
<tr>
<th />
<th
className="text-center"
key="channel1"
>
<h4>
notification.channel.channel1
</h4>
</th>
<th
className="text-center"
key="channel2"
>
<h4>
notification.channel.channel2
</h4>
</th>
</tr>
</thead>
<NotificationsList
channels={
Array [
"channel1",
"channel2",
]
}
checkboxId={[Function]}
notifications={
Array [
Object {
"channel": "channel1",
"type": "type1",
},
Object {
"channel": "channel1",
"type": "type2",
},
Object {
"channel": "channel2",
"type": "type2",
},
]
}
onAdd={[Function]}
onRemove={[Function]}
types={
Array [
"type1",
"type2",
]
}
/>
</table>
<table
className="form"
>
<thead>
<tr>
<th />
<th
className="text-center"
key="channel1"
>
<h4>
notification.channel.channel1
</h4>
</th>
<th
className="text-center"
key="channel2"
>
<h4>
notification.channel.channel2
</h4>
</th>
</tr>
</thead>
<NotificationsList
channels={
Array [
"channel1",
"channel2",
]
}
checkboxId={[Function]}
notifications={
Array [
Object {
"channel": "channel1",
"type": "type1",
},
Object {
"channel": "channel1",
"type": "type2",
},
Object {
"channel": "channel2",
"type": "type2",
},
]
}
onAdd={[Function]}
onRemove={[Function]}
types={
Array [
"type1",
"type2",
]
}
/>
</table>
</div>
</section>
`;

+ 1
- 4
server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Notifications-test.js.snap View File

@@ -10,14 +10,11 @@ exports[`should match snapshot 1`] = `
title="my_account.notifications"
/>
<p
className="big-spacer-bottom"
className="alert alert-info"
>
notification.dispatcher.information
</p>
<Connect(GlobalNotifications) />
<hr
className="account-separator"
/>
<Connect(Projects) />
</div>
`;

+ 150
- 138
server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/Projects-test.js.snap View File

@@ -1,178 +1,190 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render projects 1`] = `
<section>
<h2
className="spacer-bottom"
>
<section
className="boxed-group"
>
<h2>
my_profile.per_project_notifications.title
</h2>
<Connect(ProjectNotifications)
key="foo"
project={
Object {
"key": "foo",
"name": "Foo",
}
}
/>
<Connect(ProjectNotifications)
key="bar"
project={
Object {
"key": "bar",
"name": "Bar",
}
}
/>
<div
className="spacer-top panel bg-muted"
className="boxed-group-inner"
>
<span
className="text-middle spacer-right"
>
my_account.set_notifications_for
:
</span>
<AsyncSelect
autoload={false}
cache={false}
loadOptions={[Function]}
minimumInput={2}
name="new_project"
onChange={[Function]}
optionRenderer={[Function]}
placeholder="my_account.search_project"
style={
<Connect(ProjectNotifications)
key="foo"
project={
Object {
"key": "foo",
"name": "Foo",
}
}
/>
<Connect(ProjectNotifications)
key="bar"
project={
Object {
"width": "300px",
"key": "bar",
"name": "Bar",
}
}
/>
<div
className="spacer-top panel bg-muted"
>
<span
className="text-middle spacer-right"
>
my_account.set_notifications_for
:
</span>
<AsyncSelect
autoload={false}
cache={false}
loadOptions={[Function]}
minimumInput={2}
name="new_project"
onChange={[Function]}
optionRenderer={[Function]}
placeholder="my_account.search_project"
style={
Object {
"width": "300px",
}
}
/>
</div>
</div>
</section>
`;

exports[`should render projects 2`] = `
<section>
<h2
className="spacer-bottom"
>
<section
className="boxed-group"
>
<h2>
my_profile.per_project_notifications.title
</h2>
<Connect(ProjectNotifications)
key="foo"
project={
Object {
"key": "foo",
"name": "Foo",
}
}
/>
<Connect(ProjectNotifications)
key="bar"
project={
Object {
"key": "bar",
"name": "Bar",
}
}
/>
<Connect(ProjectNotifications)
key="qux"
project={
Object {
"key": "qux",
"name": "Qux",
}
}
/>
<div
className="spacer-top panel bg-muted"
className="boxed-group-inner"
>
<span
className="text-middle spacer-right"
>
my_account.set_notifications_for
:
</span>
<AsyncSelect
autoload={false}
cache={false}
loadOptions={[Function]}
minimumInput={2}
name="new_project"
onChange={[Function]}
optionRenderer={[Function]}
placeholder="my_account.search_project"
style={
<Connect(ProjectNotifications)
key="foo"
project={
Object {
"key": "foo",
"name": "Foo",
}
}
/>
<Connect(ProjectNotifications)
key="bar"
project={
Object {
"key": "bar",
"name": "Bar",
}
}
/>
<Connect(ProjectNotifications)
key="qux"
project={
Object {
"width": "300px",
"key": "qux",
"name": "Qux",
}
}
/>
<div
className="spacer-top panel bg-muted"
>
<span
className="text-middle spacer-right"
>
my_account.set_notifications_for
:
</span>
<AsyncSelect
autoload={false}
cache={false}
loadOptions={[Function]}
minimumInput={2}
name="new_project"
onChange={[Function]}
optionRenderer={[Function]}
placeholder="my_account.search_project"
style={
Object {
"width": "300px",
}
}
/>
</div>
</div>
</section>
`;

exports[`should render projects 3`] = `
<section>
<h2
className="spacer-bottom"
>
<section
className="boxed-group"
>
<h2>
my_profile.per_project_notifications.title
</h2>
<Connect(ProjectNotifications)
key="foo"
project={
Object {
"key": "foo",
"name": "Foo",
}
}
/>
<Connect(ProjectNotifications)
key="bar"
project={
Object {
"key": "bar",
"name": "Bar",
}
}
/>
<Connect(ProjectNotifications)
key="qux"
project={
Object {
"key": "qux",
"name": "Qux",
}
}
/>
<div
className="spacer-top panel bg-muted"
className="boxed-group-inner"
>
<span
className="text-middle spacer-right"
>
my_account.set_notifications_for
:
</span>
<AsyncSelect
autoload={false}
cache={false}
loadOptions={[Function]}
minimumInput={2}
name="new_project"
onChange={[Function]}
optionRenderer={[Function]}
placeholder="my_account.search_project"
style={
<Connect(ProjectNotifications)
key="foo"
project={
Object {
"width": "300px",
"key": "foo",
"name": "Foo",
}
}
/>
<Connect(ProjectNotifications)
key="bar"
project={
Object {
"key": "bar",
"name": "Bar",
}
}
/>
<Connect(ProjectNotifications)
key="qux"
project={
Object {
"key": "qux",
"name": "Qux",
}
}
/>
<div
className="spacer-top panel bg-muted"
>
<span
className="text-middle spacer-right"
>
my_account.set_notifications_for
:
</span>
<AsyncSelect
autoload={false}
cache={false}
loadOptions={[Function]}
minimumInput={2}
name="new_project"
onChange={[Function]}
optionRenderer={[Function]}
placeholder="my_account.search_project"
style={
Object {
"width": "300px",
}
}
/>
</div>
</div>
</section>
`;

+ 5
- 0
server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx View File

@@ -21,12 +21,17 @@ import * as React from 'react';
import { sortBy } from 'lodash';
import OrganizationCard from './OrganizationCard';
import { Organization } from '../../../app/types';
import { translate } from '../../../helpers/l10n';

interface Props {
organizations: Organization[];
}

export default function OrganizationsList({ organizations }: Props) {
if (organizations.length === 0) {
return <div>{translate('my_account.organizations.no_results')}</div>;
}

return (
<ul className="account-projects-list">
{sortBy(organizations, organization => organization.name.toLocaleLowerCase()).map(

+ 13
- 20
server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx View File

@@ -87,29 +87,22 @@ class UserOrganizations extends React.PureComponent<Props, State> {
<div className="account-body account-container">
<Helmet title={translate('my_account.organizations')} />

<header className="page-header">
<h2 className="page-title">{translate('my_account.organizations')}</h2>
<div className="boxed-group">
{canCreateOrganizations && (
<div className="page-actions">
<button onClick={this.handleCreateClick}>{translate('create')}</button>
<div className="clearfix">
<div className="boxed-group-actions">
<button onClick={this.handleCreateClick}>{translate('create')}</button>
</div>
</div>
)}
{this.props.organizations.length > 0 ? (
<div className="page-description">
{translate('my_account.organizations.description')}
</div>
) : (
<div className="page-description">
{translate('my_account.organizations.no_results')}
</div>
)}
</header>

{this.state.loading ? (
<i className="spinner" />
) : (
<OrganizationsList organizations={this.props.organizations} />
)}
<div className="boxed-group-inner">
{this.state.loading ? (
<i className="spinner" />
) : (
<OrganizationsList organizations={this.props.organizations} />
)}
</div>
</div>

{this.state.createOrganization && (
<CreateOrganizationForm

+ 19
- 17
server/sonar-web/src/main/js/apps/account/profile/Profile.js View File

@@ -45,29 +45,31 @@ function Profile(props /*: Props */) {

return (
<div className="account-body account-container">
<div className="spacer-bottom">
{translate('login')}: <strong id="login">{user.login}</strong>
</div>
<div className="boxed-group boxed-group-inner">
<div className="spacer-bottom">
{translate('login')}: <strong id="login">{user.login}</strong>
</div>

{!user.local &&
user.externalProvider !== 'sonarqube' && (
<div id="identity-provider" className="spacer-bottom">
<UserExternalIdentity user={user} />
</div>
)}

{!user.local &&
user.externalProvider !== 'sonarqube' && (
<div id="identity-provider" className="spacer-bottom">
<UserExternalIdentity user={user} />
{!!user.email && (
<div className="spacer-bottom">
{translate('my_profile.email')}: <strong id="email">{user.email}</strong>
</div>
)}

{!!user.email && (
<div className="spacer-bottom">
{translate('my_profile.email')}: <strong id="email">{user.email}</strong>
</div>
)}
{!customOrganizations && <hr className="account-separator" />}
{!customOrganizations && <UserGroups groups={user.groups} />}

{!customOrganizations && <hr className="account-separator" />}
{!customOrganizations && <UserGroups groups={user.groups} />}
<hr />

<hr className="account-separator" />

<UserScmAccounts user={user} scmAccounts={user.scmAccounts} />
<UserScmAccounts user={user} scmAccounts={user.scmAccounts} />
</div>
</div>
);
}

+ 73
- 69
server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs View File

@@ -1,77 +1,81 @@
<h2 class="spacer-bottom">{{t 'users.tokens'}}</h2>
<div class="boxed-group">
<h2>{{t 'users.tokens'}}</h2>

<div class="big-spacer-bottom big-spacer-right markdown">
<p>{{t 'my_account.tokens_description'}}</p>
</div>
<div class="boxed-group-inner">
<div class="big-spacer-bottom big-spacer-right markdown">
<p>{{t 'my_account.tokens_description'}}</p>
</div>

{{#notNull tokens}}
<table class="data">
<thead>
<tr>
<th>{{t 'name'}}</th>
<th class="text-right">{{t 'created'}}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{{#each tokens}}
<tr>
<td>
<div title="{{name}}">
{{limitString name}}
</div>
</td>
<td class="thin nowrap text-right">
{{d createdAt}}
</td>
<td class="thin nowrap text-right">
<div class="big-spacer-left">
<form class="js-revoke-token-form" data-token="{{name}}">
{{#if deleting}}
<button class="button-red active input-small">{{t 'users.tokens.sure'}}</button>
{{else}}
<button class="button-red input-small">{{t 'users.tokens.revoke'}}</button>
{{/if}}
</form>
</div>
</td>
</tr>
{{else}}
{{#notNull tokens}}
<table class="data">
<thead>
<tr>
<td colspan="3">
<span class="note">{{t 'users.no_tokens'}}</span>
</td>
<th>{{t 'name'}}</th>
<th class="text-right">{{t 'created'}}</th>
<th>&nbsp;</th>
</tr>
{{/each}}
</tbody>
</table>
{{/notNull}}
</thead>
<tbody>
{{#each tokens}}
<tr>
<td>
<div title="{{name}}">
{{limitString name}}
</div>
</td>
<td class="thin nowrap text-right">
{{d createdAt}}
</td>
<td class="thin nowrap text-right">
<div class="big-spacer-left">
<form class="js-revoke-token-form" data-token="{{name}}">
{{#if deleting}}
<button class="button-red active input-small">{{t 'users.tokens.sure'}}</button>
{{else}}
<button class="button-red input-small">{{t 'users.tokens.revoke'}}</button>
{{/if}}
</form>
</div>
</td>
</tr>
{{else}}
<tr>
<td colspan="3">
<span class="note">{{t 'users.no_tokens'}}</span>
</td>
</tr>
{{/each}}
</tbody>
</table>
{{/notNull}}

{{#each errors}}
<div class="alert alert-danger">{{msg}}</div>
{{/each}}
{{#each errors}}
<div class="alert alert-danger">{{msg}}</div>
{{/each}}

<form class="js-generate-token-form spacer-top panel bg-muted">
<label>{{t 'users.generate_new_token'}}:</label>
<input type="text" required maxlength="100" placeholder="{{t 'users.enter_token_name'}}">
<button>{{t 'users.generate'}}</button>
</form>
<form class="js-generate-token-form spacer-top panel bg-muted">
<label>{{t 'users.generate_new_token'}}:</label>
<input type="text" required maxlength="100" placeholder="{{t 'users.enter_token_name'}}">
<button>{{t 'users.generate'}}</button>
</form>

{{#if newToken}}
<div class="panel panel-white big-spacer-top">
<div class="alert alert-warning">
{{tp 'users.tokens.new_token_created' newToken.name}}
</div>
{{#if newToken}}
<div class="panel panel-white big-spacer-top">
<div class="alert alert-warning">
{{tp 'users.tokens.new_token_created' newToken.name}}
</div>

<table class="data">
<tr>
<td class="thin">
<button class="js-copy-to-clipboard" data-clipboard-text="{{newToken.token}}">{{t 'copy'}}</button>
</td>
<td class="nowrap">
<div class="monospaced text-success">{{newToken.token}}</div>
</td>
</tr>
</table>
</div>
{{/if}}
<table class="data">
<tr>
<td class="thin">
<button class="js-copy-to-clipboard" data-clipboard-text="{{newToken.token}}">{{t 'copy'}}</button>
</td>
<td class="nowrap">
<div class="monospaced text-success">{{newToken.token}}</div>
</td>
</tr>
</table>
</div>
{{/if}}
</div>
</div>

+ 30
- 28
server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.js View File

@@ -49,34 +49,36 @@ export default class Tasks extends React.PureComponent {
});

return (
<table className={className}>
<thead>
<tr>
<th>{translate('background_tasks.table.status')}</th>
<th>{translate('background_tasks.table.task')}</th>
<th>{translate('background_tasks.table.id')}</th>
<th>&nbsp;</th>
<th className="text-right">{translate('background_tasks.table.submitted')}</th>
<th className="text-right">{translate('background_tasks.table.started')}</th>
<th className="text-right">{translate('background_tasks.table.finished')}</th>
<th className="text-right">{translate('background_tasks.table.duration')}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{tasks.map((task, index, tasks) => (
<Task
key={task.id}
task={task}
tasks={tasks}
component={component}
onCancelTask={onCancelTask}
onFilterTask={onFilterTask}
previousTask={index > 0 ? tasks[index - 1] : undefined}
/>
))}
</tbody>
</table>
<div className="boxed-group boxed-group-inner">
<table className={className}>
<thead>
<tr>
<th>{translate('background_tasks.table.status')}</th>
<th>{translate('background_tasks.table.task')}</th>
<th>{translate('background_tasks.table.id')}</th>
<th>&nbsp;</th>
<th className="text-right">{translate('background_tasks.table.submitted')}</th>
<th className="text-right">{translate('background_tasks.table.started')}</th>
<th className="text-right">{translate('background_tasks.table.finished')}</th>
<th className="text-right">{translate('background_tasks.table.duration')}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{tasks.map((task, index, tasks) => (
<Task
key={task.id}
task={task}
tasks={tasks}
component={component}
onCancelTask={onCancelTask}
onFilterTask={onFilterTask}
previousTask={index > 0 ? tasks[index - 1] : undefined}
/>
))}
</tbody>
</table>
</div>
);
}
}

+ 3
- 1
server/sonar-web/src/main/js/apps/code/components/App.tsx View File

@@ -200,7 +200,9 @@ export default class App extends React.PureComponent<Props, State> {

const shouldShowBreadcrumbs = breadcrumbs.length > 1;

const componentsClassName = classNames('spacer-top', { 'new-loading': loading });
const componentsClassName = classNames('boxed-group', 'boxed-group-inner', 'spacer-top', {
'new-loading': loading
});

return (
<div className="page page-limited">

+ 8
- 6
server/sonar-web/src/main/js/apps/code/components/Search.tsx View File

@@ -182,12 +182,14 @@ export default class Search extends React.PureComponent<Props, State> {
{loading && <i className="spinner spacer-left" />}

{results != null && (
<Components
branch={this.props.branch}
components={results}
rootComponent={component}
selected={selected}
/>
<div className="boxed-group boxed-group-inner spacer-top">
<Components
branch={this.props.branch}
components={results}
rootComponent={component}
selected={selected}
/>
</div>
)}
</div>
);

+ 6
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js View File

@@ -44,6 +44,9 @@ class CodingRulesAppContainer extends React.PureComponent {
*/

componentDidMount() {
// $FlowFixMe
document.body.classList.add('white-page');

if (this.props.appState.organizationsEnabled && !this.props.params.organizationKey) {
// redirect to organization-level rules page
this.props.router.replace(
@@ -62,6 +65,9 @@ class CodingRulesAppContainer extends React.PureComponent {
}

componentWillUnmount() {
// $FlowFixMe
document.body.classList.remove('white-page');

if (this.stop) {
this.stop();
}

+ 4
- 0
server/sonar-web/src/main/js/apps/component-measures/components/App.js View File

@@ -74,6 +74,8 @@ export default class App extends React.PureComponent {

componentDidMount() {
this.mounted = true;
// $FlowFixMe
document.body.classList.add('white-page');
this.props.fetchMetrics();
this.fetchMeasures(this.props);
key.setScope('measures-files');
@@ -95,6 +97,8 @@ export default class App extends React.PureComponent {

componentWillUnmount() {
this.mounted = false;
// $FlowFixMe
document.body.classList.remove('white-page');
key.deleteScope('measures-files');
const footer = document.getElementById('footer');
if (footer) {

+ 14
- 12
server/sonar-web/src/main/js/apps/custom-measures/templates/custom-measures-list.hbs View File

@@ -1,12 +1,14 @@
<table class="data zebra">
<thead>
<tr>
<th>{{t 'custom_measures.metric'}}</th>
<th>{{t 'value'}}</th>
<th>{{t 'description'}}</th>
<th>{{t 'date'}}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="boxed-group boxed-group-inner">
<table class="data zebra">
<thead>
<tr>
<th>{{t 'custom_measures.metric'}}</th>
<th>{{t 'value'}}</th>
<th>{{t 'description'}}</th>
<th>{{t 'date'}}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>

+ 1
- 1
server/sonar-web/src/main/js/apps/groups/templates/groups-list.hbs View File

@@ -1,4 +1,4 @@
<div>
<div class="boxed-group boxed-group-inner">
{{#isNull organization}}
<div class="panel panel-vertical js-anyone">
<div class="display-inline-block text-top width-20">

+ 1
- 1
server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs View File

@@ -1,4 +1,4 @@
<div class="panel panel-vertical bordered-bottom spacer-bottom">
<div class="big-spacer-bottom">
<form id="groups-search-form" class="search-box">
<input id="groups-search-query" class="search-box-input" type="text" name="q" placeholder="{{t 'search.search_by_name'}}" maxlength="100">
<svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">

+ 6
- 0
server/sonar-web/src/main/js/apps/issues/components/App.js View File

@@ -151,6 +151,9 @@ export default class App extends React.PureComponent {
return;
}

// $FlowFixMe
document.body.classList.add('white-page');

const footer = document.getElementById('footer');
if (footer) {
footer.classList.add('page-footer-with-sidebar');
@@ -206,6 +209,9 @@ export default class App extends React.PureComponent {
componentWillUnmount() {
this.detachShortcuts();

// $FlowFixMe
document.body.classList.remove('white-page');

const footer = document.getElementById('footer');
if (footer) {
footer.classList.remove('page-footer-with-sidebar');

+ 5
- 1
server/sonar-web/src/main/js/apps/issues/components/PageActions.js View File

@@ -22,6 +22,7 @@ import React from 'react';
import IssuesCounter from './IssuesCounter';
import ReloadButton from './ReloadButton';
/*:: import type { Paging } from '../utils'; */
import { HomePageType } from '../../../app/types';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import { translate } from '../../../helpers/l10n';
@@ -74,7 +75,10 @@ export default class PageActions extends React.PureComponent {
</div>

{this.props.canSetHome && (
<HomePageSelect className="huge-spacer-left" currentPage={{ type: 'my-issues' }} />
<HomePageSelect
className="huge-spacer-left"
currentPage={{ type: HomePageType.MyIssues }}
/>
)}
</div>
);

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/PluginsList.tsx View File

@@ -79,7 +79,7 @@ export default class PluginsList extends React.PureComponent<Props> {

render() {
return (
<div id="marketplace-plugins">
<div className="boxed-group boxed-group-inner" id="marketplace-plugins">
<ul>
{this.props.plugins.map(plugin => (
<li key={plugin.key} className="panel panel-vertical">

+ 1
- 1
server/sonar-web/src/main/js/apps/marketplace/Search.tsx View File

@@ -48,7 +48,7 @@ export default class Search extends React.PureComponent<Props> {
}
];
return (
<div id="marketplace-search" className="panel panel-vertical bordered-bottom spacer-bottom">
<div id="marketplace-search" className="big-spacer-bottom">
<div className="display-inline-block text-top nowrap abs-width-150 spacer-right">
<RadioToggle
name="marketplace-filter"

+ 0
- 1
server/sonar-web/src/main/js/apps/marketplace/style.css View File

@@ -12,7 +12,6 @@
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #f3f3f3;
margin-left: 8px;
margin-right: 8px;
}

+ 1
- 1
server/sonar-web/src/main/js/apps/metrics/templates/metrics-layout.hbs View File

@@ -1,5 +1,5 @@
<div class="page page-limited">
<div id="metrics-header"></div>
<div id="metrics-list"></div>
<div id="metrics-list" class="boxed-group boxed-group-inner"></div>
<div id="metrics-list-footer"></div>
</div>

+ 16
- 14
server/sonar-web/src/main/js/apps/organizations/components/MembersList.js View File

@@ -38,20 +38,22 @@ export default class MembersList extends React.PureComponent {

render() {
return (
<table className="data zebra">
<tbody>
{this.props.members.map(member => (
<MembersListItem
key={member.login}
member={member}
organizationGroups={this.props.organizationGroups}
organization={this.props.organization}
removeMember={this.props.removeMember}
updateMemberGroups={this.props.updateMemberGroups}
/>
))}
</tbody>
</table>
<div className="boxed-group boxed-group-inner">
<table className="data zebra">
<tbody>
{this.props.members.map(member => (
<MembersListItem
key={member.login}
member={member}
organizationGroups={this.props.organizationGroups}
organization={this.props.organization}
removeMember={this.props.removeMember}
updateMemberGroups={this.props.updateMemberGroups}
/>
))}
</tbody>
</table>
</div>
);
}
}

+ 78
- 76
server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.js View File

@@ -107,87 +107,89 @@ class OrganizationEdit extends React.PureComponent {
<h1 className="page-title">{title}</h1>
</header>

<form onSubmit={this.handleSubmit}>
<div className="modal-field">
<label htmlFor="organization-name">
{translate('organization.name')}
<em className="mandatory">*</em>
</label>
<input
id="organization-name"
name="name"
required={true}
type="text"
maxLength="64"
value={this.state.name}
disabled={this.state.loading}
onChange={e => this.setState({ name: e.target.value })}
/>
<div className="modal-field-description">
{translate('organization.name.description')}
</div>
</div>
<div className="modal-field">
<label htmlFor="organization-avatar">{translate('organization.avatar')}</label>
<input
id="organization-avatar"
name="avatar"
type="text"
maxLength="256"
value={this.state.avatar}
disabled={this.state.loading}
onChange={this.handleAvatarInputChange}
/>
<div className="modal-field-description">
{translate('organization.avatar.description')}
<div className="boxed-group boxed-group-inner">
<form onSubmit={this.handleSubmit}>
<div className="modal-field">
<label htmlFor="organization-name">
{translate('organization.name')}
<em className="mandatory">*</em>
</label>
<input
id="organization-name"
name="name"
required={true}
type="text"
maxLength="64"
value={this.state.name}
disabled={this.state.loading}
onChange={e => this.setState({ name: e.target.value })}
/>
<div className="modal-field-description">
{translate('organization.name.description')}
</div>
</div>
{!!this.state.avatarImage && (
<div className="spacer-top spacer-bottom">
<div className="little-spacer-bottom">
{translate('organization.avatar.preview')}
{':'}
<div className="modal-field">
<label htmlFor="organization-avatar">{translate('organization.avatar')}</label>
<input
id="organization-avatar"
name="avatar"
type="text"
maxLength="256"
value={this.state.avatar}
disabled={this.state.loading}
onChange={this.handleAvatarInputChange}
/>
<div className="modal-field-description">
{translate('organization.avatar.description')}
</div>
{!!this.state.avatarImage && (
<div className="spacer-top spacer-bottom">
<div className="little-spacer-bottom">
{translate('organization.avatar.preview')}
{':'}
</div>
<img src={this.state.avatarImage} alt="" height={30} />
</div>
<img src={this.state.avatarImage} alt="" height={30} />
)}
</div>
<div className="modal-field">
<label htmlFor="organization-description">{translate('description')}</label>
<textarea
id="organization-description"
name="description"
rows="3"
maxLength="256"
value={this.state.description}
disabled={this.state.loading}
onChange={e => this.setState({ description: e.target.value })}
/>
<div className="modal-field-description">
{translate('organization.description.description')}
</div>
</div>
<div className="modal-field">
<label htmlFor="organization-url">{translate('organization.url')}</label>
<input
id="organization-url"
name="url"
type="text"
maxLength="256"
value={this.state.url}
disabled={this.state.loading}
onChange={e => this.setState({ url: e.target.value })}
/>
<div className="modal-field-description">
{translate('organization.url.description')}
</div>
)}
</div>
<div className="modal-field">
<label htmlFor="organization-description">{translate('description')}</label>
<textarea
id="organization-description"
name="description"
rows="3"
maxLength="256"
value={this.state.description}
disabled={this.state.loading}
onChange={e => this.setState({ description: e.target.value })}
/>
<div className="modal-field-description">
{translate('organization.description.description')}
</div>
</div>
<div className="modal-field">
<label htmlFor="organization-url">{translate('organization.url')}</label>
<input
id="organization-url"
name="url"
type="text"
maxLength="256"
value={this.state.url}
disabled={this.state.loading}
onChange={e => this.setState({ url: e.target.value })}
/>
<div className="modal-field-description">
{translate('organization.url.description')}
<div className="modal-field">
<button type="submit" disabled={this.state.loading}>
{translate('save')}
</button>
{this.state.loading && <i className="spinner spacer-left" />}
</div>
</div>
<div className="modal-field">
<button type="submit" disabled={this.state.loading}>
{translate('save')}
</button>
{this.state.loading && <i className="spinner spacer-left" />}
</div>
</form>
</form>
</div>
</div>
);
}

+ 39
- 35
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersList-test.js.snap View File

@@ -1,44 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render a list of members of an organization 1`] = `
<table
className="data zebra"
<div
className="boxed-group boxed-group-inner"
>
<tbody>
<MembersListItem
key="admin"
member={
Object {
"avatar": "",
"groupCount": 3,
"login": "admin",
"name": "Admin Istrator",
<table
className="data zebra"
>
<tbody>
<MembersListItem
key="admin"
member={
Object {
"avatar": "",
"groupCount": 3,
"login": "admin",
"name": "Admin Istrator",
}
}
}
organization={
Object {
"key": "foo",
"name": "Foo",
organization={
Object {
"key": "foo",
"name": "Foo",
}
}
}
/>
<MembersListItem
key="john"
member={
Object {
"avatar": "7daf6c79d4802916d83f6266e24850af",
"groupCount": 1,
"login": "john",
"name": "John Doe",
/>
<MembersListItem
key="john"
member={
Object {
"avatar": "7daf6c79d4802916d83f6266e24850af",
"groupCount": 1,
"login": "john",
"name": "John Doe",
}
}
}
organization={
Object {
"key": "foo",
"name": "Foo",
organization={
Object {
"key": "foo",
"name": "Foo",
}
}
}
/>
</tbody>
</table>
/>
</tbody>
</table>
</div>
`;

+ 325
- 313
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.js.snap View File

@@ -18,118 +18,122 @@ exports[`smoke test 1`] = `
organization.edit
</h1>
</header>
<form
onSubmit={[Function]}
<div
className="boxed-group boxed-group-inner"
>
<div
className="modal-field"
<form
onSubmit={[Function]}
>
<label
htmlFor="organization-name"
<div
className="modal-field"
>
organization.name
<em
className="mandatory"
<label
htmlFor="organization-name"
>
organization.name
<em
className="mandatory"
>
*
</em>
</label>
<input
disabled={false}
id="organization-name"
maxLength="64"
name="name"
onChange={[Function]}
required={true}
type="text"
value="Foo"
/>
<div
className="modal-field-description"
>
*
</em>
</label>
<input
disabled={false}
id="organization-name"
maxLength="64"
name="name"
onChange={[Function]}
required={true}
type="text"
value="Foo"
/>
organization.name.description
</div>
</div>
<div
className="modal-field-description"
className="modal-field"
>
organization.name.description
<label
htmlFor="organization-avatar"
>
organization.avatar
</label>
<input
disabled={false}
id="organization-avatar"
maxLength="256"
name="avatar"
onChange={[Function]}
type="text"
value=""
/>
<div
className="modal-field-description"
>
organization.avatar.description
</div>
</div>
</div>
<div
className="modal-field"
>
<label
htmlFor="organization-avatar"
>
organization.avatar
</label>
<input
disabled={false}
id="organization-avatar"
maxLength="256"
name="avatar"
onChange={[Function]}
type="text"
value=""
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.avatar.description
<label
htmlFor="organization-description"
>
description
</label>
<textarea
disabled={false}
id="organization-description"
maxLength="256"
name="description"
onChange={[Function]}
rows="3"
value=""
/>
<div
className="modal-field-description"
>
organization.description.description
</div>
</div>
</div>
<div
className="modal-field"
>
<label
htmlFor="organization-description"
>
description
</label>
<textarea
disabled={false}
id="organization-description"
maxLength="256"
name="description"
onChange={[Function]}
rows="3"
value=""
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.description.description
<label
htmlFor="organization-url"
>
organization.url
</label>
<input
disabled={false}
id="organization-url"
maxLength="256"
name="url"
onChange={[Function]}
type="text"
value=""
/>
<div
className="modal-field-description"
>
organization.url.description
</div>
</div>
</div>
<div
className="modal-field"
>
<label
htmlFor="organization-url"
>
organization.url
</label>
<input
disabled={false}
id="organization-url"
maxLength="256"
name="url"
onChange={[Function]}
type="text"
value=""
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.url.description
<button
disabled={false}
type="submit"
>
save
</button>
</div>
</div>
<div
className="modal-field"
>
<button
disabled={false}
type="submit"
>
save
</button>
</div>
</form>
</form>
</div>
</div>
`;

@@ -151,133 +155,137 @@ exports[`smoke test 2`] = `
organization.edit
</h1>
</header>
<form
onSubmit={[Function]}
<div
className="boxed-group boxed-group-inner"
>
<div
className="modal-field"
<form
onSubmit={[Function]}
>
<label
htmlFor="organization-name"
>
organization.name
<em
className="mandatory"
>
*
</em>
</label>
<input
disabled={false}
id="organization-name"
maxLength="64"
name="name"
onChange={[Function]}
required={true}
type="text"
value="New Foo"
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.name.description
<label
htmlFor="organization-name"
>
organization.name
<em
className="mandatory"
>
*
</em>
</label>
<input
disabled={false}
id="organization-name"
maxLength="64"
name="name"
onChange={[Function]}
required={true}
type="text"
value="New Foo"
/>
<div
className="modal-field-description"
>
organization.name.description
</div>
</div>
</div>
<div
className="modal-field"
>
<label
htmlFor="organization-avatar"
>
organization.avatar
</label>
<input
disabled={false}
id="organization-avatar"
maxLength="256"
name="avatar"
onChange={[Function]}
type="text"
value="foo-avatar"
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.avatar.description
<label
htmlFor="organization-avatar"
>
organization.avatar
</label>
<input
disabled={false}
id="organization-avatar"
maxLength="256"
name="avatar"
onChange={[Function]}
type="text"
value="foo-avatar"
/>
<div
className="modal-field-description"
>
organization.avatar.description
</div>
<div
className="spacer-top spacer-bottom"
>
<div
className="little-spacer-bottom"
>
organization.avatar.preview
:
</div>
<img
alt=""
height={30}
src="foo-avatar-image"
/>
</div>
</div>
<div
className="spacer-top spacer-bottom"
className="modal-field"
>
<label
htmlFor="organization-description"
>
description
</label>
<textarea
disabled={false}
id="organization-description"
maxLength="256"
name="description"
onChange={[Function]}
rows="3"
value="foo-description"
/>
<div
className="little-spacer-bottom"
className="modal-field-description"
>
organization.avatar.preview
:
organization.description.description
</div>
<img
alt=""
height={30}
src="foo-avatar-image"
/>
</div>
</div>
<div
className="modal-field"
>
<label
htmlFor="organization-description"
>
description
</label>
<textarea
disabled={false}
id="organization-description"
maxLength="256"
name="description"
onChange={[Function]}
rows="3"
value="foo-description"
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.description.description
<label
htmlFor="organization-url"
>
organization.url
</label>
<input
disabled={false}
id="organization-url"
maxLength="256"
name="url"
onChange={[Function]}
type="text"
value="foo-url"
/>
<div
className="modal-field-description"
>
organization.url.description
</div>
</div>
</div>
<div
className="modal-field"
>
<label
htmlFor="organization-url"
>
organization.url
</label>
<input
disabled={false}
id="organization-url"
maxLength="256"
name="url"
onChange={[Function]}
type="text"
value="foo-url"
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.url.description
<button
disabled={false}
type="submit"
>
save
</button>
</div>
</div>
<div
className="modal-field"
>
<button
disabled={false}
type="submit"
>
save
</button>
</div>
</form>
</form>
</div>
</div>
`;

@@ -299,135 +307,139 @@ exports[`smoke test 3`] = `
organization.edit
</h1>
</header>
<form
onSubmit={[Function]}
<div
className="boxed-group boxed-group-inner"
>
<div
className="modal-field"
<form
onSubmit={[Function]}
>
<label
htmlFor="organization-name"
>
organization.name
<em
className="mandatory"
>
*
</em>
</label>
<input
disabled={true}
id="organization-name"
maxLength="64"
name="name"
onChange={[Function]}
required={true}
type="text"
value="New Foo"
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.name.description
<label
htmlFor="organization-name"
>
organization.name
<em
className="mandatory"
>
*
</em>
</label>
<input
disabled={true}
id="organization-name"
maxLength="64"
name="name"
onChange={[Function]}
required={true}
type="text"
value="New Foo"
/>
<div
className="modal-field-description"
>
organization.name.description
</div>
</div>
</div>
<div
className="modal-field"
>
<label
htmlFor="organization-avatar"
>
organization.avatar
</label>
<input
disabled={true}
id="organization-avatar"
maxLength="256"
name="avatar"
onChange={[Function]}
type="text"
value="foo-avatar"
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.avatar.description
<label
htmlFor="organization-avatar"
>
organization.avatar
</label>
<input
disabled={true}
id="organization-avatar"
maxLength="256"
name="avatar"
onChange={[Function]}
type="text"
value="foo-avatar"
/>
<div
className="modal-field-description"
>
organization.avatar.description
</div>
<div
className="spacer-top spacer-bottom"
>
<div
className="little-spacer-bottom"
>
organization.avatar.preview
:
</div>
<img
alt=""
height={30}
src="foo-avatar-image"
/>
</div>
</div>
<div
className="spacer-top spacer-bottom"
className="modal-field"
>
<label
htmlFor="organization-description"
>
description
</label>
<textarea
disabled={true}
id="organization-description"
maxLength="256"
name="description"
onChange={[Function]}
rows="3"
value="foo-description"
/>
<div
className="little-spacer-bottom"
className="modal-field-description"
>
organization.avatar.preview
:
organization.description.description
</div>
<img
alt=""
height={30}
src="foo-avatar-image"
/>
</div>
</div>
<div
className="modal-field"
>
<label
htmlFor="organization-description"
>
description
</label>
<textarea
disabled={true}
id="organization-description"
maxLength="256"
name="description"
onChange={[Function]}
rows="3"
value="foo-description"
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.description.description
<label
htmlFor="organization-url"
>
organization.url
</label>
<input
disabled={true}
id="organization-url"
maxLength="256"
name="url"
onChange={[Function]}
type="text"
value="foo-url"
/>
<div
className="modal-field-description"
>
organization.url.description
</div>
</div>
</div>
<div
className="modal-field"
>
<label
htmlFor="organization-url"
>
organization.url
</label>
<input
disabled={true}
id="organization-url"
maxLength="256"
name="url"
onChange={[Function]}
type="text"
value="foo-url"
/>
<div
className="modal-field-description"
className="modal-field"
>
organization.url.description
<button
disabled={true}
type="submit"
>
save
</button>
<i
className="spinner spacer-left"
/>
</div>
</div>
<div
className="modal-field"
>
<button
disabled={true}
type="submit"
>
save
</button>
<i
className="spinner spacer-left"
/>
</div>
</form>
</form>
</div>
</div>
`;

+ 26
- 6
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx View File

@@ -18,15 +18,21 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Organization } from '../../../app/types';
import { connect } from 'react-redux';
import { Organization, HomePageType } from '../../../app/types';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import { translate } from '../../../helpers/l10n';
import { getGlobalSettingValue } from '../../../store/rootReducer';

interface Props {
interface StateProps {
onSonarCloud: boolean;
}

interface Props extends StateProps {
organization: Organization;
}

export default function OrganizationNavigationMeta({ organization }: Props) {
export function OrganizationNavigationMeta({ onSonarCloud, organization }: Props) {
return (
<div className="navbar-context-meta">
{organization.url != null && (
@@ -41,9 +47,23 @@ export default function OrganizationNavigationMeta({ organization }: Props) {
<div className="text-muted">
<strong>{translate('organization.key')}:</strong> {organization.key}
</div>
<div className="navbar-context-meta-secondary">
<HomePageSelect currentPage={{ type: 'organization', key: organization.key }} />
</div>
{onSonarCloud && (
<div className="navbar-context-meta-secondary">
<HomePageSelect
currentPage={{ type: HomePageType.Organization, parameter: organization.key }}
/>
</div>
)}
</div>
);
}

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

return {
onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true')
};
};

export default connect(mapStateToProps)(OrganizationNavigationMeta);

+ 2
- 1
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMeta-test.tsx View File

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

it('renders', () => {
expect(
shallow(
<OrganizationNavigationMeta
onSonarCloud={true}
organization={{
key: 'foo',
name: 'Foo',

+ 1
- 1
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap View File

@@ -14,7 +14,7 @@ exports[`render 1`] = `
}
}
/>
<OrganizationNavigationMeta
<Connect(OrganizationNavigationMeta)
organization={
Object {
"key": "foo",

+ 2
- 2
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap View File

@@ -20,8 +20,8 @@ exports[`renders 1`] = `
<Connect(HomePageSelect)
currentPage={
Object {
"key": "foo",
"type": "organization",
"parameter": "foo",
"type": "ORGANIZATION",
}
}
/>

+ 0
- 8
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js View File

@@ -68,10 +68,6 @@ export default class OverviewApp extends React.PureComponent {

componentDidMount() {
this.mounted = true;
const domElement = document.querySelector('html');
if (domElement) {
domElement.classList.add('dashboard-page');
}
this.loadMeasures().then(this.loadHistory);
}

@@ -86,10 +82,6 @@ export default class OverviewApp extends React.PureComponent {

componentWillUnmount() {
this.mounted = false;
const domElement = document.querySelector('html');
if (domElement) {
domElement.classList.remove('dashboard-page');
}
}

loadMeasures() {

+ 6
- 4
server/sonar-web/src/main/js/apps/permission-templates/components/List.js View File

@@ -44,10 +44,12 @@ export default class List extends React.PureComponent {
));

return (
<table id="permission-templates" className="data zebra permissions-table">
<ListHeader organization={this.props.organization} permissions={this.props.permissions} />
<tbody>{permissionTemplates}</tbody>
</table>
<div className="boxed-group boxed-group-inner">
<table id="permission-templates" className="data zebra permissions-table">
<ListHeader organization={this.props.organization} permissions={this.props.permissions} />
<tbody>{permissionTemplates}</tbody>
</table>
</div>
);
}
}

+ 10
- 8
server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx View File

@@ -92,14 +92,16 @@ export default class HoldersList extends React.PureComponent<Props> {
));

return (
<table className="data zebra permissions-table">
{this.renderTableHeader()}
<tbody>
{users.length === 0 && groups.length === 0 && this.renderEmpty()}
{users}
{groups}
</tbody>
</table>
<div className="boxed-group boxed-group-inner">
<table className="data zebra permissions-table">
{this.renderTableHeader()}
<tbody>
{users.length === 0 && groups.length === 0 && this.renderEmpty()}
{users}
{groups}
</tbody>
</table>
</div>
);
}
}

+ 0
- 8
server/sonar-web/src/main/js/apps/portfolio/components/App.tsx View File

@@ -50,10 +50,6 @@ export default class App extends React.PureComponent<Props, State> {

componentDidMount() {
this.mounted = true;
const html = document.querySelector('html');
if (html) {
html.classList.add('dashboard-page');
}
this.fetchData();
}

@@ -65,10 +61,6 @@ export default class App extends React.PureComponent<Props, State> {

componentWillUnmount() {
this.mounted = false;
const html = document.querySelector('html');
if (html) {
html.classList.remove('dashboard-page');
}
}

fetchData() {

+ 1
- 1
server/sonar-web/src/main/js/apps/project-admin/key/Key.js View File

@@ -101,7 +101,7 @@ class Key extends React.PureComponent {
)}

{hasModules && (
<div>
<div className="boxed-group boxed-group-inner">
<div className="big-spacer-bottom">
<ul className="tabs">
<li>

+ 6
- 4
server/sonar-web/src/main/js/apps/project-admin/links/Table.js View File

@@ -54,10 +54,12 @@ export default class Table extends React.PureComponent {
));

return (
<table id="project-links" className="data zebra">
{this.renderHeader()}
<tbody>{linkRows}</tbody>
</table>
<div className="boxed-group boxed-group-inner">
<table id="project-links" className="data zebra">
{this.renderHeader()}
<tbody>{linkRows}</tbody>
</table>
</div>
);
}
}

+ 0
- 4
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js View File

@@ -94,8 +94,6 @@ export default class ProjectActivityAppContainer extends React.PureComponent {

componentDidMount() {
this.mounted = true;
const elem = document.querySelector('html');
elem && elem.classList.add('dashboard-page');
if (this.shouldRedirect()) {
const newQuery = { ...this.state.query, graph: getGraph() };
if (isCustomGraph(newQuery.graph)) {
@@ -129,8 +127,6 @@ export default class ProjectActivityAppContainer extends React.PureComponent {

componentWillUnmount() {
this.mounted = false;
const elem = document.querySelector('html');
elem && elem.classList.remove('dashboard-page');
}

addCustomEvent = (analysis /*: string */, name /*: string */, category /*: ?string */) =>

+ 24
- 20
server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx View File

@@ -119,26 +119,30 @@ export default class App extends React.PureComponent<Props, State> {
{this.renderBranchLifeTime()}
</header>

<table className="data zebra zebra-hover">
<thead>
<tr>
<th>{translate('branch')}</th>
<th className="thin nowrap text-right">{translate('status')}</th>
<th className="thin nowrap text-right">{translate('branches.last_analysis_date')}</th>
<th className="thin nowrap text-right">{translate('actions')}</th>
</tr>
</thead>
<tbody>
{sortBranchesAsTree(branches).map(branch => (
<BranchRow
branch={branch}
component={component.key}
key={branch.name}
onChange={onBranchesChange}
/>
))}
</tbody>
</table>
<div className="boxed-group boxed-group-inner">
<table className="data zebra zebra-hover">
<thead>
<tr>
<th>{translate('branch')}</th>
<th className="thin nowrap text-right">{translate('status')}</th>
<th className="thin nowrap text-right">
{translate('branches.last_analysis_date')}
</th>
<th className="thin nowrap text-right">{translate('actions')}</th>
</tr>
</thead>
<tbody>
{sortBranchesAsTree(branches).map(branch => (
<BranchRow
branch={branch}
component={component.key}
key={branch.name}
onChange={onBranchesChange}
/>
))}
</tbody>
</table>
</div>
</div>
);
}

+ 64
- 60
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -41,69 +41,73 @@ exports[`renders sorted list of branches 1`] = `
/>
</p>
</header>
<table
className="data zebra zebra-hover"
<div
className="boxed-group boxed-group-inner"
>
<thead>
<tr>
<th>
branch
</th>
<th
className="thin nowrap text-right"
>
status
</th>
<th
className="thin nowrap text-right"
>
branches.last_analysis_date
</th>
<th
className="thin nowrap text-right"
>
actions
</th>
</tr>
</thead>
<tbody>
<BranchRow
branch={
Object {
"isMain": true,
"name": "master",
<table
className="data zebra zebra-hover"
>
<thead>
<tr>
<th>
branch
</th>
<th
className="thin nowrap text-right"
>
status
</th>
<th
className="thin nowrap text-right"
>
branches.last_analysis_date
</th>
<th
className="thin nowrap text-right"
>
actions
</th>
</tr>
</thead>
<tbody>
<BranchRow
branch={
Object {
"isMain": true,
"name": "master",
}
}
}
component="foo"
key="master"
onChange={[Function]}
/>
<BranchRow
branch={
Object {
"isMain": false,
"mergeBranch": "master",
"name": "branch-1.0",
"type": "SHORT",
component="foo"
key="master"
onChange={[Function]}
/>
<BranchRow
branch={
Object {
"isMain": false,
"mergeBranch": "master",
"name": "branch-1.0",
"type": "SHORT",
}
}
}
component="foo"
key="branch-1.0"
onChange={[Function]}
/>
<BranchRow
branch={
Object {
"isMain": false,
"name": "branch-1.0",
"type": "LONG",
component="foo"
key="branch-1.0"
onChange={[Function]}
/>
<BranchRow
branch={
Object {
"isMain": false,
"name": "branch-1.0",
"type": "LONG",
}
}
}
component="foo"
key="branch-1.0"
onChange={[Function]}
/>
</tbody>
</table>
component="foo"
key="branch-1.0"
onChange={[Function]}
/>
</tbody>
</table>
</div>
</div>
`;

+ 13
- 11
server/sonar-web/src/main/js/apps/projectQualityProfiles/Table.tsx View File

@@ -44,16 +44,18 @@ export default function Table(props: Props) {
));

return (
<table className="data zebra">
<thead>
<tr>
<th className="thin nowrap">{translate('language')}</th>
<th className="thin nowrap">{translate('quality_profile')}</th>
{/* keep one empty cell for the spinner */}
<th>&nbsp;</th>
</tr>
</thead>
<tbody>{profileRows}</tbody>
</table>
<div className="boxed-group boxed-group-inner">
<table className="data zebra">
<thead>
<tr>
<th className="thin nowrap">{translate('language')}</th>
<th className="thin nowrap">{translate('quality_profile')}</th>
{/* keep one empty cell for the spinner */}
<th>&nbsp;</th>
</tr>
</thead>
<tbody>{profileRows}</tbody>
</table>
</div>
);
}

+ 82
- 78
server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/__snapshots__/Table-test.tsx.snap View File

@@ -1,32 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<table
className="data zebra"
<div
className="boxed-group boxed-group-inner"
>
<thead>
<tr>
<th
className="thin nowrap"
>
language
</th>
<th
className="thin nowrap"
>
quality_profile
</th>
<th>
 
</th>
</tr>
</thead>
<tbody>
<ProfileRow
key="java"
onChangeProfile={[Function]}
possibleProfiles={
Array [
<table
className="data zebra"
>
<thead>
<tr>
<th
className="thin nowrap"
>
language
</th>
<th
className="thin nowrap"
>
quality_profile
</th>
<th>
 
</th>
</tr>
</thead>
<tbody>
<ProfileRow
key="java"
onChangeProfile={[Function]}
possibleProfiles={
Array [
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 17,
"key": "foo-java",
"language": "java",
"languageName": "java",
"name": "foo-java",
"organization": "org",
},
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 17,
"key": "bar-java",
"language": "java",
"languageName": "java",
"name": "bar-java",
"organization": "org",
},
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 17,
"key": "baz-java",
"language": "java",
"languageName": "java",
"name": "baz-java",
"organization": "org",
},
]
}
profile={
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 17,
@@ -35,44 +68,26 @@ exports[`renders 1`] = `
"languageName": "java",
"name": "foo-java",
"organization": "org",
},
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 17,
"key": "bar-java",
"language": "java",
"languageName": "java",
"name": "bar-java",
"organization": "org",
},
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 17,
"key": "baz-java",
"language": "java",
"languageName": "java",
"name": "baz-java",
"organization": "org",
},
]
}
profile={
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 17,
"key": "foo-java",
"language": "java",
"languageName": "java",
"name": "foo-java",
"organization": "org",
}
}
/>
<ProfileRow
key="js"
onChangeProfile={[Function]}
possibleProfiles={
Array [
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 17,
"key": "foo-js",
"language": "js",
"languageName": "js",
"name": "foo-js",
"organization": "org",
},
]
}
}
/>
<ProfileRow
key="js"
onChangeProfile={[Function]}
possibleProfiles={
Array [
profile={
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 17,
@@ -81,21 +96,10 @@ exports[`renders 1`] = `
"languageName": "js",
"name": "foo-js",
"organization": "org",
},
]
}
profile={
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 17,
"key": "foo-js",
"language": "js",
"languageName": "js",
"name": "foo-js",
"organization": "org",
}
}
}
/>
</tbody>
</table>
/>
</tbody>
</table>
</div>
`;

+ 0
- 10
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx View File

@@ -70,11 +70,6 @@ export default class AllProjects extends React.PureComponent<Props, State> {
componentDidMount() {
this.mounted = true;

const html = document.querySelector('html');
if (html) {
html.classList.add('dashboard-page');
}

if (this.props.isFavorite && !isLoggedIn(this.props.currentUser)) {
handleRequiredAuthentication();
return;
@@ -95,11 +90,6 @@ export default class AllProjects extends React.PureComponent<Props, State> {
componentWillUnmount() {
this.mounted = false;

const html = document.querySelector('html');
if (html) {
html.classList.remove('dashboard-page');
}

const footer = document.getElementById('footer');
if (footer) {
footer.classList.remove('page-footer-with-sidebar');

+ 7
- 7
server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx View File

@@ -23,7 +23,7 @@ import SearchFilterContainer from '../filters/SearchFilterContainer';
import Tooltip from '../../../components/controls/Tooltip';
import PerspectiveSelect from './PerspectiveSelect';
import ProjectsSortingSelect from './ProjectsSortingSelect';
import { CurrentUser, isLoggedIn } from '../../../app/types';
import { CurrentUser, isLoggedIn, HomePageType } from '../../../app/types';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../helpers/query';
@@ -101,12 +101,12 @@ export default function PageHeader(props: Props) {
)}
</div>

{props.onSonarCloud &&
isLoggedIn(currentUser) &&
props.isFavorite &&
!props.organization && (
<HomePageSelect className="huge-spacer-left" currentPage={{ type: 'my-projects' }} />
)}
{props.isFavorite && (
<HomePageSelect
className="huge-spacer-left"
currentPage={{ type: HomePageType.MyProjects }}
/>
)}
</header>
);
}

+ 1
- 0
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx View File

@@ -49,6 +49,7 @@ export default function ProjectCardLeak({ organization, project }: Props) {
className="spacer-right"
component={project.key}
favorite={project.isFavorite}
qualifier="TRK"
/>
)}
<h2 className="project-card-name">

+ 1
- 0
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx View File

@@ -48,6 +48,7 @@ export default function ProjectCardOverall({ organization, project }: Props) {
className="spacer-right"
component={project.key}
favorite={project.isFavorite}
qualifier="TRK"
/>
)}
<h2 className="project-card-name">

+ 28
- 26
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx View File

@@ -50,32 +50,34 @@ export default class Projects extends React.PureComponent<Props> {

render() {
return (
<table
className={classNames('data', 'zebra', { 'new-loading': !this.props.ready })}
id="projects-management-page-projects">
<thead>
<tr>
<th />
<th>{translate('name')}</th>
<th />
<th>{translate('key')}</th>
<th className="thin nowrap text-right">{translate('last_analysis')}</th>
<th />
</tr>
</thead>
<tbody>
{this.props.projects.map(project => (
<ProjectRow
currentUser={this.props.currentUser}
key={project.key}
onApplyTemplate={this.handleApplyTemplate}
onProjectCheck={this.onProjectCheck}
project={project}
selected={this.props.selection.includes(project.key)}
/>
))}
</tbody>
</table>
<div className="boxed-group boxed-group-inner">
<table
className={classNames('data', 'zebra', { 'new-loading': !this.props.ready })}
id="projects-management-page-projects">
<thead>
<tr>
<th />
<th>{translate('name')}</th>
<th />
<th>{translate('key')}</th>
<th className="thin nowrap text-right">{translate('last_analysis')}</th>
<th />
</tr>
</thead>
<tbody>
{this.props.projects.map(project => (
<ProjectRow
currentUser={this.props.currentUser}
key={project.key}
onApplyTemplate={this.handleApplyTemplate}
onProjectCheck={this.onProjectCheck}
project={project}
selected={this.props.selection.includes(project.key)}
/>
))}
</tbody>
</table>
</div>
);
}
}

+ 62
- 58
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap View File

@@ -1,67 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders list of projects 1`] = `
<table
className="data zebra new-loading"
id="projects-management-page-projects"
<div
className="boxed-group boxed-group-inner"
>
<thead>
<tr>
<th />
<th>
name
</th>
<th />
<th>
key
</th>
<th
className="thin nowrap text-right"
>
last_analysis
</th>
<th />
</tr>
</thead>
<tbody>
<ProjectRow
currentUser={
Object {
"login": "foo",
<table
className="data zebra new-loading"
id="projects-management-page-projects"
>
<thead>
<tr>
<th />
<th>
name
</th>
<th />
<th>
key
</th>
<th
className="thin nowrap text-right"
>
last_analysis
</th>
<th />
</tr>
</thead>
<tbody>
<ProjectRow
currentUser={
Object {
"login": "foo",
}
}
}
key="a"
onApplyTemplate={[Function]}
onProjectCheck={[Function]}
project={
Object {
"key": "a",
"name": "A",
"qualifier": "TRK",
"visibility": "public",
key="a"
onApplyTemplate={[Function]}
onProjectCheck={[Function]}
project={
Object {
"key": "a",
"name": "A",
"qualifier": "TRK",
"visibility": "public",
}
}
}
selected={true}
/>
<ProjectRow
currentUser={
Object {
"login": "foo",
selected={true}
/>
<ProjectRow
currentUser={
Object {
"login": "foo",
}
}
}
key="b"
onApplyTemplate={[Function]}
onProjectCheck={[Function]}
project={
Object {
"key": "b",
"name": "B",
"qualifier": "TRK",
"visibility": "public",
key="b"
onApplyTemplate={[Function]}
onProjectCheck={[Function]}
project={
Object {
"key": "b",
"name": "B",
"qualifier": "TRK",
"visibility": "public",
}
}
}
selected={false}
/>
</tbody>
</table>
selected={false}
/>
</tbody>
</table>
</div>
`;

+ 4
- 0
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatesApp.js View File

@@ -37,6 +37,8 @@ export default class QualityGatesApp extends Component {

componentDidMount() {
this.fetchQualityGates();
// $FlowFixMe
document.body.classList.add('white-page');
const footer = document.getElementById('footer');
if (footer) {
footer.classList.add('page-footer-with-sidebar');
@@ -44,6 +46,8 @@ export default class QualityGatesApp extends Component {
}

componentWillUnmount() {
// $FlowFixMe
document.body.classList.remove('white-page');
const footer = document.getElementById('footer');
if (footer) {
footer.classList.remove('page-footer-with-sidebar');

+ 0
- 11
server/sonar-web/src/main/js/apps/quality-profiles/components/App.tsx View File

@@ -43,13 +43,6 @@ export default class App extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: true };

componentWillMount() {
const html = document.querySelector('html');
if (html) {
html.classList.add('dashboard-page');
}
}

componentDidMount() {
this.mounted = true;
this.loadData();
@@ -57,10 +50,6 @@ export default class App extends React.PureComponent<Props, State> {

componentWillUnmount() {
this.mounted = false;
const html = document.querySelector('html');
if (html) {
html.classList.remove('dashboard-page');
}
}

fetchProfiles() {

+ 0
- 11
server/sonar-web/src/main/js/apps/settings/components/App.js View File

@@ -47,10 +47,6 @@ export default class App extends React.PureComponent {
state /*: State */ = { loaded: false };

componentDidMount() {
const html = document.querySelector('html');
if (html) {
html.classList.add('dashboard-page');
}
const componentKey = this.props.component ? this.props.component.key : null;
this.props.fetchSettings(componentKey).then(() => this.setState({ loaded: true }));
}
@@ -62,13 +58,6 @@ export default class App extends React.PureComponent {
}
}

componentWillUnmount() {
const html = document.querySelector('html');
if (html) {
html.classList.remove('dashboard-page');
}
}

render() {
if (!this.state.loaded) {
return null;

+ 28
- 26
server/sonar-web/src/main/js/apps/users/UsersList.tsx View File

@@ -38,31 +38,33 @@ export default function UsersList({
users
}: Props) {
return (
<table id="users-list" className="data zebra">
<thead>
<tr>
<th />
<th className="nowrap" />
<th className="nowrap">{translate('my_profile.scm_accounts')}</th>
{!organizationsEnabled && <th className="nowrap">{translate('my_profile.groups')}</th>}
<th className="nowrap">{translate('users.tokens')}</th>
<th className="nowrap">&nbsp;</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<UserListItem
identityProvider={identityProviders.find(
provider => user.externalProvider === provider.key
)}
isCurrentUser={currentUser.isLoggedIn && currentUser.login === user.login}
key={user.login}
onUpdateUsers={onUpdateUsers}
organizationsEnabled={organizationsEnabled}
user={user}
/>
))}
</tbody>
</table>
<div className="boxed-group boxed-group-inner">
<table id="users-list" className="data zebra">
<thead>
<tr>
<th />
<th className="nowrap" />
<th className="nowrap">{translate('my_profile.scm_accounts')}</th>
{!organizationsEnabled && <th className="nowrap">{translate('my_profile.groups')}</th>}
<th className="nowrap">{translate('users.tokens')}</th>
<th className="nowrap">&nbsp;</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<UserListItem
identityProvider={identityProviders.find(
provider => user.externalProvider === provider.key
)}
isCurrentUser={currentUser.isLoggedIn && currentUser.login === user.login}
key={user.login}
onUpdateUsers={onUpdateUsers}
organizationsEnabled={organizationsEnabled}
user={user}
/>
))}
</tbody>
</table>
</div>
);
}

+ 61
- 57
server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList.tsx.snap View File

@@ -1,64 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<table
className="data zebra"
id="users-list"
<div
className="boxed-group boxed-group-inner"
>
<thead>
<tr>
<th />
<th
className="nowrap"
/>
<th
className="nowrap"
>
my_profile.scm_accounts
</th>
<th
className="nowrap"
>
users.tokens
</th>
<th
className="nowrap"
>
 
</th>
</tr>
</thead>
<tbody>
<UserListItem
isCurrentUser={true}
key="luke"
onUpdateUsers={[Function]}
organizationsEnabled={true}
user={
Object {
"active": true,
"local": false,
"login": "luke",
"name": "Luke",
"scmAccounts": Array [],
<table
className="data zebra"
id="users-list"
>
<thead>
<tr>
<th />
<th
className="nowrap"
/>
<th
className="nowrap"
>
my_profile.scm_accounts
</th>
<th
className="nowrap"
>
users.tokens
</th>
<th
className="nowrap"
>
 
</th>
</tr>
</thead>
<tbody>
<UserListItem
isCurrentUser={true}
key="luke"
onUpdateUsers={[Function]}
organizationsEnabled={true}
user={
Object {
"active": true,
"local": false,
"login": "luke",
"name": "Luke",
"scmAccounts": Array [],
}
}
}
/>
<UserListItem
isCurrentUser={false}
key="obi"
onUpdateUsers={[Function]}
organizationsEnabled={true}
user={
Object {
"active": true,
"local": false,
"login": "obi",
"name": "One",
"scmAccounts": Array [],
/>
<UserListItem
isCurrentUser={false}
key="obi"
onUpdateUsers={[Function]}
organizationsEnabled={true}
user={
Object {
"active": true,
"local": false,
"login": "obi",
"name": "One",
"scmAccounts": Array [],
}
}
}
/>
</tbody>
</table>
/>
</tbody>
</table>
</div>
`;

+ 21
- 19
server/sonar-web/src/main/js/apps/web-api/components/Action.tsx View File

@@ -120,7 +120,7 @@ export default class Action extends React.PureComponent<Props, State> {
);
}

return <hr />;
return null;
}

render() {
@@ -130,8 +130,8 @@ export default class Action extends React.PureComponent<Props, State> {
const actionKey = getActionKey(domain.path, action.key);

return (
<div id={actionKey} className="web-api-action">
<header className="web-api-action-header">
<div id={actionKey} className="boxed-group">
<header className="web-api-action-header boxed-group-header">
<Link
to={{ pathname: '/web_api/' + actionKey }}
className="spacer-right link-no-underline">
@@ -161,26 +161,28 @@ export default class Action extends React.PureComponent<Props, State> {
)}
</header>

<div
className="web-api-action-description markdown"
dangerouslySetInnerHTML={{ __html: action.description }}
/>
<div className="boxed-group-inner">
<div
className="web-api-action-description markdown"
dangerouslySetInnerHTML={{ __html: action.description }}
/>

{this.renderTabs()}
{this.renderTabs()}

{showParams &&
action.params && (
<Params
params={action.params}
showDeprecated={this.props.showDeprecated}
showInternal={this.props.showInternal}
/>
)}
{showParams &&
action.params && (
<Params
params={action.params}
showDeprecated={this.props.showDeprecated}
showInternal={this.props.showInternal}
/>
)}

{showResponse &&
action.hasResponseExample && <ResponseExample domain={domain} action={action} />}
{showResponse &&
action.hasResponseExample && <ResponseExample domain={domain} action={action} />}

{showChangelog && <ActionChangelog changelog={action.changelog} />}
{showChangelog && <ActionChangelog changelog={action.changelog} />}
</div>
</div>
);
}

+ 43
- 39
server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap View File

@@ -91,11 +91,11 @@ exports[`should display the response example 1`] = `

exports[`should render correctly 1`] = `
<div
className="web-api-action"
className="boxed-group"
id="foo/foo"
>
<header
className="web-api-action-header"
className="web-api-action-header boxed-group-header"
>
<Link
className="spacer-right link-no-underline"
@@ -118,43 +118,47 @@ exports[`should render correctly 1`] = `
</h3>
</header>
<div
className="web-api-action-description markdown"
dangerouslySetInnerHTML={
Object {
"__html": "Foo Desc",
}
}
/>
<ul
className="web-api-action-actions tabs"
className="boxed-group-inner"
>
<li>
<a
className=""
href="#"
onClick={[Function]}
>
api_documentation.parameters
</a>
</li>
<li>
<a
className=""
href="#"
onClick={[Function]}
>
api_documentation.response_example
</a>
</li>
<li>
<a
className=""
href="#"
onClick={[Function]}
>
api_documentation.changelog
</a>
</li>
</ul>
<div
className="web-api-action-description markdown"
dangerouslySetInnerHTML={
Object {
"__html": "Foo Desc",
}
}
/>
<ul
className="web-api-action-actions tabs"
>
<li>
<a
className=""
href="#"
onClick={[Function]}
>
api_documentation.parameters
</a>
</li>
<li>
<a
className=""
href="#"
onClick={[Function]}
>
api_documentation.response_example
</a>
</li>
<li>
<a
className=""
href="#"
onClick={[Function]}
>
api_documentation.changelog
</a>
</li>
</ul>
</div>
</div>
`;

+ 1
- 4
server/sonar-web/src/main/js/apps/web-api/styles/web-api.css View File

@@ -26,10 +26,7 @@
}

.web-api-domain-actions {
}

.web-api-action {
padding-top: 30px;
margin-top: calc(2 * var(--gridSize));
}

.web-api-action-title {

+ 1
- 1
server/sonar-web/src/main/js/components/SourceViewer/styles.css View File

@@ -20,7 +20,7 @@
.source-viewer {
width: 100%;
min-height: 200px;
border: 1px solid var(--barBorderColor);
border: 1px solid var(--gray80);
box-sizing: border-box;
background-color: #fff;
overflow-x: auto;

+ 3
- 0
server/sonar-web/src/main/js/components/common/BranchStatus.css View File

@@ -1,5 +1,8 @@
.branch-status {
display: flex;
align-items: center;
min-width: 64px;
line-height: calc(2 * var(--gridSize));
text-align: right;
}


+ 5
- 5
server/sonar-web/src/main/js/components/common/BranchStatus.tsx View File

@@ -44,26 +44,26 @@ export default function BranchStatus({ branch, concise = false }: Props) {
const indicatorColor = totalIssues > 0 ? 'red' : 'green';

return concise ? (
<ul className="list-inline branch-status">
<ul className="branch-status">
<li>{totalIssues}</li>
<li className="spacer-left">
<StatusIndicator color={indicatorColor} size="small" />
</li>
</ul>
) : (
<ul className="list-inline branch-status">
<ul className="branch-status">
<li className="spacer-right">
<StatusIndicator color={indicatorColor} size="small" />
</li>
<li>
<li className="spacer-left">
{branch.status.bugs}
<BugIcon />
</li>
<li>
<li className="spacer-left">
{branch.status.vulnerabilities}
<VulnerabilityIcon />
</li>
<li>
<li className="spacer-left">
{branch.status.codeSmells}
<CodeSmellIcon />
</li>

+ 30
- 12
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap View File

@@ -16,7 +16,7 @@ exports[`renders status of long-living branches 2`] = `

exports[`renders status of short-living branches 1`] = `
<ul
className="list-inline branch-status"
className="branch-status"
>
<li
className="spacer-right"
@@ -26,15 +26,21 @@ exports[`renders status of short-living branches 1`] = `
size="small"
/>
</li>
<li>
<li
className="spacer-left"
>
0
<BugIcon />
</li>
<li>
<li
className="spacer-left"
>
0
<VulnerabilityIcon />
</li>
<li>
<li
className="spacer-left"
>
0
<CodeSmellIcon />
</li>
@@ -43,7 +49,7 @@ exports[`renders status of short-living branches 1`] = `

exports[`renders status of short-living branches 2`] = `
<ul
className="list-inline branch-status"
className="branch-status"
>
<li
className="spacer-right"
@@ -53,15 +59,21 @@ exports[`renders status of short-living branches 2`] = `
size="small"
/>
</li>
<li>
<li
className="spacer-left"
>
0
<BugIcon />
</li>
<li>
<li
className="spacer-left"
>
0
<VulnerabilityIcon />
</li>
<li>
<li
className="spacer-left"
>
1
<CodeSmellIcon />
</li>
@@ -70,7 +82,7 @@ exports[`renders status of short-living branches 2`] = `

exports[`renders status of short-living branches 3`] = `
<ul
className="list-inline branch-status"
className="branch-status"
>
<li
className="spacer-right"
@@ -80,15 +92,21 @@ exports[`renders status of short-living branches 3`] = `
size="small"
/>
</li>
<li>
<li
className="spacer-left"
>
7
<BugIcon />
</li>
<li>
<li
className="spacer-left"
>
6
<VulnerabilityIcon />
</li>
<li>
<li
className="spacer-left"
>
3
<CodeSmellIcon />
</li>

+ 2
- 2
server/sonar-web/src/main/js/components/controls/Favorite.tsx View File

@@ -25,13 +25,13 @@ interface Props {
className?: string;
component: string;
favorite: boolean;
qualifier: string;
}

export default function Favorite({ favorite, component, ...other }: Props) {
export default function Favorite({ component, ...other }: Props) {
return (
<FavoriteBase
{...other}
favorite={favorite}
addFavorite={() => addFavorite(component)}
removeFavorite={() => removeFavorite(component)}
/>

+ 5
- 4
server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx View File

@@ -23,10 +23,11 @@ import Tooltip from './Tooltip';
import FavoriteIcon from '../icons-components/FavoriteIcon';
import { translate } from '../../helpers/l10n';

interface Props {
export interface Props {
addFavorite: () => Promise<void>;
className?: string;
favorite: boolean;
qualifier: string;
removeFavorite: () => Promise<void>;
}

@@ -83,10 +84,10 @@ export default class FavoriteBase extends React.PureComponent<Props, State> {

render() {
const tooltip = this.state.favorite
? translate('favorite.current')
: translate('favorite.check');
? translate('favorite.current', this.props.qualifier)
: translate('favorite.check', this.props.qualifier);
return (
<Tooltip overlay={tooltip}>
<Tooltip overlay={tooltip} placement="left">
<a
className={classNames('display-inline-block', 'link-no-underline', this.props.className)}
href="#"

+ 0
- 42
server/sonar-web/src/main/js/components/controls/FavoriteIssueFilter.js View File

@@ -1,42 +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.
*/
import React from 'react';
import PropTypes from 'prop-types';
import FavoriteBase from './FavoriteBase';
import { toggleIssueFilter } from '../../api/issue-filters';

export default class FavoriteIssueFilter extends React.PureComponent {
static propTypes = {
favorite: PropTypes.bool.isRequired,
filter: PropTypes.shape({
id: PropTypes.string.isRequired
}).isRequired
};

render() {
return (
<FavoriteBase
favorite={this.props.favorite}
addFavorite={() => toggleIssueFilter(this.props.filter.id)}
removeFavorite={() => toggleIssueFilter(this.props.filter.id)}
/>
);
}
}

+ 13
- 7
server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx View File

@@ -24,11 +24,12 @@ import Tooltip from './Tooltip';
import HomeIcon from '../icons-components/HomeIcon';
import { CurrentUser, isLoggedIn, HomePage, isSameHomePage } from '../../app/types';
import { translate } from '../../helpers/l10n';
import { getCurrentUser } from '../../store/rootReducer';
import { getCurrentUser, getGlobalSettingValue } from '../../store/rootReducer';
import { setHomePage } from '../../store/users/actions';

interface StateProps {
currentUser: CurrentUser;
onSonarCloud: boolean;
}

interface DispatchProps {
@@ -48,9 +49,9 @@ class HomePageSelect extends React.PureComponent<Props> {
};

render() {
const { currentPage, currentUser } = this.props;
const { currentPage, currentUser, onSonarCloud } = this.props;

if (!isLoggedIn(currentUser)) {
if (!isLoggedIn(currentUser) || !onSonarCloud) {
return null;
}

@@ -59,7 +60,7 @@ class HomePageSelect extends React.PureComponent<Props> {
const tooltip = checked ? translate('homepage.current') : translate('homepage.check');

return (
<Tooltip overlay={tooltip}>
<Tooltip overlay={tooltip} placement="left">
{checked ? (
<span className={classNames('display-inline-block', this.props.className)}>
<HomeIcon filled={checked} />
@@ -81,9 +82,14 @@ class HomePageSelect extends React.PureComponent<Props> {
}
}

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

return {
currentUser: getCurrentUser(state),
onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true')
};
};

const mapDispatchToProps: DispatchProps = { setHomePage };


+ 1
- 1
server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx View File

@@ -22,5 +22,5 @@ import { shallow } from 'enzyme';
import Favorite from '../Favorite';

it('renders', () => {
expect(shallow(<Favorite component="foo" favorite={true} />)).toMatchSnapshot();
expect(shallow(<Favorite component="foo" favorite={true} qualifier="TRK" />)).toMatchSnapshot();
});

+ 9
- 3
server/sonar-web/src/main/js/components/controls/__tests__/FavoriteBase-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import FavoriteBase from '../FavoriteBase';
import FavoriteBase, { Props } from '../FavoriteBase';
import { click } from '../../../helpers/testUtils';

it('should render favorite', () => {
@@ -46,8 +46,14 @@ it('should remove favorite', () => {
expect(removeFavorite).toBeCalled();
});

function renderFavoriteBase(props?: any) {
function renderFavoriteBase(props: Partial<Props> = {}) {
return shallow(
<FavoriteBase favorite={true} addFavorite={jest.fn()} removeFavorite={jest.fn()} {...props} />
<FavoriteBase
favorite={true}
addFavorite={jest.fn()}
qualifier="TRK"
removeFavorite={jest.fn()}
{...props}
/>
);
}

+ 1
- 0
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Favorite-test.tsx.snap View File

@@ -4,6 +4,7 @@ exports[`renders 1`] = `
<FavoriteBase
addFavorite={[Function]}
favorite={true}
qualifier="TRK"
removeFavorite={[Function]}
/>
`;

+ 4
- 4
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap View File

@@ -2,8 +2,8 @@

exports[`should render favorite 1`] = `
<Tooltip
overlay="favorite.current"
placement="bottom"
overlay="favorite.current.TRK"
placement="left"
>
<a
className="display-inline-block link-no-underline"
@@ -19,8 +19,8 @@ exports[`should render favorite 1`] = `

exports[`should render not favorite 1`] = `
<Tooltip
overlay="favorite.check"
placement="bottom"
overlay="favorite.check.TRK"
placement="left"
>
<a
className="display-inline-block link-no-underline"

+ 3
- 2
server/sonar-web/src/main/js/components/nav/ContextNavBar.css View File

@@ -1,6 +1,6 @@
.navbar-context,
.navbar-context .navbar-inner {
background-color: var(--barBackgroundColor);
background-color: #fff;
z-index: 420;
}

@@ -14,7 +14,7 @@
}

.navbar-context-header {
display: inline-flex;
display: flex;
align-items: center;
height: calc(4 * var(--gridSize));
font-size: var(--bigFontSize);
@@ -48,6 +48,7 @@
top: 36px;
right: 0;
padding: 0 20px;
white-space: nowrap;
}

.navbar-context-description {

+ 7
- 7
server/sonar-web/src/main/js/helpers/urls.ts View File

@@ -21,7 +21,7 @@ import { stringify } from 'querystring';
import { omitBy, isNil } from 'lodash';
import { isShortLivingBranch } from './branches';
import { getProfilePath } from '../apps/quality-profiles/utils';
import { Branch, HomePage } from '../app/types';
import { Branch, HomePage, HomePageType } from '../app/types';

interface Query {
[x: string]: string | undefined;
@@ -174,13 +174,13 @@ export function getOrganizationUrl(organization: string) {

export function getHomePageUrl(homepage: HomePage) {
switch (homepage.type) {
case 'project':
return getProjectUrl(homepage.key!);
case 'organization':
return getOrganizationUrl(homepage.key!);
case 'my-projects':
case HomePageType.Project:
return getProjectUrl(homepage.parameter!);
case HomePageType.Organization:
return getOrganizationUrl(homepage.parameter!);
case HomePageType.MyProjects:
return '/projects';
case 'my-issues':
case HomePageType.MyIssues:
return { pathname: '/issues', query: { resolved: 'false' } };
}


+ 22
- 3
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -1318,7 +1318,7 @@ email_configuration.test.email_was_sent_to_x=Email was sent to {0}
#
#------------------------------------------------------------------------------
notification.channel.EmailNotificationChannel=Email
notification.dispatcher.information=Receive notifications when specific types of events occur. A notification is never sent to the author of the event.
notification.dispatcher.information=A notification is never sent to the author of the event.
notification.dispatcher.ChangesOnMyIssue=Changes in issues assigned to me
notification.dispatcher.NewIssues=New issues
notification.dispatcher.NewAlerts=New quality gate status
@@ -2736,5 +2736,24 @@ homepage.check=Check to make the current page your homepage
# FAVORITE
#
#------------------------------------------------------------------------------
favorite.current=This is your favorite component. Click to unset.
favorite.check=Click to mark this component as favorite.
favorite.check.TRK=Click to mark this project as favorite.
favorite.check.BRC=Click to mark this sub-project as favorite.
favorite.check.DIR=Click to mark this directory as favorite.
favorite.check.PAC=Click to mark this package as favorite.
favorite.check.VW=Click to mark this portfolio as favorite.
favorite.check.SVW=Click to mark this sub-ortfolio as favorite.
favorite.check.APP=Click to mark this application as favorite.
favorite.check.FIL=Click to mark this file as favorite.
favorite.check.CLA=Click to mark this file as favorite.
favorite.check.UTS=Click to mark this test file as favorite.

favorite.current.TRK=This project is marked as favorite.
favorite.current.BRC=This sub-project is marked as favorite.
favorite.current.DIR=This directory is marked as favorite.
favorite.current.PAC=This package is marked as favorite.
favorite.current.VW=This portfolio is marked as favorite.
favorite.current.SVW=This sub-ortfolio is marked as favorite.
favorite.current.APP=This application is marked as favorite.
favorite.current.FIL=This file is marked as favorite.
favorite.current.CLA=This file is marked as favorite.
favorite.current.UTS=This test file is marked as favorite.

Loading…
Cancel
Save