Browse Source

Update prettier to last version

* Prettier format all modules
tags/7.8
Grégoire Aubert 5 years ago
parent
commit
1355d080cf
100 changed files with 2964 additions and 1159 deletions
  1. 1
    1
      server/sonar-docs/package.json
  2. 1
    1
      server/sonar-vsts/package.json
  3. 3
    2
      server/sonar-web/package.json
  4. 2
    1
      server/sonar-web/scripts/utils/getMessages.js
  5. 14
    15
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  6. 8
    8
      server/sonar-web/src/main/js/app/components/GlobalFooter.tsx
  7. 1
    1
      server/sonar-web/src/main/js/app/components/__tests__/GlobalContainer-test.tsx
  8. 5
    6
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
  9. 26
    28
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
  10. 13
    15
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
  11. 20
    24
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
  12. 7
    8
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
  13. 11
    13
      server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx
  14. 28
    29
      server/sonar-web/src/main/js/app/components/search/Search.tsx
  15. 2
    2
      server/sonar-web/src/main/js/apps/about/routes.ts
  16. 5
    6
      server/sonar-web/src/main/js/apps/account/profile/Profile.tsx
  17. 8
    9
      server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
  18. 5
    7
      server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx
  19. 19
    22
      server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx
  20. 24
    26
      server/sonar-web/src/main/js/apps/code/components/App.tsx
  21. 3
    4
      server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx
  22. 26
    29
      server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx
  23. 20
    21
      server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx
  24. 3
    2
      server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx
  25. 15
    17
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx
  26. 11
    9
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx
  27. 11
    36
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx
  28. 30
    31
      server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx
  29. 64
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChange-test.tsx
  30. 98
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx
  31. 99
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChange-test.tsx.snap
  32. 204
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap
  33. 41
    56
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
  34. 12
    14
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
  35. 3
    8
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
  36. 96
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx
  37. 312
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap
  38. 22
    25
      server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx
  39. 19
    20
      server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx
  40. 11
    12
      server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
  41. 5
    6
      server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
  42. 40
    42
      server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx
  43. 29
    32
      server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx
  44. 10
    11
      server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx
  45. 8
    9
      server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx
  46. 16
    19
      server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx
  47. 2
    3
      server/sonar-web/src/main/js/apps/documentation/utils.ts
  48. 10
    11
      server/sonar-web/src/main/js/apps/groups/components/App.tsx
  49. 74
    5
      server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx
  50. 1
    1
      server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap
  51. 23
    27
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  52. 6
    8
      server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx
  53. 2
    2
      server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx
  54. 2
    2
      server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx
  55. 31
    34
      server/sonar-web/src/main/js/apps/marketplace/components/PluginActions.tsx
  56. 9
    10
      server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx
  57. 8
    9
      server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdates.tsx
  58. 57
    0
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginChangeLog-test.tsx
  59. 72
    0
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginChangeLog-test.tsx.snap
  60. 30
    31
      server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
  61. 8
    9
      server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx
  62. 24
    28
      server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
  63. 24
    25
      server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
  64. 8
    9
      server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
  65. 3
    2
      server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx
  66. 15
    18
      server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
  67. 9
    8
      server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx
  68. 5
    6
      server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.tsx
  69. 5
    3
      server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx
  70. 16
    20
      server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx
  71. 134
    0
      server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/App-test.tsx
  72. 136
    0
      server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/__snapshots__/App-test.tsx.snap
  73. 23
    28
      server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx
  74. 162
    0
      server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx
  75. 224
    0
      server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap
  76. 9
    10
      server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx
  77. 7
    8
      server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
  78. 16
    18
      server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx
  79. 2
    2
      server/sonar-web/src/main/js/apps/projectActivity/actions.ts
  80. 3
    4
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx
  81. 37
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityEventSelectOption-test.tsx
  82. 19
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityEventSelectOption-test.tsx.snap
  83. 15
    17
      server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx
  84. 14
    16
      server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx
  85. 12
    15
      server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
  86. 2
    2
      server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
  87. 10
    8
      server/sonar-web/src/main/js/apps/projects/utils.ts
  88. 5
    7
      server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
  89. 18
    20
      server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx
  90. 3
    4
      server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx
  91. 3
    5
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx
  92. 7
    11
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx
  93. 7
    8
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx
  94. 26
    38
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx
  95. 15
    16
      server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx
  96. 15
    18
      server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
  97. 5
    6
      server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx
  98. 89
    0
      server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx
  99. 91
    0
      server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap
  100. 0
    0
      server/sonar-web/src/main/js/apps/settings/encryption/EncryptionApp.tsx

+ 1
- 1
server/sonar-docs/package.json View File

@@ -47,7 +47,7 @@
"glob-promise": "3.4.0",
"graphql-code-generator": "0.5.2",
"jest": "24.5.0",
"prettier": "1.16.0",
"prettier": "1.16.4",
"react-test-renderer": "16.8.5",
"remark": "10.0.1",
"ts-jest": "24.0.0",

+ 1
- 1
server/sonar-vsts/package.json View File

@@ -57,7 +57,7 @@
"postcss-calc": "7.0.1",
"postcss-custom-properties": "8.0.9",
"postcss-loader": "3.0.0",
"prettier": "1.14.3",
"prettier": "1.16.4",
"react-dev-utils": "5.0.0",
"react-error-overlay": "1.0.7",
"react-test-renderer": "16.8.5",

+ 3
- 2
server/sonar-web/package.json View File

@@ -114,7 +114,7 @@
"postcss-calc": "7.0.1",
"postcss-custom-properties": "8.0.9",
"postcss-loader": "3.0.0",
"prettier": "1.14.3",
"prettier": "1.16.4",
"raw-loader": "2.0.0",
"react-dev-utils": "5.0.1",
"react-error-overlay": "1.0.7",
@@ -158,7 +158,8 @@
"src/main/js/**/*.{ts,tsx,js}"
],
"coverageReporters": [
"lcovonly"
"lcovonly",
"text"
],
"globals": {
"ts-jest": {

+ 2
- 1
server/sonar-web/scripts/utils/getMessages.js View File

@@ -28,7 +28,8 @@ const filename = '../../../../sonar-core/src/main/resources/org/sonar/l10n/core.
const extensionsFilenames = [
'../../../../private/core-extension-billing/src/main/resources/org/sonar/l10n/billing.properties',
'../../../../private/core-extension-governance/src/main/resources/org/sonar/l10n/governance.properties',
'../../../../private/core-extension-license/src/main/resources/org/sonar/l10n/license.properties'
'../../../../private/core-extension-license/src/main/resources/org/sonar/l10n/license.properties',
'../../../../private/core-extension-developer-server/src/main/resources/org/sonar/l10n/developer-server.properties'
];

function getFileMessage(filename) {

+ 14
- 15
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx View File

@@ -45,7 +45,7 @@ import { isSonarCloud } from '../../helpers/system';
import { withRouter, Router, Location } from '../../components/hoc/withRouter';

interface Props {
children: React.ReactElement<any>;
children: React.ReactElement;
fetchOrganization: (organization: string) => void;
location: Pick<Location, 'query'>;
registerBranchStatus: (branchLike: T.BranchLike, component: string, status: T.Status) => void;
@@ -319,20 +319,19 @@ export class ComponentContainer extends React.PureComponent<Props, State> {

return (
<div>
{component &&
!['FIL', 'UTS'].includes(component.qualifier) && (
<ComponentNav
branchLikes={branchLikes}
component={component}
currentBranchLike={branchLike}
currentTask={currentTask}
currentTaskOnSameBranch={currentTask && this.isSameBranch(currentTask, branchLike)}
isInProgress={isInProgress}
isPending={isPending}
location={this.props.location}
warnings={this.state.warnings}
/>
)}
{component && !['FIL', 'UTS'].includes(component.qualifier) && (
<ComponentNav
branchLikes={branchLikes}
component={component}
currentBranchLike={branchLike}
currentTask={currentTask}
currentTaskOnSameBranch={currentTask && this.isSameBranch(currentTask, branchLike)}
isInProgress={isInProgress}
isPending={isPending}
location={this.props.location}
warnings={this.state.warnings}
/>
)}
{loading ? (
<div className="page page-limited">
<i className="spinner" />

+ 8
- 8
server/sonar-web/src/main/js/app/components/GlobalFooter.tsx View File

@@ -60,14 +60,14 @@ export default function GlobalFooter({
<GlobalFooterBranding />

<ul className="page-footer-menu">
{!hideLoggedInInfo &&
currentEdition && <li className="page-footer-menu-item">{currentEdition.name}</li>}
{!hideLoggedInInfo &&
sonarqubeVersion && (
<li className="page-footer-menu-item">
{translateWithParameters('footer.version_x', sonarqubeVersion)}
</li>
)}
{!hideLoggedInInfo && currentEdition && (
<li className="page-footer-menu-item">{currentEdition.name}</li>
)}
{!hideLoggedInInfo && sonarqubeVersion && (
<li className="page-footer-menu-item">
{translateWithParameters('footer.version_x', sonarqubeVersion)}
</li>
)}
<li className="page-footer-menu-item">
<a href="http://www.gnu.org/licenses/lgpl-3.0.txt">{translate('footer.license')}</a>
</li>

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

@@ -28,7 +28,7 @@ jest.mock('../embed-docs-modal/SuggestionsProvider', () => {
return this.props.children;
}
}
return { default: SuggestionsProvider };
});


+ 5
- 6
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx View File

@@ -154,12 +154,11 @@ export class ComponentNavBranch extends React.PureComponent<Props, State> {
fill={theme.gray80}
/>
<span className="note">{displayName}</span>
{configuration &&
configuration.showSettings && (
<HelpTooltip className="spacer-left" overlay={this.renderOverlay()}>
<PlusCircleIcon className="vertical-middle" fill={theme.blue} size={12} />
</HelpTooltip>
)}
{configuration && configuration.showSettings && (
<HelpTooltip className="spacer-left" overlay={this.renderOverlay()}>
<PlusCircleIcon className="vertical-middle" fill={theme.blue} size={12} />
</HelpTooltip>
)}
</div>
);
} else {

+ 26
- 28
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx View File

@@ -53,38 +53,36 @@ export function ComponentNavHeader(props: Props) {
organization={organization && isSonarCloud() ? organization : undefined}
title={component.name}
/>
{organization &&
isSonarCloud() && (
<>
<OrganizationAvatar organization={organization} />
<OrganizationLink
className="navbar-context-header-breadcrumb-link link-base-color link-no-underline spacer-left"
organization={organization}>
{organization.name}
</OrganizationLink>
<span className="slash-separator" />
</>
)}
{organization && isSonarCloud() && (
<>
<OrganizationAvatar organization={organization} />
<OrganizationLink
className="navbar-context-header-breadcrumb-link link-base-color link-no-underline spacer-left"
organization={organization}>
{organization.name}
</OrganizationLink>
<span className="slash-separator" />
</>
)}
{renderBreadcrumbs(
component.breadcrumbs,
props.currentBranchLike !== undefined && !isMainBranch(props.currentBranchLike)
)}
{isSonarCloud() &&
component.alm && (
<a
className="link-no-underline"
href={component.alm.url}
rel="noopener noreferrer"
target="_blank">
<img
alt={sanitizeAlmId(component.alm.key)}
className="text-text-top spacer-left"
height={16}
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(component.alm.key)}.svg`}
width={16}
/>
</a>
)}
{isSonarCloud() && component.alm && (
<a
className="link-no-underline"
href={component.alm.url}
rel="noopener noreferrer"
target="_blank">
<img
alt={sanitizeAlmId(component.alm.key)}
className="text-text-top spacer-left"
height={16}
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(component.alm.key)}.svg`}
width={16}
/>
</a>
)}
{props.currentBranchLike && (
<ComponentNavBranch
branchLikes={props.branchLikes}

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

@@ -76,25 +76,23 @@ export function ComponentNavMeta({ branchLike, component, currentUser, warnings
qualifier={component.qualifier}
/>
)}
{(mainBranch || longBranch) &&
currentPage !== undefined && (
<HomePageSelect className="spacer-left" currentPage={currentPage} />
)}
{(mainBranch || longBranch) && currentPage !== undefined && (
<HomePageSelect className="spacer-left" currentPage={currentPage} />
)}
</div>
)}
{(isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && (
<div className="navbar-context-meta-secondary display-inline-flex-center">
{isPullRequest(branchLike) &&
branchLike.url !== undefined && (
<a
className="display-inline-flex-center big-spacer-right"
href={branchLike.url}
rel="noopener noreferrer"
target="_blank">
{translate('branches.see_the_pr')}
<DetachIcon className="little-spacer-left" size={12} />
</a>
)}
{isPullRequest(branchLike) && branchLike.url !== undefined && (
<a
className="display-inline-flex-center big-spacer-right"
href={branchLike.url}
rel="noopener noreferrer"
target="_blank">
{translate('branches.see_the_pr')}
<DetachIcon className="little-spacer-left" size={12} />
</a>
)}
<BranchStatus branchLike={branchLike} component={component.key} />
</div>
)}

+ 20
- 24
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx View File

@@ -180,17 +180,15 @@ export class GlobalNav extends React.PureComponent<Props, State> {
<GlobalNavMenu {...this.props} />

<ul className="global-navbar-menu global-navbar-menu-right">
{isSonarCloud() &&
isLoggedIn(currentUser) &&
news.length > 0 && (
<NavLatestNotification
lastNews={news[0]}
notificationsLastReadDate={this.props.notificationsLastReadDate}
notificationsOptOut={this.props.notificationsOptOut}
onClick={this.handleOpenNotificationSidebar}
setCurrentUserSetting={this.props.setCurrentUserSetting}
/>
)}
{isSonarCloud() && isLoggedIn(currentUser) && news.length > 0 && (
<NavLatestNotification
lastNews={news[0]}
notificationsLastReadDate={this.props.notificationsLastReadDate}
notificationsOptOut={this.props.notificationsOptOut}
onClick={this.handleOpenNotificationSidebar}
setCurrentUserSetting={this.props.setCurrentUserSetting}
/>
)}
{isSonarCloud() && <GlobalNavExplore location={this.props.location} />}
<EmbedDocsPopupHelper />
<Search appState={appState} currentUser={currentUser} />
@@ -207,19 +205,17 @@ export class GlobalNav extends React.PureComponent<Props, State> {
)}
<GlobalNavUserContainer appState={appState} currentUser={currentUser} />
</ul>
{isSonarCloud() &&
isLoggedIn(currentUser) &&
this.state.notificationSidebar && (
<NotificationsSidebar
fetchMoreFeatureNews={this.fetchMoreFeatureNews}
loading={this.state.loadingNews}
loadingMore={this.state.loadingMoreNews}
news={news}
notificationsLastReadDate={this.props.notificationsLastReadDate}
onClose={this.handleCloseNotificationSidebar}
paging={this.state.newsPaging}
/>
)}
{isSonarCloud() && isLoggedIn(currentUser) && this.state.notificationSidebar && (
<NotificationsSidebar
fetchMoreFeatureNews={this.fetchMoreFeatureNews}
loading={this.state.loadingNews}
loadingMore={this.state.loadingMoreNews}
news={news}
notificationsLastReadDate={this.props.notificationsLastReadDate}
onClose={this.handleCloseNotificationSidebar}
paging={this.state.newsPaging}
/>
)}
</NavBar>
);
}

+ 7
- 8
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx View File

@@ -188,14 +188,13 @@ export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps,
<PlusIcon />
</a>
</Dropdown>
{this.state.governanceReady &&
this.state.createPortfolio && (
<CreateFormShim
defaultQualifier={defaultQualifier}
onClose={this.closeCreatePortfolioForm}
onCreate={this.handleCreatePortfolio}
/>
)}
{this.state.governanceReady && this.state.createPortfolio && (
<CreateFormShim
defaultQualifier={defaultQualifier}
onClose={this.closeCreatePortfolioForm}
onCreate={this.handleCreatePortfolio}
/>
)}
</>
);
}

+ 11
- 13
server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx View File

@@ -63,20 +63,18 @@ export default function NotificationsSidebar(props: Props) {
))
)}
</div>
{!loading &&
paging &&
paging.total > news.length && (
<div className="notifications-sidebar-footer">
<div className="spacer-top note text-center">
<a className="spacer-left" href="#" onClick={props.fetchMoreFeatureNews}>
{translate('show_more')}
</a>
{loadingMore && (
<DeferredSpinner className="vertical-bottom spacer-left position-absolute" />
)}
</div>
{!loading && paging && paging.total > news.length && (
<div className="notifications-sidebar-footer">
<div className="spacer-top note text-center">
<a className="spacer-left" href="#" onClick={props.fetchMoreFeatureNews}>
{translate('show_more')}
</a>
{loadingMore && (
<DeferredSpinner className="vertical-bottom spacer-left position-absolute" />
)}
</div>
)}
</div>
)}
</div>
</Modal>
);

+ 28
- 29
server/sonar-web/src/main/js/app/components/search/Search.tsx View File

@@ -376,37 +376,36 @@ export class Search extends React.PureComponent<Props, State> {
</span>
)}

{this.state.open &&
Object.keys(this.state.results).length > 0 && (
<DropdownOverlay noPadding={true}>
<div className="global-navbar-search-dropdown" ref={node => (this.node = node)}>
<SearchResults
allowMore={this.state.query.length !== 1}
loadingMore={this.state.loadingMore}
more={this.state.more}
onMoreClick={this.searchMore}
onSelect={this.handleSelect}
renderNoResults={this.renderNoResults}
renderResult={this.renderResult}
results={this.state.results}
selected={this.state.selected}
/>
<div className="dropdown-bottom-hint">
<div className="pull-right">
<ClockIcon className="little-spacer-right" size={12} />
{translate('recently_browsed')}
</div>
<FormattedMessage
defaultMessage={translate('search.shortcut_hint')}
id="search.shortcut_hint"
values={{
shortcut: <span className="shortcut-button shortcut-button-small">s</span>
}}
/>
{this.state.open && Object.keys(this.state.results).length > 0 && (
<DropdownOverlay noPadding={true}>
<div className="global-navbar-search-dropdown" ref={node => (this.node = node)}>
<SearchResults
allowMore={this.state.query.length !== 1}
loadingMore={this.state.loadingMore}
more={this.state.more}
onMoreClick={this.searchMore}
onSelect={this.handleSelect}
renderNoResults={this.renderNoResults}
renderResult={this.renderResult}
results={this.state.results}
selected={this.state.selected}
/>
<div className="dropdown-bottom-hint">
<div className="pull-right">
<ClockIcon className="little-spacer-right" size={12} />
{translate('recently_browsed')}
</div>
<FormattedMessage
defaultMessage={translate('search.shortcut_hint')}
id="search.shortcut_hint"
values={{
shortcut: <span className="shortcut-button shortcut-button-small">s</span>
}}
/>
</div>
</DropdownOverlay>
)}
</div>
</DropdownOverlay>
)}
</li>
);


+ 2
- 2
server/sonar-web/src/main/js/apps/about/routes.ts View File

@@ -23,8 +23,8 @@ import { isSonarCloud } from '../../helpers/system';
const routes = [
{
indexRoute: {
component: lazyLoad(
() => (isSonarCloud() ? import('./sonarcloud/Home') : import('./components/AboutApp'))
component: lazyLoad(() =>
isSonarCloud() ? import('./sonarcloud/Home') : import('./components/AboutApp')
)
},
childRoutes: isSonarCloud()

+ 5
- 6
server/sonar-web/src/main/js/apps/account/profile/Profile.tsx View File

@@ -38,12 +38,11 @@ function Profile({ customOrganizations, user }: Props) {
{translate('login')}: <strong id="login">{user.login}</strong>
</div>

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

{!!user.email && (
<div className="spacer-bottom">

+ 8
- 9
server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx View File

@@ -110,15 +110,14 @@ export default class TaskActions extends React.PureComponent<Props, State> {
return (
<td className="thin nowrap">
<ActionsDropdown className="js-task-action">
{canFilter &&
task.componentName && (
<ActionsDropdownItem className="js-task-filter" onClick={this.handleFilterClick}>
{translateWithParameters(
'background_tasks.filter_by_component_x',
task.componentName
)}
</ActionsDropdownItem>
)}
{canFilter && task.componentName && (
<ActionsDropdownItem className="js-task-filter" onClick={this.handleFilterClick}>
{translateWithParameters(
'background_tasks.filter_by_component_x',
task.componentName
)}
</ActionsDropdownItem>
)}
{canCancel && (
<ActionsDropdownItem
className="js-task-cancel"

+ 5
- 7
server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx View File

@@ -52,13 +52,11 @@ export default function TaskComponent({ task }: Props) {
{task.branchType === 'LONG' && <LongLivingBranchIcon className="little-spacer-right" />}
{task.pullRequest !== undefined && <PullRequestIcon className="little-spacer-right" />}

{!task.branchType &&
!task.pullRequest &&
task.componentQualifier && (
<span className="little-spacer-right">
<QualifierIcon qualifier={task.componentQualifier} />
</span>
)}
{!task.branchType && !task.pullRequest && task.componentQualifier && (
<span className="little-spacer-right">
<QualifierIcon qualifier={task.componentQualifier} />
</span>
)}

{task.organization && <Organization organizationKey={task.organization} />}


+ 19
- 22
server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx View File

@@ -102,14 +102,13 @@ export default class Workers extends React.PureComponent<{}, State> {

return (
<div className="display-flex-center">
{!loading &&
workerCount > 1 && (
<Tooltip overlay={translate('background_tasks.number_of_workers.warning')}>
<span className="display-inline-flex-center little-spacer-right">
<AlertWarnIcon fill="#d3d3d3" />
</span>
</Tooltip>
)}
{!loading && workerCount > 1 && (
<Tooltip overlay={translate('background_tasks.number_of_workers.warning')}>
<span className="display-inline-flex-center little-spacer-right">
<AlertWarnIcon fill="#d3d3d3" />
</span>
</Tooltip>
)}

<span className="text-middle">
{translate('background_tasks.number_of_workers')}
@@ -121,20 +120,18 @@ export default class Workers extends React.PureComponent<{}, State> {
)}
</span>

{!loading &&
canSetWorkerCount && (
<Tooltip overlay={translate('background_tasks.change_number_of_workers')}>
<EditButton
className="js-edit button-small spacer-left"
onClick={this.handleChangeClick}
/>
</Tooltip>
)}

{!loading &&
!canSetWorkerCount && (
<HelpTooltip className="spacer-left" overlay={<NoWorkersSupportPopup />} />
)}
{!loading && canSetWorkerCount && (
<Tooltip overlay={translate('background_tasks.change_number_of_workers')}>
<EditButton
className="js-edit button-small spacer-left"
onClick={this.handleChangeClick}
/>
</Tooltip>
)}

{!loading && !canSetWorkerCount && (
<HelpTooltip className="spacer-left" overlay={<NoWorkersSupportPopup />} />
)}

{formOpen && <WorkersForm onClose={this.closeForm} workerCount={this.state.workerCount} />}
</div>

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

@@ -290,33 +290,31 @@ export class App extends React.PureComponent<Props, State> {
</>
)}

{showSearch &&
searchResults && (
<div className={componentsClassName}>
<Components
branchLike={this.props.branchLike}
components={searchResults}
metrics={{}}
onHighlight={this.handleHighlight}
onSelect={this.handleSelect}
rootComponent={component}
selected={highlighted}
/>
</div>
)}
{showSearch && searchResults && (
<div className={componentsClassName}>
<Components
branchLike={this.props.branchLike}
components={searchResults}
metrics={{}}
onHighlight={this.handleHighlight}
onSelect={this.handleSelect}
rootComponent={component}
selected={highlighted}
/>
</div>
)}

{sourceViewer !== undefined &&
!showSearch && (
<div className="spacer-top">
<SourceViewerWrapper
branchLike={branchLike}
component={sourceViewer.key}
isFile={true}
location={location}
onGoToParent={this.handleGoToParent}
/>
</div>
)}
{sourceViewer !== undefined && !showSearch && (
<div className="spacer-top">
<SourceViewerWrapper
branchLike={branchLike}
component={sourceViewer.key}
isFile={true}
location={location}
onGoToParent={this.handleGoToParent}
/>
</div>
)}
</div>
</div>
);

+ 3
- 4
server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx View File

@@ -160,10 +160,9 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat
</div>

<div className="modal-body">
{!isUpdateMode &&
activeInAllProfiles && (
<Alert variant="info">{translate('coding_rules.active_in_all_profiles')}</Alert>
)}
{!isUpdateMode && activeInAllProfiles && (
<Alert variant="info">{translate('coding_rules.active_in_all_profiles')}</Alert>
)}

<div className="modal-field">
<label>{translate('coding_rules.quality_profile')}</label>

+ 26
- 29
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx View File

@@ -101,44 +101,41 @@ export default class BulkChange extends React.PureComponent<Props, State> {
{translate('coding_rules.activate_in')}…
</a>
</li>
{allowActivateOnProfile &&
profile && (
<li>
<a href="#" onClick={this.handleActivateInProfileClick}>
{translate('coding_rules.activate_in')} <strong>{profile.name}</strong>
</a>
</li>
)}
{allowActivateOnProfile && profile && (
<li>
<a href="#" onClick={this.handleActivateInProfileClick}>
{translate('coding_rules.activate_in')} <strong>{profile.name}</strong>
</a>
</li>
)}
<li>
<a href="#" onClick={this.handleDeactivateClick}>
{translate('coding_rules.deactivate_in')}…
</a>
</li>
{allowDeactivateOnProfile &&
profile && (
<li>
<a href="#" onClick={this.handleDeactivateInProfileClick}>
{translate('coding_rules.deactivate_in')} <strong>{profile.name}</strong>
</a>
</li>
)}
{allowDeactivateOnProfile && profile && (
<li>
<a href="#" onClick={this.handleDeactivateInProfileClick}>
{translate('coding_rules.deactivate_in')} <strong>{profile.name}</strong>
</a>
</li>
)}
</ul>
}>
<Button className="js-bulk-change">{translate('bulk_change')}</Button>
</Dropdown>
{this.state.modal &&
this.state.action && (
<BulkChangeModal
action={this.state.action}
languages={this.props.languages}
onClose={this.closeModal}
organization={this.props.organization}
profile={this.state.profile}
query={this.props.query}
referencedProfiles={this.props.referencedProfiles}
total={this.props.total}
/>
)}
{this.state.modal && this.state.action && (
<BulkChangeModal
action={this.state.action}
languages={this.props.languages}
onClose={this.closeModal}
organization={this.props.organization}
profile={this.state.profile}
query={this.props.query}
referencedProfiles={this.props.referencedProfiles}
total={this.props.total}
/>
)}
</>
);
}

+ 20
- 21
server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx View File

@@ -207,27 +207,26 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
<div className="modal-body">
{this.state.results.map(this.renderResult)}

{!this.state.finished &&
!this.state.submitting && (
<div className="modal-field">
<h3>
<label htmlFor="coding-rules-bulk-change-profile">
{action === 'activate'
? translate('coding_rules.activate_in')
: translate('coding_rules.deactivate_in')}
</label>
</h3>
{profile ? (
<span>
{profile.name}
{' — '}
{translate('are_you_sure')}
</span>
) : (
this.renderProfileSelect()
)}
</div>
)}
{!this.state.finished && !this.state.submitting && (
<div className="modal-field">
<h3>
<label htmlFor="coding-rules-bulk-change-profile">
{action === 'activate'
? translate('coding_rules.activate_in')
: translate('coding_rules.deactivate_in')}
</label>
</h3>
{profile ? (
<span>
{profile.name}
{' — '}
{translate('are_you_sure')}
</span>
) : (
this.renderProfileSelect()
)}
</div>
)}
</div>

<footer className="modal-foot">

+ 3
- 2
server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx View File

@@ -118,8 +118,9 @@ export default class Facet extends React.PureComponent<Props> {
{this.props.children}
</FacetHeader>

{this.props.open &&
items !== undefined && <FacetItemsList>{items.map(this.renderItem)}</FacetItemsList>}
{this.props.open && items !== undefined && (
<FacetItemsList>{items.map(this.renderItem)}</FacetItemsList>
)}

{this.props.open && this.props.renderFooter !== undefined && this.props.renderFooter()}
</FacetBox>

+ 15
- 17
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx View File

@@ -103,8 +103,8 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
handleTagsChange = (tags: string[]) => {
// optimistic update
const oldTags = this.state.ruleDetails && this.state.ruleDetails.tags;
this.setState(
state => (state.ruleDetails ? { ruleDetails: { ...state.ruleDetails, tags } } : null)
this.setState(state =>
state.ruleDetails ? { ruleDetails: { ...state.ruleDetails, tags } } : null
);
updateRule({
key: this.props.ruleKey,
@@ -112,9 +112,8 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
tags: tags.join()
}).catch(() => {
if (this.mounted) {
this.setState(
state =>
state.ruleDetails ? { ruleDetails: { ...state.ruleDetails, tags: oldTags } } : null
this.setState(state =>
state.ruleDetails ? { ruleDetails: { ...state.ruleDetails, tags: oldTags } } : null
);
}
});
@@ -239,18 +238,17 @@ export default class RuleDetails extends React.PureComponent<Props, State> {
/>
)}

{!ruleDetails.isTemplate &&
!hideQualityProfiles && (
<RuleDetailsProfiles
activations={this.state.actives}
canWrite={canWrite}
onActivate={this.handleActivate}
onDeactivate={this.handleDeactivate}
organization={organization}
referencedProfiles={referencedProfiles}
ruleDetails={ruleDetails}
/>
)}
{!ruleDetails.isTemplate && !hideQualityProfiles && (
<RuleDetailsProfiles
activations={this.state.actives}
canWrite={canWrite}
onActivate={this.handleActivate}
onDeactivate={this.handleDeactivate}
organization={organization}
referencedProfiles={referencedProfiles}
ruleDetails={ruleDetails}
/>
)}

{!ruleDetails.isTemplate && (
<RuleDetailsIssues organization={organization} ruleDetails={ruleDetails} />

+ 11
- 9
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx View File

@@ -109,15 +109,17 @@ export default class RuleDetailsCustomRules extends React.PureComponent<Props, S

<td className="coding-rules-detail-list-parameters">
{rule.params &&
rule.params.filter(param => param.defaultValue).map(param => (
<div className="coding-rules-detail-list-parameter" key={param.key}>
<span className="key">{param.key}</span>
<span className="sep">:&nbsp;</span>
<span className="value" title={param.defaultValue}>
{param.defaultValue}
</span>
</div>
))}
rule.params
.filter(param => param.defaultValue)
.map(param => (
<div className="coding-rules-detail-list-parameter" key={param.key}>
<span className="key">{param.key}</span>
<span className="sep">:&nbsp;</span>
<span className="value" title={param.defaultValue}>
{param.defaultValue}
</span>
</div>
))}
</td>

{this.props.canChange && (

+ 11
- 36
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx View File

@@ -42,30 +42,7 @@ interface Props {
ruleDetails: T.RuleDetails;
}

interface State {
loading: boolean;
}

export default class RuleDetailsProfiles extends React.PureComponent<Props, State> {
mounted = false;

componentDidMount() {
this.mounted = true;
this.fetchProfiles();
}

componentDidUpdate(prevProps: Props) {
if (prevProps.ruleDetails.key !== this.props.ruleDetails.key) {
this.fetchProfiles();
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchProfiles = () => this.setState({ loading: true });

export default class RuleDetailsProfiles extends React.PureComponent<Props> {
handleActivate = () => this.props.onActivate();

handleDeactivate = (key?: string) => {
@@ -119,12 +96,11 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props, Stat
<SeverityHelper className="display-inline-flex-center" severity={activation.severity} />
</span>
</Tooltip>
{parentActivation !== undefined &&
activation.severity !== parentActivation.severity && (
<div className="coding-rules-detail-quality-profile-inheritance">
{translate('coding_rules.original')} {translate('severity', parentActivation.severity)}
</div>
)}
{parentActivation !== undefined && activation.severity !== parentActivation.severity && (
<div className="coding-rules-detail-quality-profile-inheritance">
{translate('coding_rules.original')} {translate('severity', parentActivation.severity)}
</div>
)}
</td>
);

@@ -143,12 +119,11 @@ export default class RuleDetailsProfiles extends React.PureComponent<Props, Stat
<span className="value" title={param.value}>
{param.value}
</span>
{parentActivation &&
param.value !== originalValue && (
<div className="coding-rules-detail-quality-profile-inheritance">
{translate('coding_rules.original')} <span className="value">{originalValue}</span>
</div>
)}
{parentActivation && param.value !== originalValue && (
<div className="coding-rules-detail-quality-profile-inheritance">
{translate('coding_rules.original')} <span className="value">{originalValue}</span>
</div>
)}
</div>
);
};

+ 30
- 31
server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx View File

@@ -89,37 +89,36 @@ export default class RuleListItem extends React.PureComponent<Props> {
return (
<td className="coding-rule-table-meta-cell coding-rule-activation">
<SeverityIcon severity={activation.severity} />
{selectedProfile &&
selectedProfile.parentName && (
<>
{activation.inherit === 'OVERRIDES' && (
<Tooltip
overlay={translateWithParameters(
'coding_rules.overrides',
selectedProfile.name,
selectedProfile.parentName
)}>
<RuleInheritanceIcon
className="little-spacer-left"
inheritance={activation.inherit}
/>
</Tooltip>
)}
{activation.inherit === 'INHERITED' && (
<Tooltip
overlay={translateWithParameters(
'coding_rules.inherits',
selectedProfile.name,
selectedProfile.parentName
)}>
<RuleInheritanceIcon
className="little-spacer-left"
inheritance={activation.inherit}
/>
</Tooltip>
)}
</>
)}
{selectedProfile && selectedProfile.parentName && (
<>
{activation.inherit === 'OVERRIDES' && (
<Tooltip
overlay={translateWithParameters(
'coding_rules.overrides',
selectedProfile.name,
selectedProfile.parentName
)}>
<RuleInheritanceIcon
className="little-spacer-left"
inheritance={activation.inherit}
/>
</Tooltip>
)}
{activation.inherit === 'INHERITED' && (
<Tooltip
overlay={translateWithParameters(
'coding_rules.inherits',
selectedProfile.name,
selectedProfile.parentName
)}>
<RuleInheritanceIcon
className="little-spacer-left"
inheritance={activation.inherit}
/>
</Tooltip>
)}
</>
)}
</td>
);
};

+ 64
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChange-test.tsx View File

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

const profile = mockQualityProfile({
actions: {
edit: true,
setAsDefault: true,
copy: true,
associateProjects: true,
delete: false
}
});

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
});

it('should not render anything', () => {
const wrapper = shallowRender({
referencedProfiles: { key: { ...profile, actions: { ...profile.actions, edit: false } } }
});
expect(wrapper.type()).toBeNull();
});

it('should display BulkChangeModal', () => {
const wrapper = shallowRender();
wrapper.instance().handleActivateClick(mockEvent());
expect(wrapper.find('BulkChangeModal')).toMatchSnapshot();
});

function shallowRender(props: Partial<BulkChange['props']> = {}) {
return shallow<BulkChange>(
<BulkChange
languages={{ js: { key: 'js', name: 'JavaScript' } }}
organization={undefined}
query={{ activation: false, profile: 'key' } as BulkChange['props']['query']}
referencedProfiles={{ key: profile }}
total={2}
{...props}
/>
);
}

+ 98
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx View File

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

jest.mock('../../../../api/rules', () => ({
deleteRule: jest.fn(),
getRuleDetails: jest.fn().mockResolvedValue({
rule: getMockHelpers().mockRuleDetails(),
actives: [
{
qProfile: 'key',
inherit: 'NONE',
severity: 'MAJOR',
params: [],
createdAt: '2017-06-16T16:13:38+0200',
updatedAt: '2017-06-16T16:13:38+0200'
}
]
}),
updateRule: jest.fn().mockResolvedValue({})
}));

const { mockQualityProfile } = getMockHelpers();
const profile = mockQualityProfile();

beforeEach(() => {
jest.clearAllMocks();
});

it('should render correctly', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});

it('should correctly handle tag changes', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.instance().handleTagsChange(['foo', 'bar']);
const ruleDetails = wrapper.state('ruleDetails');
expect(ruleDetails && ruleDetails.tags).toEqual(['foo', 'bar']);
await waitAndUpdate(wrapper);
expect(updateRule).toHaveBeenCalledWith({
key: 'squid:S1337',
organization: undefined,
tags: 'foo,bar'
});
});

function getMockHelpers() {
// We use this little "force-requiring" instead of an import statement in
// order to prevent a hoisting race condition while mocking. If we want to use
// a mock helper in a Jest mock, we have to require it like this. Otherwise,
// we get errors like:
// ReferenceError: testMocks_1 is not defined
return require.requireActual('../../../../helpers/testMocks');
}

function shallowRender(props: Partial<RuleDetails['props']> = {}) {
return shallow<RuleDetails>(
<RuleDetails
onActivate={jest.fn()}
onDeactivate={jest.fn()}
onDelete={jest.fn()}
onFilterChange={jest.fn()}
organization={undefined}
referencedProfiles={{ key: profile }}
referencedRepositories={{
javascript: { key: 'javascript', language: 'js', name: 'SonarAnalyzer' }
}}
ruleKey="squid:S1337"
selectedProfile={profile}
{...props}
/>
);
}

+ 99
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChange-test.tsx.snap View File

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

exports[`should display BulkChangeModal 1`] = `
<BulkChangeModal
action="activate"
languages={
Object {
"js": Object {
"key": "js",
"name": "JavaScript",
},
}
}
onClose={[Function]}
query={
Object {
"activation": false,
"profile": "key",
}
}
referencedProfiles={
Object {
"key": Object {
"actions": Object {
"associateProjects": true,
"copy": true,
"delete": false,
"edit": true,
"setAsDefault": true,
},
"activeDeprecatedRuleCount": 2,
"activeRuleCount": 10,
"childrenCount": 0,
"depth": 1,
"isBuiltIn": false,
"isDefault": false,
"isInherited": false,
"key": "key",
"language": "js",
"languageName": "JavaScript",
"name": "name",
"organization": "foo",
"projectCount": 3,
},
}
}
total={2}
/>
`;

exports[`should render correctly 1`] = `
<Fragment>
<Dropdown
className="pull-left"
overlay={
<ul
className="menu"
>
<li>
<a
href="#"
onClick={[Function]}
>
coding_rules.activate_in
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
coding_rules.activate_in
<strong>
name
</strong>
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
coding_rules.deactivate_in
</a>
</li>
</ul>
}
>
<Button
className="js-bulk-change"
>
bulk_change
</Button>
</Dropdown>
</Fragment>
`;

+ 204
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<div
className="coding-rule-details"
/>
`;

exports[`should render correctly 2`] = `
<div
className="coding-rule-details"
>
<DeferredSpinner
loading={false}
timeout={100}
>
<RuleDetailsMeta
onFilterChange={[MockFunction]}
onTagsChange={[Function]}
referencedRepositories={
Object {
"javascript": Object {
"key": "javascript",
"language": "js",
"name": "SonarAnalyzer",
},
}
}
ruleDetails={
Object {
"createdAt": "2014-12-16T17:26:54+0100",
"debtOverloaded": false,
"debtRemFnOffset": "5min",
"debtRemFnType": "CONSTANT_ISSUE",
"defaultDebtRemFnOffset": "5min",
"defaultDebtRemFnType": "CONSTANT_ISSUE",
"defaultRemFnBaseEffort": "5min",
"defaultRemFnType": "CONSTANT_ISSUE",
"htmlDesc": "",
"isExternal": false,
"isTemplate": false,
"key": "squid:S1337",
"lang": "java",
"langName": "Java",
"mdDesc": "",
"name": "\\".equals()\\" should not be used to test the values of \\"Atomic\\" classes",
"params": Array [],
"remFnBaseEffort": "5min",
"remFnOverloaded": false,
"remFnType": "CONSTANT_ISSUE",
"repo": "squid",
"scope": "MAIN",
"severity": "MAJOR",
"status": "READY",
"sysTags": Array [
"multi-threading",
],
"tags": Array [],
"type": "BUG",
}
}
/>
<RuleDetailsDescription
onChange={[Function]}
ruleDetails={
Object {
"createdAt": "2014-12-16T17:26:54+0100",
"debtOverloaded": false,
"debtRemFnOffset": "5min",
"debtRemFnType": "CONSTANT_ISSUE",
"defaultDebtRemFnOffset": "5min",
"defaultDebtRemFnType": "CONSTANT_ISSUE",
"defaultRemFnBaseEffort": "5min",
"defaultRemFnType": "CONSTANT_ISSUE",
"htmlDesc": "",
"isExternal": false,
"isTemplate": false,
"key": "squid:S1337",
"lang": "java",
"langName": "Java",
"mdDesc": "",
"name": "\\".equals()\\" should not be used to test the values of \\"Atomic\\" classes",
"params": Array [],
"remFnBaseEffort": "5min",
"remFnOverloaded": false,
"remFnType": "CONSTANT_ISSUE",
"repo": "squid",
"scope": "MAIN",
"severity": "MAJOR",
"status": "READY",
"sysTags": Array [
"multi-threading",
],
"tags": Array [],
"type": "BUG",
}
}
/>
<RuleDetailsProfiles
activations={
Array [
Object {
"createdAt": "2017-06-16T16:13:38+0200",
"inherit": "NONE",
"params": Array [],
"qProfile": "key",
"severity": "MAJOR",
"updatedAt": "2017-06-16T16:13:38+0200",
},
]
}
onActivate={[Function]}
onDeactivate={[Function]}
referencedProfiles={
Object {
"key": Object {
"activeDeprecatedRuleCount": 2,
"activeRuleCount": 10,
"childrenCount": 0,
"depth": 1,
"isBuiltIn": false,
"isDefault": false,
"isInherited": false,
"key": "key",
"language": "js",
"languageName": "JavaScript",
"name": "name",
"organization": "foo",
"projectCount": 3,
},
}
}
ruleDetails={
Object {
"createdAt": "2014-12-16T17:26:54+0100",
"debtOverloaded": false,
"debtRemFnOffset": "5min",
"debtRemFnType": "CONSTANT_ISSUE",
"defaultDebtRemFnOffset": "5min",
"defaultDebtRemFnType": "CONSTANT_ISSUE",
"defaultRemFnBaseEffort": "5min",
"defaultRemFnType": "CONSTANT_ISSUE",
"htmlDesc": "",
"isExternal": false,
"isTemplate": false,
"key": "squid:S1337",
"lang": "java",
"langName": "Java",
"mdDesc": "",
"name": "\\".equals()\\" should not be used to test the values of \\"Atomic\\" classes",
"params": Array [],
"remFnBaseEffort": "5min",
"remFnOverloaded": false,
"remFnType": "CONSTANT_ISSUE",
"repo": "squid",
"scope": "MAIN",
"severity": "MAJOR",
"status": "READY",
"sysTags": Array [
"multi-threading",
],
"tags": Array [],
"type": "BUG",
}
}
/>
<Connect(withAppState(RuleDetailsIssues))
ruleDetails={
Object {
"createdAt": "2014-12-16T17:26:54+0100",
"debtOverloaded": false,
"debtRemFnOffset": "5min",
"debtRemFnType": "CONSTANT_ISSUE",
"defaultDebtRemFnOffset": "5min",
"defaultDebtRemFnType": "CONSTANT_ISSUE",
"defaultRemFnBaseEffort": "5min",
"defaultRemFnType": "CONSTANT_ISSUE",
"htmlDesc": "",
"isExternal": false,
"isTemplate": false,
"key": "squid:S1337",
"lang": "java",
"langName": "Java",
"mdDesc": "",
"name": "\\".equals()\\" should not be used to test the values of \\"Atomic\\" classes",
"params": Array [],
"remFnBaseEffort": "5min",
"remFnOverloaded": false,
"remFnType": "CONSTANT_ISSUE",
"repo": "squid",
"scope": "MAIN",
"severity": "MAJOR",
"status": "READY",
"sysTags": Array [
"multi-threading",
],
"tags": Array [],
"type": "BUG",
}
}
/>
</DeferredSpinner>
</div>
`;

+ 41
- 56
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx View File

@@ -53,7 +53,6 @@ interface Props {
interface State {
baseComponent?: T.ComponentMeasure;
components: T.ComponentMeasureEnhanced[];
loading: boolean;
loadingMoreComponents: boolean;
measure?: T.Measure;
metric?: T.Metric;
@@ -67,7 +66,6 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
components: [],
loading: true,
loadingMoreComponents: false
};

@@ -94,7 +92,6 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
}

fetchComponentTree = () => {
this.setState({ loading: true });
const { metricKeys, opts, strategy } = this.getComponentRequestParams(
this.props.view,
this.props.requestedMetric
@@ -111,41 +108,30 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
metricKeys: baseComponentMetrics.join(),
...getBranchLikeQuery(this.props.branchLike)
})
]).then(
([tree, measures]) => {
if (this.mounted) {
const metric = tree.metrics.find(m => m.key === this.props.requestedMetric.key);
const components = tree.components.map(component =>
enhanceComponent(component, metric, this.props.metrics)
);
]).then(([tree, measures]) => {
if (this.mounted) {
const metric = tree.metrics.find(m => m.key === this.props.requestedMetric.key);
const components = tree.components.map(component =>
enhanceComponent(component, metric, this.props.metrics)
);

const measure = measures.find(
measure => measure.metric === this.props.requestedMetric.key
);
const secondaryMeasure = measures.find(
measure => measure.metric !== this.props.requestedMetric.key
);
const measure = measures.find(measure => measure.metric === this.props.requestedMetric.key);
const secondaryMeasure = measures.find(
measure => measure.metric !== this.props.requestedMetric.key
);

this.setState(({ selected }) => ({
baseComponent: tree.baseComponent,
components,
measure,
metric,
paging: tree.paging,
secondaryMeasure,
selected:
components.length > 0 && components.find(c => c.key === selected)
? selected
: undefined
}));
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
this.setState(({ selected }) => ({
baseComponent: tree.baseComponent,
components,
measure,
metric,
paging: tree.paging,
secondaryMeasure,
selected:
components.length > 0 && components.find(c => c.key === selected) ? selected : undefined
}));
}
);
});
};

fetchMoreComponents = () => {
@@ -336,28 +322,27 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
}
right={
<div className="display-flex-center">
{!isFile &&
metric && (
<>
<div>{translate('component_measures.view_as')}</div>
<MeasureViewSelect
className="measure-view-select spacer-left big-spacer-right"
handleViewChange={this.updateView}
metric={metric}
view={view}
/>
{!isFile && metric && (
<>
<div>{translate('component_measures.view_as')}</div>
<MeasureViewSelect
className="measure-view-select spacer-left big-spacer-right"
handleViewChange={this.updateView}
metric={metric}
view={view}
/>

<PageActions
current={
selectedIdx !== undefined && view !== 'treemap'
? selectedIdx + 1
: undefined
}
showShortcuts={['list', 'tree'].includes(view)}
total={paging && paging.total}
/>
</>
)}
<PageActions
current={
selectedIdx !== undefined && view !== 'treemap'
? selectedIdx + 1
: undefined
}
showShortcuts={['list', 'tree'].includes(view)}
total={paging && paging.total}
/>
</>
)}
</div>
}
/>

+ 12
- 14
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx View File

@@ -61,22 +61,20 @@ export default function MeasureHeader(props: Props) {
/>
</strong>
</span>
{!isDiff &&
hasHistory && (
<Tooltip overlay={translate('component_measures.show_metric_history')}>
<Link
className="js-show-history spacer-left button button-small"
to={getMeasureHistoryUrl(component.key, metric.key, branchLike)}>
<HistoryIcon />
</Link>
</Tooltip>
)}
{!isDiff && hasHistory && (
<Tooltip overlay={translate('component_measures.show_metric_history')}>
<Link
className="js-show-history spacer-left button button-small"
to={getMeasureHistoryUrl(component.key, metric.key, branchLike)}>
<HistoryIcon />
</Link>
</Tooltip>
)}
</div>
<div className="measure-details-primary-actions">
{displayLeak &&
leakPeriod && (
<LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} />
)}
{displayLeak && leakPeriod && (
<LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} />
)}
</div>
</div>
{secondaryMeasure &&

+ 3
- 8
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx View File

@@ -167,14 +167,9 @@ export default class MeasureOverview extends React.PureComponent<Props, State> {
</div>
<div className="layout-page-main-inner measure-details-content">
<div className="clearfix big-spacer-bottom">
{leakPeriod &&
displayLeak && (
<LeakPeriodLegend
className="pull-right"
component={component}
period={leakPeriod}
/>
)}
{leakPeriod && displayLeak && (
<LeakPeriodLegend className="pull-right" component={component} period={leakPeriod} />
)}
</div>
<DeferredSpinner loading={this.props.loading} />
{!this.props.loading && this.renderContent()}

+ 96
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx View File

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

jest.mock('../../../../api/components', () => {
const { mockComponentMeasure } = require.requireActual('../../../../helpers/testMocks');
return {
getComponentTree: jest.fn().mockResolvedValue({
paging: { pageIndex: 1, pageSize: 500, total: 2 },
baseComponent: mockComponentMeasure(),
components: [mockComponentMeasure(true)],
metrics: [
{
bestValue: '0',
custom: false,
description: 'Bugs',
domain: 'Reliability',
hidden: false,
higherValuesAreBetter: false,
key: 'bugs',
name: 'Bugs',
qualitative: true,
type: 'INT'
}
]
})
};
});

jest.mock('../../../../api/measures', () => ({
getMeasures: jest.fn().mockResolvedValue([{ metric: 'bugs', value: '12', bestValue: false }])
}));

const METRICS = {
bugs: { id: '1', key: 'bugs', type: 'INT', name: 'Bugs', domain: 'Reliability' }
};

beforeEach(() => {
jest.clearAllMocks();
});

it('should render correctly for a project', async () => {
const wrapper = shallowRender();
expect(wrapper.type()).toBeNull();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});

it('should render correctly for a file', async () => {
(getComponentTree as jest.Mock).mockResolvedValueOnce({
paging: { pageIndex: 1, pageSize: 500, total: 0 },
baseComponent: mockComponentMeasure(true),
components: [],
metrics: [METRICS.bugs]
});
const wrapper = shallowRender();
expect(wrapper.type()).toBeNull();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});

function shallowRender(props: Partial<MeasureContent['props']> = {}) {
return shallow(
<MeasureContent
metrics={METRICS}
requestedMetric={{ direction: 1, key: 'bugs' }}
rootComponent={mockComponentMeasure()}
router={mockRouter()}
updateQuery={jest.fn()}
view="list"
{...props}
/>
);
}

+ 312
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap View File

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

exports[`should render correctly for a file 1`] = `
<div
className="layout-page-main no-outline"
>
<A11ySkipTarget
anchor="measures_main"
/>
<div
className="layout-page-header-panel layout-page-main-header"
>
<div
className="layout-page-header-panel-inner layout-page-main-header-inner"
>
<div
className="layout-page-main-inner"
>
<MeasureContentHeader
left={
<Breadcrumbs
backToFirst={true}
className="text-ellipsis flex-1"
component={
Object {
"key": "foo:src/index.tsx",
"measures": Array [
Object {
"bestValue": false,
"metric": "bugs",
"value": "1",
},
],
"name": "index.tsx",
"path": "src/index.tsx",
"qualifier": "FIL",
}
}
handleSelect={[Function]}
rootComponent={
Object {
"key": "foo",
"measures": Array [
Object {
"bestValue": false,
"metric": "bugs",
"value": "12",
},
],
"name": "Foo",
"qualifier": "TRK",
}
}
/>
}
right={
<div
className="display-flex-center"
/>
}
/>
</div>
</div>
</div>
<div
className="layout-page-main-inner measure-details-content"
>
<MeasureHeader
component={
Object {
"key": "foo:src/index.tsx",
"measures": Array [
Object {
"bestValue": false,
"metric": "bugs",
"value": "1",
},
],
"name": "index.tsx",
"path": "src/index.tsx",
"qualifier": "FIL",
}
}
measureValue="12"
metric={
Object {
"domain": "Reliability",
"id": "1",
"key": "bugs",
"name": "Bugs",
"type": "INT",
}
}
/>
<div
className="measure-details-viewer"
>
<LazyLoader
component="foo:src/index.tsx"
/>
</div>
</div>
</div>
`;

exports[`should render correctly for a project 1`] = `
<div
className="layout-page-main no-outline"
>
<A11ySkipTarget
anchor="measures_main"
/>
<div
className="layout-page-header-panel layout-page-main-header"
>
<div
className="layout-page-header-panel-inner layout-page-main-header-inner"
>
<div
className="layout-page-main-inner"
>
<MeasureContentHeader
left={
<Breadcrumbs
backToFirst={true}
className="text-ellipsis flex-1"
component={
Object {
"key": "foo",
"measures": Array [
Object {
"bestValue": false,
"metric": "bugs",
"value": "12",
},
],
"name": "Foo",
"qualifier": "TRK",
}
}
handleSelect={[Function]}
rootComponent={
Object {
"key": "foo",
"measures": Array [
Object {
"bestValue": false,
"metric": "bugs",
"value": "12",
},
],
"name": "Foo",
"qualifier": "TRK",
}
}
/>
}
right={
<div
className="display-flex-center"
>
<React.Fragment>
<div>
component_measures.view_as
</div>
<MeasureViewSelect
className="measure-view-select spacer-left big-spacer-right"
handleViewChange={[Function]}
metric={
Object {
"bestValue": "0",
"custom": false,
"description": "Bugs",
"domain": "Reliability",
"hidden": false,
"higherValuesAreBetter": false,
"key": "bugs",
"name": "Bugs",
"qualitative": true,
"type": "INT",
}
}
view="list"
/>
<PageActions
showShortcuts={true}
total={2}
/>
</React.Fragment>
</div>
}
/>
</div>
</div>
</div>
<div
className="layout-page-main-inner measure-details-content"
>
<MeasureHeader
component={
Object {
"key": "foo",
"measures": Array [
Object {
"bestValue": false,
"metric": "bugs",
"value": "12",
},
],
"name": "Foo",
"qualifier": "TRK",
}
}
measureValue="12"
metric={
Object {
"bestValue": "0",
"custom": false,
"description": "Bugs",
"domain": "Reliability",
"hidden": false,
"higherValuesAreBetter": false,
"key": "bugs",
"name": "Bugs",
"qualitative": true,
"type": "INT",
}
}
/>
<FilesView
components={
Array [
Object {
"key": "foo:src/index.tsx",
"leak": undefined,
"measures": Array [
Object {
"bestValue": false,
"leak": undefined,
"metric": Object {
"domain": "Reliability",
"id": "1",
"key": "bugs",
"name": "Bugs",
"type": "INT",
},
"value": "1",
},
],
"name": "index.tsx",
"path": "src/index.tsx",
"qualifier": "FIL",
"value": "1",
},
]
}
defaultShowBestMeasures={false}
fetchMore={[Function]}
handleOpen={[Function]}
handleSelect={[Function]}
loadingMore={false}
metric={
Object {
"bestValue": "0",
"custom": false,
"description": "Bugs",
"domain": "Reliability",
"hidden": false,
"higherValuesAreBetter": false,
"key": "bugs",
"name": "Bugs",
"qualitative": true,
"type": "INT",
}
}
metrics={
Object {
"bugs": Object {
"domain": "Reliability",
"id": "1",
"key": "bugs",
"name": "Bugs",
"type": "INT",
},
}
}
paging={
Object {
"pageIndex": 1,
"pageSize": 500,
"total": 2,
}
}
rootComponent={
Object {
"key": "foo",
"measures": Array [
Object {
"bestValue": false,
"metric": "bugs",
"value": "12",
},
],
"name": "Foo",
"qualifier": "TRK",
}
}
view="list"
/>
</div>
</div>
`;

+ 22
- 25
server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx View File

@@ -177,31 +177,28 @@ export default class FilesView extends React.PureComponent<Props, State> {
selectedComponent={this.props.selectedKey}
view={this.props.view}
/>
{hidingBestMeasures &&
this.props.paging && (
<Alert className="spacer-top" variant="info">
<div className="display-flex-center">
{translateWithParameters(
'component_measures.hidden_best_score_metrics',
formatMeasure(this.props.paging.total - filteredComponents.length, 'INT'),
formatMeasure(this.props.metric.bestValue, this.props.metric.type)
)}
<Button className="button-small spacer-left" onClick={this.handleShowBestMeasures}>
{translate('show_them')}
</Button>
</div>
</Alert>
)}
{!hidingBestMeasures &&
this.props.paging &&
this.props.components.length > 0 && (
<ListFooter
count={this.props.components.length}
loadMore={this.props.fetchMore}
loading={this.props.loadingMore}
total={this.props.paging.total}
/>
)}
{hidingBestMeasures && this.props.paging && (
<Alert className="spacer-top" variant="info">
<div className="display-flex-center">
{translateWithParameters(
'component_measures.hidden_best_score_metrics',
formatMeasure(this.props.paging.total - filteredComponents.length, 'INT'),
formatMeasure(this.props.metric.bestValue, this.props.metric.type)
)}
<Button className="button-small spacer-left" onClick={this.handleShowBestMeasures}>
{translate('show_them')}
</Button>
</div>
</Alert>
)}
{!hidingBestMeasures && this.props.paging && this.props.components.length > 0 && (
<ListFooter
count={this.props.components.length}
loadMore={this.props.fetchMore}
loading={this.props.loadingMore}
total={this.props.paging.total}
/>
)}
</div>
);
}

+ 19
- 20
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx View File

@@ -105,26 +105,25 @@ export default class DomainFacet extends React.PureComponent<Props> {
);
});

return sortedItems.map(
item =>
typeof item === 'string' ? (
this.renderCategoryItem(item)
) : (
<FacetItem
active={item.metric.key === selected}
disabled={false}
key={item.metric.key}
name={
<span className="big-spacer-left" id={`measure-${item.metric.key}-name`}>
{translateMetric(item.metric)}
</span>
}
onClick={this.props.onChange}
stat={this.renderItemFacetStat(item)}
tooltip={translateMetric(item.metric)}
value={item.metric.key}
/>
)
return sortedItems.map(item =>
typeof item === 'string' ? (
this.renderCategoryItem(item)
) : (
<FacetItem
active={item.metric.key === selected}
disabled={false}
key={item.metric.key}
name={
<span className="big-spacer-left" id={`measure-${item.metric.key}-name`}>
{translateMetric(item.metric)}
</span>
}
onClick={this.props.onChange}
stat={this.renderItemFacetStat(item)}
tooltip={translateMetric(item.metric)}
value={item.metric.key}
/>
)
);
};


+ 11
- 12
server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx View File

@@ -200,18 +200,17 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
)}
</OrganizationDetailsStep>

{subscriptionPlans !== undefined &&
filter !== Filters.Bind && (
<PlanStep
almApplication={this.props.almApplication}
almOrganization={this.props.almOrganization}
createOrganization={this.handleCreateOrganization}
onDone={this.props.onDone}
onUpgradeFail={this.props.onUpgradeFail}
open={step === Step.Plan}
subscriptionPlans={subscriptionPlans}
/>
)}
{subscriptionPlans !== undefined && filter !== Filters.Bind && (
<PlanStep
almApplication={this.props.almApplication}
almOrganization={this.props.almOrganization}
createOrganization={this.handleCreateOrganization}
onDone={this.props.onDone}
onUpgradeFail={this.props.onUpgradeFail}
open={step === Step.Plan}
subscriptionPlans={subscriptionPlans}
/>
)}
</div>
);
}

+ 5
- 6
server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx View File

@@ -463,12 +463,11 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
<h1 className="page-title huge big-spacer-bottom">
<strong>{header}</strong>
</h1>
{!importPersonalOrg &&
startedPrice !== undefined && (
<p className="page-description">
{translate('onboarding.create_organization.page.description')}
</p>
)}
{!importPersonalOrg && startedPrice !== undefined && (
<p className="page-description">
{translate('onboarding.create_organization.page.description')}
</p>
)}
</header>
{this.state.loading ? (
<DeferredSpinner />

+ 40
- 42
server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx View File

@@ -103,48 +103,46 @@ export class RemoteOrganizationChoose extends React.PureComponent<Props & WithRo
<h2>{translate('onboarding.import_organization.import_org_details')}</h2>
</div>
<div className="boxed-group-inner">
{almInstallId &&
!almOrganization && (
<Alert className="big-spacer-bottom width-60" variant="error">
<div className="markdown">
{translate('onboarding.import_organization.org_not_found')}
<ul>
<li>{translate('onboarding.import_organization.org_not_found.tips_1')}</li>
<li>{translate('onboarding.import_organization.org_not_found.tips_2')}</li>
</ul>
</div>
</Alert>
)}
{almOrganization &&
boundOrganization && (
<Alert className="big-spacer-bottom width-60" variant="error">
<FormattedMessage
defaultMessage={translate('onboarding.import_organization.already_bound_x')}
id="onboarding.import_organization.already_bound_x"
values={{
avatar: (
<img
alt={almApplication.name}
className="little-spacer-left"
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(
almApplication.key
)}.svg`}
width={16}
/>
),
name: <strong>{almOrganization.name}</strong>,
boundAvatar: (
<OrganizationAvatar
className="little-spacer-left"
organization={boundOrganization}
small={true}
/>
),
boundName: <strong>{boundOrganization.name}</strong>
}}
/>
</Alert>
)}
{almInstallId && !almOrganization && (
<Alert className="big-spacer-bottom width-60" variant="error">
<div className="markdown">
{translate('onboarding.import_organization.org_not_found')}
<ul>
<li>{translate('onboarding.import_organization.org_not_found.tips_1')}</li>
<li>{translate('onboarding.import_organization.org_not_found.tips_2')}</li>
</ul>
</div>
</Alert>
)}
{almOrganization && boundOrganization && (
<Alert className="big-spacer-bottom width-60" variant="error">
<FormattedMessage
defaultMessage={translate('onboarding.import_organization.already_bound_x')}
id="onboarding.import_organization.already_bound_x"
values={{
avatar: (
<img
alt={almApplication.name}
className="little-spacer-left"
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(
almApplication.key
)}.svg`}
width={16}
/>
),
name: <strong>{almOrganization.name}</strong>,
boundAvatar: (
<OrganizationAvatar
className="little-spacer-left"
organization={boundOrganization}
small={true}
/>
),
boundName: <strong>{boundOrganization.name}</strong>
}}
/>
</Alert>
)}
<div className="display-flex-center">
<div className="display-inline-block">
<IdentityProviderLink

+ 29
- 32
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx View File

@@ -173,14 +173,13 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
<div className="create-project">
<div className="flex-1 huge-spacer-right">
<form className="manual-project-create" onSubmit={this.handleFormSubmit}>
{isSonarCloud() &&
this.props.userOrganizations && (
<OrganizationInput
onChange={this.handleOrganizationSelect}
organization={selectedOrganization ? selectedOrganization.key : ''}
organizations={this.props.userOrganizations}
/>
)}
{isSonarCloud() && this.props.userOrganizations && (
<OrganizationInput
onChange={this.handleOrganizationSelect}
organization={selectedOrganization ? selectedOrganization.key : ''}
organizations={this.props.userOrganizations}
/>
)}
<ProjectKeyInput
className="form-field"
initialValue={this.state.projectKey}
@@ -212,20 +211,19 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
{translate('onboarding.create_project.display_name.description')}
</div>
</div>
{isSonarCloud() &&
selectedOrganization && (
<div
className={classNames('visibility-select-wrapper', {
open: Boolean(this.state.selectedOrganization)
})}>
<VisibilitySelector
canTurnToPrivate={canChoosePrivate}
onChange={this.handleVisibilityChange}
showDetails={true}
visibility={canChoosePrivate ? this.state.selectedVisibility : 'public'}
/>
</div>
)}
{isSonarCloud() && selectedOrganization && (
<div
className={classNames('visibility-select-wrapper', {
open: Boolean(this.state.selectedOrganization)
})}>
<VisibilitySelector
canTurnToPrivate={canChoosePrivate}
onChange={this.handleVisibilityChange}
showDetails={true}
visibility={canChoosePrivate ? this.state.selectedVisibility : 'public'}
/>
</div>
)}
<SubmitButton disabled={!this.canSubmit(this.state) || submitting}>
{translate('set_up')}
</SubmitButton>
@@ -233,16 +231,15 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
</form>
</div>

{isSonarCloud() &&
selectedOrganization && (
<div className="create-project-side-sticky">
<UpgradeOrganizationBox
className={classNames('animated', { open: !canChoosePrivate })}
onOrganizationUpgrade={this.handleOrganizationUpgrade}
organization={selectedOrganization}
/>
</div>
)}
{isSonarCloud() && selectedOrganization && (
<div className="create-project-side-sticky">
<UpgradeOrganizationBox
className={classNames('animated', { open: !canChoosePrivate })}
onOrganizationUpgrade={this.handleOrganizationUpgrade}
organization={selectedOrganization}
/>
</div>
)}
</div>
);
}

+ 10
- 11
server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx View File

@@ -109,8 +109,8 @@ export default class App extends React.PureComponent<Props, State> {
return updateCustomMeasure(data).then(() => {
if (this.mounted) {
this.setState(({ measures = [] }: State) => ({
measures: measures.map(
measure => (measure.id === data.id ? { ...measure, ...data } : measure)
measures: measures.map(measure =>
measure.id === data.id ? { ...measure, ...data } : measure
)
}));
}
@@ -144,15 +144,14 @@ export default class App extends React.PureComponent<Props, State> {
{measures && (
<List measures={measures} onDelete={this.handleDelete} onEdit={this.handleEdit} />
)}
{measures &&
paging && (
<ListFooter
count={measures.length}
loadMore={this.fetchMore}
ready={!loading}
total={paging.total}
/>
)}
{measures && paging && (
<ListFooter
count={measures.length}
loadMore={this.fetchMore}
ready={!loading}
total={paging.total}
/>
)}
</div>
</>
);

+ 8
- 9
server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx View File

@@ -159,15 +159,14 @@ export default class App extends React.PureComponent<Props, State> {
types={types}
/>
)}
{metrics &&
paging && (
<ListFooter
count={metrics.length}
loadMore={this.fetchMore}
ready={!loading}
total={paging.total}
/>
)}
{metrics && paging && (
<ListFooter
count={metrics.length}
loadMore={this.fetchMore}
ready={!loading}
total={paging.total}
/>
)}
</div>
</>
);

+ 16
- 19
server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx View File

@@ -106,12 +106,11 @@ export default class Item extends React.PureComponent<Props, State> {

<td className="thin nowrap">
<ActionsDropdown>
{domains &&
types && (
<ActionsDropdownItem className="js-metric-update" onClick={this.handleEditClick}>
{translate('update_details')}
</ActionsDropdownItem>
)}
{domains && types && (
<ActionsDropdownItem className="js-metric-update" onClick={this.handleEditClick}>
{translate('update_details')}
</ActionsDropdownItem>
)}
<ActionsDropdownDivider />
<ActionsDropdownItem
className="js-metric-delete"
@@ -122,19 +121,17 @@ export default class Item extends React.PureComponent<Props, State> {
</ActionsDropdown>
</td>

{this.state.editForm &&
domains &&
types && (
<Form
confirmButtonText={translate('update_verb')}
domains={domains}
header={translate('custom_metrics.update_metric')}
metric={metric}
onClose={this.closeEditForm}
onSubmit={this.handleEditFormSubmit}
types={types}
/>
)}
{this.state.editForm && domains && types && (
<Form
confirmButtonText={translate('update_verb')}
domains={domains}
header={translate('custom_metrics.update_metric')}
metric={metric}
onClose={this.closeEditForm}
onSubmit={this.handleEditFormSubmit}
types={types}
/>
)}

{this.state.deleteForm && (
<DeleteForm

+ 2
- 3
server/sonar-web/src/main/js/apps/documentation/utils.ts View File

@@ -56,9 +56,8 @@ export function getUrlsList(navigation: DocsNavigationItem[]): string[] {
return flatten(
navigation
.filter(item => !isDocsNavigationExternalLink(item))
.map(
(item: string | DocsNavigationBlock) =>
isDocsNavigationBlock(item) ? item.children : [item]
.map((item: string | DocsNavigationBlock) =>
isDocsNavigationBlock(item) ? item.children : [item]
)
);
}

+ 10
- 11
server/sonar-web/src/main/js/apps/groups/components/App.tsx View File

@@ -165,17 +165,16 @@ export default class App extends React.PureComponent<Props, State> {
/>
)}

{groups !== undefined &&
paging !== undefined && (
<div id="groups-list-footer">
<ListFooter
count={showAnyone ? groups.length + 1 : groups.length}
loadMore={this.fetchMoreGroups}
ready={!loading}
total={showAnyone ? paging.total + 1 : paging.total}
/>
</div>
)}
{groups !== undefined && paging !== undefined && (
<div id="groups-list-footer">
<ListFooter
count={showAnyone ? groups.length + 1 : groups.length}
loadMore={this.fetchMoreGroups}
ready={!loading}
total={showAnyone ? paging.total + 1 : paging.total}
/>
</div>
)}
</div>
</>
);

+ 74
- 5
server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx View File

@@ -22,12 +22,24 @@ import { shallow } from 'enzyme';
import App from '../App';
import { mockOrganization } from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import {
createGroup,
deleteGroup,
searchUsersGroups,
updateGroup
} from '../../../../api/user_groups';

jest.mock('../../../../api/user_groups', () => ({
createGroup: jest.fn(),
deleteGroup: jest.fn(),
createGroup: jest.fn().mockResolvedValue({
default: false,
description: 'Desc foo',
id: 3,
membersCount: 0,
name: 'Foo'
}),
deleteGroup: jest.fn().mockResolvedValue({}),
searchUsersGroups: jest.fn().mockResolvedValue({
paging: { pageIndex: 1, pageSize: 100, total: 2 },
paging: { pageIndex: 1, pageSize: 2, total: 4 },
groups: [
{
default: false,
@@ -45,16 +57,73 @@ jest.mock('../../../../api/user_groups', () => ({
}
]
}),
updateGroup: jest.fn()
updateGroup: jest.fn().mockResolvedValue({})
}));

beforeEach(() => {
jest.clearAllMocks();
});

it('should render correctly', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(searchUsersGroups).toHaveBeenCalledWith({ organization: 'foo', q: '' });
expect(wrapper).toMatchSnapshot();
});

it('should correctly handle creation', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper.state('groups')).toHaveLength(2);
wrapper.instance().handleCreate({ description: 'Desc foo', name: 'foo' });
await waitAndUpdate(wrapper);
expect(createGroup).toHaveBeenCalled();
expect(wrapper.state('groups')).toHaveLength(3);
});

it('should correctly handle deletion', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper.state('groups')).toHaveLength(2);
wrapper.instance().handleDelete('Members');
await waitAndUpdate(wrapper);
expect(deleteGroup).toHaveBeenCalled();
expect(wrapper.state('groups')).toHaveLength(1);
});

it('should correctly handle edition', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.instance().handleEdit({ id: 1, description: 'foo', name: 'bar' });
await waitAndUpdate(wrapper);
expect(updateGroup).toHaveBeenCalled();
expect(wrapper.state('groups')).toContainEqual({
default: false,
description: 'foo',
id: 1,
membersCount: 1,
name: 'bar'
});
});

it('should fetch more groups', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('ListFooter').prop<Function>('loadMore')();
await waitAndUpdate(wrapper);
expect(searchUsersGroups).toHaveBeenCalledWith({ organization: 'foo', p: 2, q: '' });
expect(wrapper.state('groups')).toHaveLength(4);
});

it('should search for groups', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('SearchBox').prop<Function>('onChange')('foo');
expect(searchUsersGroups).toBeCalledWith({ organization: 'foo', q: 'foo' });
expect(wrapper.state('query')).toBe('foo');
});

function shallowRender(props: Partial<App['props']> = {}) {
return shallow(<App organization={mockOrganization()} {...props} />);
return shallow<App>(<App organization={mockOrganization()} {...props} />);
}

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

@@ -88,7 +88,7 @@ exports[`should render correctly 2`] = `
count={2}
loadMore={[Function]}
ready={true}
total={2}
total={4}
/>
</div>
</div>

+ 23
- 27
server/sonar-web/src/main/js/apps/issues/components/App.tsx View File

@@ -884,13 +884,12 @@ export class App extends React.PureComponent<Props, State> {

return (
<div className="layout-page-filters">
{currentUser.isLoggedIn &&
!isSonarCloud() && (
<MyIssuesFilter
myIssues={this.state.myIssues}
onMyIssuesChange={this.handleMyIssuesChange}
/>
)}
{currentUser.isLoggedIn && !isSonarCloud() && (
<MyIssuesFilter
myIssues={this.state.myIssues}
onMyIssuesChange={this.handleMyIssuesChange}
/>
)}
<FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} />
<Sidebar
component={component}
@@ -936,15 +935,14 @@ export class App extends React.PureComponent<Props, State> {
selectedFlowIndex={this.state.selectedFlowIndex}
selectedLocationIndex={this.state.selectedLocationIndex}
/>
{paging &&
paging.total > 0 && (
<ListFooter
count={issues.length}
loadMore={this.fetchMoreIssues}
loading={loadingMore}
total={paging.total}
/>
)}
{paging && paging.total > 0 && (
<ListFooter
count={issues.length}
loadMore={this.fetchMoreIssues}
loading={loadingMore}
total={paging.total}
/>
)}
</div>
);
}
@@ -1065,17 +1063,15 @@ export class App extends React.PureComponent<Props, State> {
/>
) : (
<DeferredSpinner loading={loading}>
{checkAll &&
paging &&
paging.total > MAX_PAGE_SIZE && (
<Alert className="big-spacer-bottom" variant="warning">
<FormattedMessage
defaultMessage={translate('issue_bulk_change.max_issues_reached')}
id="issue_bulk_change.max_issues_reached"
values={{ max: <strong>{MAX_PAGE_SIZE}</strong> }}
/>
</Alert>
)}
{checkAll && paging && paging.total > MAX_PAGE_SIZE && (
<Alert className="big-spacer-bottom" variant="warning">
<FormattedMessage
defaultMessage={translate('issue_bulk_change.max_issues_reached')}
id="issue_bulk_change.max_issues_reached"
values={{ max: <strong>{MAX_PAGE_SIZE}</strong> }}
/>
</Alert>
)}
{this.renderList()}
</DeferredSpinner>
)}

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

@@ -68,14 +68,12 @@ export default function ComponentBreadcrumbs({
</span>
)}

{displaySubProject &&
issue.subProject !== undefined &&
issue.subProjectName !== undefined && (
<span title={issue.subProjectName}>
{limitComponentName(issue.subProjectName)}
<span className="slash-separator" />
</span>
)}
{displaySubProject && issue.subProject !== undefined && issue.subProjectName !== undefined && (
<span title={issue.subProjectName}>
{limitComponentName(issue.subProjectName)}
<span className="slash-separator" />
</span>
)}

{collapsePath(componentName || '')}
</div>

+ 2
- 2
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx View File

@@ -88,8 +88,8 @@ export default class IssuesSourceViewer extends React.PureComponent<Props> {
: openIssue.textRange && openIssue.textRange.endLine;

// replace locations in another file with `undefined` to keep the same location indexes
const highlightedLocations = locations.map(
location => (location.component === component ? location : undefined)
const highlightedLocations = locations.map(location =>
location.component === component ? location : undefined
);

const highlightedLocationMessage =

+ 2
- 2
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx View File

@@ -59,8 +59,8 @@ export default class ConciseIssueBox extends React.PureComponent<Props> {
selectedFlowIndex !== undefined
? flows[selectedFlowIndex]
: flows.length > 0
? flows[0]
: secondaryLocations;
? flows[0]
: secondaryLocations;

if (!locations || locations.length < 15) {
// if there are no locations, or there are just few

+ 31
- 34
server/sonar-web/src/main/js/apps/marketplace/components/PluginActions.tsx View File

@@ -91,20 +91,18 @@ export default class PluginActions extends React.PureComponent<Props, State> {
{translate('marketplace.installed')}
</p>
)}
{isPluginInstalled(plugin) &&
plugin.updates &&
plugin.updates.length > 0 && (
<div className="spacer-top">
{plugin.updates.map((update, idx) => (
<PluginUpdateButton
disabled={this.state.loading}
key={idx}
onClick={this.handleUpdate}
update={update}
/>
))}
</div>
)}
{isPluginInstalled(plugin) && plugin.updates && plugin.updates.length > 0 && (
<div className="spacer-top">
{plugin.updates.map((update, idx) => (
<PluginUpdateButton
disabled={this.state.loading}
key={idx}
onClick={this.handleUpdate}
update={update}
/>
))}
</div>
)}
</div>
);
}
@@ -119,26 +117,25 @@ export default class PluginActions extends React.PureComponent<Props, State> {
const { loading } = this.state;
return (
<div className="js-actions">
{isPluginAvailable(plugin) &&
plugin.termsAndConditionsUrl && (
<p className="little-spacer-bottom">
<Checkbox
checked={this.state.acceptTerms}
className="js-terms"
id={'plugin-terms-' + plugin.key}
onCheck={this.handleTermsCheck}>
<label className="little-spacer-left" htmlFor={'plugin-terms-' + plugin.key}>
{translate('marketplace.i_accept_the')}
</label>
</Checkbox>
<a
className="js-plugin-terms nowrap little-spacer-left"
href={plugin.termsAndConditionsUrl}
target="_blank">
{translate('marketplace.terms_and_conditions')}
</a>
</p>
)}
{isPluginAvailable(plugin) && plugin.termsAndConditionsUrl && (
<p className="little-spacer-bottom">
<Checkbox
checked={this.state.acceptTerms}
className="js-terms"
id={'plugin-terms-' + plugin.key}
onCheck={this.handleTermsCheck}>
<label className="little-spacer-left" htmlFor={'plugin-terms-' + plugin.key}>
{translate('marketplace.i_accept_the')}
</label>
</Checkbox>
<a
className="js-plugin-terms nowrap little-spacer-left"
href={plugin.termsAndConditionsUrl}
target="_blank">
{translate('marketplace.terms_and_conditions')}
</a>
</p>
)}
{loading && <i className="spinner spacer-right little-spacer-top little-spacer-bottom" />}
{isPluginInstalled(plugin) && (
<div className="display-inlin-block">

+ 9
- 10
server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx View File

@@ -22,7 +22,7 @@ import PluginChangeLogItem from './PluginChangeLogItem';
import { Release, Update } from '../../../api/plugins';
import { translate } from '../../../helpers/l10n';

interface Props {
export interface Props {
release: Release;
update: Update;
}
@@ -33,15 +33,14 @@ export default function PluginChangeLog({ release, update }: Props) {
<h6>{translate('changelog')}</h6>
<ul className="js-plugin-changelog-list">
{update.previousUpdates &&
update.previousUpdates.map(
previousUpdate =>
previousUpdate.release ? (
<PluginChangeLogItem
key={previousUpdate.release.version}
release={previousUpdate.release}
update={previousUpdate}
/>
) : null
update.previousUpdates.map(previousUpdate =>
previousUpdate.release ? (
<PluginChangeLogItem
key={previousUpdate.release.version}
release={previousUpdate.release}
update={previousUpdate}
/>
) : null
)}
<PluginChangeLogItem release={release} update={update} />
</ul>

+ 8
- 9
server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdates.tsx View File

@@ -34,15 +34,14 @@ export default function PluginUpdates({ updates }: Props) {
<li className="spacer-top">
<strong>{translate('marketplace.updates')}:</strong>
<ul className="little-spacer-top">
{updates.map(
update =>
update.release ? (
<PluginUpdateItem
key={update.release.version}
release={update.release}
update={update}
/>
) : null
{updates.map(update =>
update.release ? (
<PluginUpdateItem
key={update.release.version}
release={update.release}
update={update}
/>
) : null
)}
</ul>
</li>

+ 57
- 0
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginChangeLog-test.tsx View File

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

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
});

function shallowRender(props: Partial<Props> = {}) {
return shallow(
<PluginChangeLog
release={{
version: '0.11',
date: '2018-11-05',
description: 'Change version description',
changeLogUrl: 'https://my.change.log/url'
}}
update={{
previousUpdates: [
{
release: {
version: '0.10',
date: '2018-06-05',
description: 'Change version description',
changeLogUrl: 'https://my.change.log/url'
},
requires: [],
status: 'COMPATIBLE'
}
],
requires: [{ key: 'java', name: 'SonarJava', description: 'Code Analyzer for Java' }],
status: 'COMPATIBLE'
}}
{...props}
/>
);
}

+ 72
- 0
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginChangeLog-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<div
className="abs-width-300"
>
<h6>
changelog
</h6>
<ul
className="js-plugin-changelog-list"
>
<PluginChangeLogItem
key="0.10"
release={
Object {
"changeLogUrl": "https://my.change.log/url",
"date": "2018-06-05",
"description": "Change version description",
"version": "0.10",
}
}
update={
Object {
"release": Object {
"changeLogUrl": "https://my.change.log/url",
"date": "2018-06-05",
"description": "Change version description",
"version": "0.10",
},
"requires": Array [],
"status": "COMPATIBLE",
}
}
/>
<PluginChangeLogItem
release={
Object {
"changeLogUrl": "https://my.change.log/url",
"date": "2018-11-05",
"description": "Change version description",
"version": "0.11",
}
}
update={
Object {
"previousUpdates": Array [
Object {
"release": Object {
"changeLogUrl": "https://my.change.log/url",
"date": "2018-06-05",
"description": "Change version description",
"version": "0.10",
},
"requires": Array [],
"status": "COMPATIBLE",
},
],
"requires": Array [
Object {
"description": "Code Analyzer for Java",
"key": "java",
"name": "SonarJava",
},
],
"status": "COMPATIBLE",
}
}
/>
</ul>
</div>
`;

+ 30
- 31
server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx View File

@@ -47,38 +47,37 @@ export default function MembersListHeader({
{total !== undefined && (
<span className="pull-right little-spacer-top">
<strong>{formatMeasure(total, 'INT')}</strong> {translate('organization.members.members')}
{organization.alm &&
organization.alm.membersSync && (
<HelpTooltip
className="spacer-left"
overlay={
<div className="abs-width-300 markdown cut-margins">
<p>
{translate(
'organization.members.auto_sync_total_help',
sanitizeAlmId(organization.alm.key)
)}
</p>
{currentUser.personalOrganization !== organization.key && (
<>
<hr />
<p>
<a
href={getAlmMembersUrl(organization.alm.key, organization.alm.url)}
rel="noopener noreferrer"
target="_blank">
{translateWithParameters(
'organization.members.see_all_members_on_x',
translate(sanitizeAlmId(organization.alm.key))
)}
</a>
</p>
</>
{organization.alm && organization.alm.membersSync && (
<HelpTooltip
className="spacer-left"
overlay={
<div className="abs-width-300 markdown cut-margins">
<p>
{translate(
'organization.members.auto_sync_total_help',
sanitizeAlmId(organization.alm.key)
)}
</div>
}
/>
)}
</p>
{currentUser.personalOrganization !== organization.key && (
<>
<hr />
<p>
<a
href={getAlmMembersUrl(organization.alm.key, organization.alm.url)}
rel="noopener noreferrer"
target="_blank">
{translateWithParameters(
'organization.members.see_all_members_on_x',
translate(sanitizeAlmId(organization.alm.key))
)}
</a>
</p>
</>
)}
</div>
}
/>
)}
</span>
)}
</div>

+ 8
- 9
server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx View File

@@ -127,15 +127,14 @@ export default class MembersListItem extends React.PureComponent<Props, State> {
/>
)}

{removeMember &&
this.state.removeMemberForm && (
<RemoveMemberForm
member={this.props.member}
onClose={this.closeRemoveMemberForm}
organization={this.props.organization}
removeMember={removeMember}
/>
)}
{removeMember && this.state.removeMemberForm && (
<RemoveMemberForm
member={this.props.member}
onClose={this.closeRemoveMemberForm}
organization={this.props.organization}
removeMember={removeMember}
/>
)}
</>
)}
</tr>

+ 24
- 28
server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx View File

@@ -50,16 +50,14 @@ export default function MembersPageHeader(props: Props) {
<DeferredSpinner loading={props.loading} />
{isAdmin && (
<div className="page-actions text-right">
{almKey &&
isGithub(almKey) &&
!showSyncNotif && (
<SyncMemberForm
buttonText={translate('organization.members.config_synchro')}
hasOtherMembers={members && members.length > 1}
organization={organization}
refreshMembers={refreshMembers}
/>
)}
{almKey && isGithub(almKey) && !showSyncNotif && (
<SyncMemberForm
buttonText={translate('organization.members.config_synchro')}
hasOtherMembers={members && members.length > 1}
organization={organization}
refreshMembers={refreshMembers}
/>
)}
{!hasMemberSync && (
<div className="display-inline-block spacer-left spacer-bottom">
<AddMemberForm
@@ -87,24 +85,22 @@ export default function MembersPageHeader(props: Props) {
)
}}
/>
{almKey &&
isGithub(almKey) &&
showSyncNotif && (
<Alert className="spacer-top" display="inline" variant="info">
{translateWithParameters(
'organization.members.auto_sync_members_from_org_x',
translate('organization', almKey)
)}
<span className="spacer-left">
<SyncMemberForm
buttonText={translate('configure')}
hasOtherMembers={members && members.length > 1}
organization={organization}
refreshMembers={refreshMembers}
/>
</span>
</Alert>
)}
{almKey && isGithub(almKey) && showSyncNotif && (
<Alert className="spacer-top" display="inline" variant="info">
{translateWithParameters(
'organization.members.auto_sync_members_from_org_x',
translate('organization', almKey)
)}
<span className="spacer-left">
<SyncMemberForm
buttonText={translate('configure')}
hasOtherMembers={members && members.length > 1}
organization={organization}
refreshMembers={refreshMembers}
/>
</span>
</Alert>
)}
</div>
</header>
);

+ 24
- 25
server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx View File

@@ -209,33 +209,32 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
organization={organization}
refreshMembers={this.refreshMembers}
/>
{members !== undefined &&
paging !== undefined && (
<>
<MembersListHeader
currentUser={currentUser}
handleSearch={this.handleSearchMembers}
organization={organization}
{members !== undefined && paging !== undefined && (
<>
<MembersListHeader
currentUser={currentUser}
handleSearch={this.handleSearchMembers}
organization={organization}
total={paging.total}
/>
<MembersList
currentUser={currentUser}
members={members}
organization={organization}
organizationGroups={groups}
removeMember={hasMemberSync ? undefined : this.handleRemoveMember}
updateMemberGroups={this.updateMemberGroups}
/>
{paging.total !== 0 && (
<ListFooter
count={members.length}
loadMore={this.handleLoadMoreMembers}
ready={!loading}
total={paging.total}
/>
<MembersList
currentUser={currentUser}
members={members}
organization={organization}
organizationGroups={groups}
removeMember={hasMemberSync ? undefined : this.handleRemoveMember}
updateMemberGroups={this.updateMemberGroups}
/>
{paging.total !== 0 && (
<ListFooter
count={members.length}
loadMore={this.handleLoadMoreMembers}
ready={!loading}
total={paging.total}
/>
)}
</>
)}
)}
</>
)}
</div>
);
}

+ 8
- 9
server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx View File

@@ -139,15 +139,14 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
<li>{translate('organization.members.management.choose_members_permissions')}</li>
</ul>
</div>
{almKey &&
showWarning && (
<Alert className="big-spacer-top" variant="warning">
{translateWithParameters(
'organization.members.management.automatic.warning_x',
translate('organization', almKey)
)}
</Alert>
)}
{almKey && showWarning && (
<Alert className="big-spacer-top" variant="warning">
{translateWithParameters(
'organization.members.management.automatic.warning_x',
translate('organization', almKey)
)}
</Alert>
)}
</RadioCard>
</div>
);

+ 3
- 2
server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx View File

@@ -70,8 +70,9 @@ export function EmptyOverview({
}
/>
)}
{!hasBranches &&
!hasAnalyses && <AnalyzeTutorial component={component} currentUser={currentUser} />}
{!hasBranches && !hasAnalyses && (
<AnalyzeTutorial component={component} currentUser={currentUser} />
)}
</>
) : (
<WarningMessage

+ 15
- 18
server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx View File

@@ -82,14 +82,13 @@ export class Meta extends React.PureComponent<Props> {
/>
)}

{qualityProfiles &&
qualityProfiles.length > 0 && (
<MetaQualityProfiles
headerClassName={qualityGate ? 'big-spacer-top' : undefined}
organization={organizationsEnabled ? component.organization : undefined}
profiles={qualityProfiles}
/>
)}
{qualityProfiles && qualityProfiles.length > 0 && (
<MetaQualityProfiles
headerClassName={qualityGate ? 'big-spacer-top' : undefined}
organization={organizationsEnabled ? component.organization : undefined}
profiles={qualityProfiles}
/>
)}
</div>
);
}
@@ -145,16 +144,14 @@ export class Meta extends React.PureComponent<Props> {
{organizationsEnabled && <MetaOrganizationKey organization={component.organization} />}
</div>

{!isPrivate &&
(isProject || isApp) &&
metrics && (
<BadgesModal
branchLike={branchLike}
metrics={metrics}
project={component.key}
qualifier={component.qualifier}
/>
)}
{!isPrivate && (isProject || isApp) && metrics && (
<BadgesModal
branchLike={branchLike}
metrics={metrics}
project={component.key}
qualifier={component.qualifier}
/>
)}
</div>
);
}

+ 9
- 8
server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx View File

@@ -104,20 +104,21 @@ export default class ApplicationQualityGate extends React.PureComponent<Props, S
)}
</h2>

{projects &&
metrics && (
<div
className="overview-quality-gate-conditions-list clearfix"
id="overview-quality-gate-conditions-list">
{projects.filter(project => project.status !== 'OK').map(project => (
{projects && metrics && (
<div
className="overview-quality-gate-conditions-list clearfix"
id="overview-quality-gate-conditions-list">
{projects
.filter(project => project.status !== 'OK')
.map(project => (
<ApplicationQualityGateProject
key={project.key}
metrics={metrics}
project={project}
/>
))}
</div>
)}
</div>
)}
</div>
);
}

+ 5
- 6
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.tsx View File

@@ -160,12 +160,11 @@ export default class QualityGateCondition extends React.PureComponent<Props> {
<IssueTypeIcon className="little-spacer-right" query={metric.key} />
{metric.name}
</div>
{!isDiff &&
condition.period != null && (
<div className="overview-quality-gate-condition-period">
{translate('quality_gates.conditions.new_code')}
</div>
)}
{!isDiff && condition.period != null && (
<div className="overview-quality-gate-condition-period">
{translate('quality_gates.conditions.new_code')}
</div>
)}
<div className="overview-quality-gate-threshold">
{operator} {formatMeasure(threshold, metric.type)}
</div>

+ 5
- 3
server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx View File

@@ -27,9 +27,11 @@ interface Props {
confirmButtonText: string;
header: string;
onClose: () => void;
onSubmit: (
data: { description: string; name: string; projectKeyPattern: string }
) => Promise<void>;
onSubmit: (data: {
description: string;
name: string;
projectKeyPattern: string;
}) => Promise<void>;
permissionTemplate?: { description?: string; name: string; projectKeyPattern?: string };
}


+ 16
- 20
server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx View File

@@ -132,38 +132,34 @@ export default class App extends React.PureComponent<Props, State> {
};

addPermissionToGroup = (groups: T.PermissionGroup[], group: string, permission: string) => {
return groups.map(
candidate =>
candidate.name === group
? { ...candidate, permissions: [...candidate.permissions, permission] }
: candidate
return groups.map(candidate =>
candidate.name === group
? { ...candidate, permissions: [...candidate.permissions, permission] }
: candidate
);
};

addPermissionToUser = (users: T.PermissionUser[], user: string, permission: string) => {
return users.map(
candidate =>
candidate.login === user
? { ...candidate, permissions: [...candidate.permissions, permission] }
: candidate
return users.map(candidate =>
candidate.login === user
? { ...candidate, permissions: [...candidate.permissions, permission] }
: candidate
);
};

removePermissionFromGroup = (groups: T.PermissionGroup[], group: string, permission: string) => {
return groups.map(
candidate =>
candidate.name === group
? { ...candidate, permissions: without(candidate.permissions, permission) }
: candidate
return groups.map(candidate =>
candidate.name === group
? { ...candidate, permissions: without(candidate.permissions, permission) }
: candidate
);
};

removePermissionFromUser = (users: T.PermissionUser[], user: string, permission: string) => {
return users.map(
candidate =>
candidate.login === user
? { ...candidate, permissions: without(candidate.permissions, permission) }
: candidate
return users.map(candidate =>
candidate.login === user
? { ...candidate, permissions: without(candidate.permissions, permission) }
: candidate
);
};


+ 134
- 0
server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/App-test.tsx View File

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

jest.mock('../../../../../api/permissions', () => ({
getGlobalPermissionsGroups: jest.fn().mockResolvedValue({
paging: { pageIndex: 1, pageSize: 100, total: 2 },
groups: [
{ name: 'Anyone', permissions: ['admin', 'codeviewer', 'issueadmin'] },
{ id: '1', name: 'SonarSource', description: 'SonarSource team', permissions: [] }
]
}),
getGlobalPermissionsUsers: jest.fn().mockResolvedValue({
paging: { pageIndex: 1, pageSize: 100, total: 3 },
users: [
{
avatar: 'admin-avatar',
email: 'admin@gmail.com',
login: 'admin',
name: 'Admin Admin',
permissions: ['admin']
},
{
avatar: 'user-avatar-1',
email: 'user1@gmail.com',
login: 'user1',
name: 'User Number 1',
permissions: []
},
{
avatar: 'user-avatar-2',
email: 'user2@gmail.com',
login: 'user2',
name: 'User Number 2',
permissions: []
}
]
}),
grantPermissionToGroup: jest.fn().mockResolvedValue({}),
grantPermissionToUser: jest.fn().mockResolvedValue({}),
revokePermissionFromGroup: jest.fn().mockResolvedValue({}),
revokePermissionFromUser: jest.fn().mockResolvedValue({})
}));

beforeEach(() => {
jest.clearAllMocks();
});

it('should render correctly', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});

describe('should manage state correctly', () => {
it('should add and remove permission to a group', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
const instance = wrapper.instance();
const apiPayload = { groupName: 'Anyone', permission: 'foo', organization: 'foo' };

instance.grantPermissionToGroup('Anyone', 'foo');
const groupState = wrapper.state('groups');
expect(groupState[0].permissions).toHaveLength(4);
expect(groupState[0].permissions).toContain('foo');
await waitAndUpdate(wrapper);
expect(grantPermissionToGroup).toHaveBeenCalledWith(apiPayload);
expect(wrapper.state('groups')).toBe(groupState);

(grantPermissionToGroup as jest.Mock).mockRejectedValueOnce({});
instance.grantPermissionToGroup('Anyone', 'bar');
expect(wrapper.state('groups')[0].permissions).toHaveLength(5);
expect(wrapper.state('groups')[0].permissions).toContain('bar');
await waitAndUpdate(wrapper);
expect(wrapper.state('groups')[0].permissions).toHaveLength(4);
expect(wrapper.state('groups')[0].permissions).not.toContain('bar');

instance.revokePermissionFromGroup('Anyone', 'foo');
expect(wrapper.state('groups')[0].permissions).toHaveLength(3);
expect(wrapper.state('groups')[0].permissions).not.toContain('foo');
await waitAndUpdate(wrapper);
expect(revokePermissionFromGroup).toHaveBeenCalledWith(apiPayload);
});

it('should add and remove permission to a user', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
const instance = wrapper.instance();
const apiPayload = { login: 'user1', permission: 'foo', organization: 'foo' };

instance.grantPermissionToUser('user1', 'foo');
expect(wrapper.state('users')[1].permissions).toHaveLength(1);
expect(wrapper.state('users')[1].permissions).toContain('foo');
await waitAndUpdate(wrapper);
expect(grantPermissionToUser).toHaveBeenCalledWith(apiPayload);

instance.revokePermissionFromUser('user1', 'foo');
expect(wrapper.state('users')[1].permissions).toHaveLength(0);
await waitAndUpdate(wrapper);
expect(revokePermissionFromUser).toHaveBeenCalledWith(apiPayload);
});
});

function shallowRender(props: Partial<App['props']> = {}) {
return shallow<App>(<App organization={mockOrganization()} {...props} />);
}

+ 136
- 0
server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/__snapshots__/App-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<div
className="page page-limited"
>
<Suggestions
suggestions="global_permissions"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="global_permissions.permission"
/>
<PageHeader
loading={true}
organization={
Object {
"key": "foo",
"name": "Foo",
}
}
/>
<Connect(AllHoldersList)
filter="all"
grantPermissionToGroup={[Function]}
grantPermissionToUser={[Function]}
groups={Array []}
loadHolders={[Function]}
loading={true}
onFilter={[Function]}
onLoadMore={[Function]}
onSearch={[Function]}
query=""
revokePermissionFromGroup={[Function]}
revokePermissionFromUser={[Function]}
users={Array []}
/>
</div>
`;

exports[`should render correctly 2`] = `
<div
className="page page-limited"
>
<Suggestions
suggestions="global_permissions"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="global_permissions.permission"
/>
<PageHeader
loading={false}
organization={
Object {
"key": "foo",
"name": "Foo",
}
}
/>
<Connect(AllHoldersList)
filter="all"
grantPermissionToGroup={[Function]}
grantPermissionToUser={[Function]}
groups={
Array [
Object {
"name": "Anyone",
"permissions": Array [
"admin",
"codeviewer",
"issueadmin",
],
},
Object {
"description": "SonarSource team",
"id": "1",
"name": "SonarSource",
"permissions": Array [],
},
]
}
groupsPaging={
Object {
"pageIndex": 1,
"pageSize": 100,
"total": 2,
}
}
loadHolders={[Function]}
loading={false}
onFilter={[Function]}
onLoadMore={[Function]}
onSearch={[Function]}
query=""
revokePermissionFromGroup={[Function]}
revokePermissionFromUser={[Function]}
users={
Array [
Object {
"avatar": "admin-avatar",
"email": "admin@gmail.com",
"login": "admin",
"name": "Admin Admin",
"permissions": Array [
"admin",
],
},
Object {
"avatar": "user-avatar-1",
"email": "user1@gmail.com",
"login": "user1",
"name": "User Number 1",
"permissions": Array [],
},
Object {
"avatar": "user-avatar-2",
"email": "user2@gmail.com",
"login": "user2",
"name": "User Number 2",
"permissions": Array [],
},
]
}
usersPaging={
Object {
"pageIndex": 1,
"pageSize": 100,
"total": 3,
}
}
/>
</div>
`;

+ 23
- 28
server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx View File

@@ -166,38 +166,34 @@ export default class App extends React.PureComponent<Props, State> {
};

addPermissionToGroup = (group: string, permission: string) => {
return this.state.groups.map(
candidate =>
candidate.name === group
? { ...candidate, permissions: [...candidate.permissions, permission] }
: candidate
return this.state.groups.map(candidate =>
candidate.name === group
? { ...candidate, permissions: [...candidate.permissions, permission] }
: candidate
);
};

addPermissionToUser = (user: string, permission: string) => {
return this.state.users.map(
candidate =>
candidate.login === user
? { ...candidate, permissions: [...candidate.permissions, permission] }
: candidate
return this.state.users.map(candidate =>
candidate.login === user
? { ...candidate, permissions: [...candidate.permissions, permission] }
: candidate
);
};

removePermissionFromGroup = (group: string, permission: string) => {
return this.state.groups.map(
candidate =>
candidate.name === group
? { ...candidate, permissions: without(candidate.permissions, permission) }
: candidate
return this.state.groups.map(candidate =>
candidate.name === group
? { ...candidate, permissions: without(candidate.permissions, permission) }
: candidate
);
};

removePermissionFromUser = (user: string, permission: string) => {
return this.state.users.map(
candidate =>
candidate.login === user
? { ...candidate, permissions: without(candidate.permissions, permission) }
: candidate
return this.state.users.map(candidate =>
candidate.login === user
? { ...candidate, permissions: without(candidate.permissions, permission) }
: candidate
);
};

@@ -389,14 +385,13 @@ export default class App extends React.PureComponent<Props, State> {
onChange={this.handleVisibilityChange}
visibility={component.visibility}
/>
{showUpgradeBox &&
organization && (
<UpgradeOrganizationBox
className="big-spacer-bottom"
onOrganizationUpgrade={this.handleOrganizationUpgrade}
organization={organization}
/>
)}
{showUpgradeBox && organization && (
<UpgradeOrganizationBox
className="big-spacer-bottom"
onOrganizationUpgrade={this.handleOrganizationUpgrade}
organization={organization}
/>
)}
{this.state.disclaimer && (
<PublicProjectDisclaimer
component={component}

+ 162
- 0
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx View File

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

jest.mock('../../../../../api/permissions', () => ({
getPermissionsGroupsForComponent: jest.fn().mockResolvedValue({
paging: { pageIndex: 1, pageSize: 100, total: 2 },
groups: [
{ name: 'Anyone', permissions: ['admin', 'codeviewer', 'issueadmin'] },
{ id: '1', name: 'SonarSource', description: 'SonarSource team', permissions: [] }
]
}),
getPermissionsUsersForComponent: jest.fn().mockResolvedValue({
paging: { pageIndex: 1, pageSize: 100, total: 3 },
users: [
{
avatar: 'admin-avatar',
email: 'admin@gmail.com',
login: 'admin',
name: 'Admin Admin',
permissions: ['admin']
},
{
avatar: 'user-avatar-1',
email: 'user1@gmail.com',
login: 'user1',
name: 'User Number 1',
permissions: []
},
{
avatar: 'user-avatar-2',
email: 'user2@gmail.com',
login: 'user2',
name: 'User Number 2',
permissions: []
}
]
}),
grantPermissionToGroup: jest.fn().mockResolvedValue({}),
grantPermissionToUser: jest.fn().mockResolvedValue({}),
revokePermissionFromGroup: jest.fn().mockResolvedValue({}),
revokePermissionFromUser: jest.fn().mockResolvedValue({})
}));

beforeEach(() => {
jest.clearAllMocks();
});

it('should render correctly', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
});

describe('should manage state correctly', () => {
it('should handle permission select', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
const instance = wrapper.instance();
instance.handlePermissionSelect('foo');
expect(wrapper.state('selectedPermission')).toBe('foo');
instance.handlePermissionSelect('foo');
expect(wrapper.state('selectedPermission')).toBe(undefined);
});

it('should add and remove permission to a group', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
const instance = wrapper.instance();
const apiPayload = {
projectKey: 'my-project',
groupName: 'Anyone',
permission: 'foo',
organization: 'foo'
};

instance.grantPermissionToGroup('Anyone', 'foo');
const groupState = wrapper.state('groups');
expect(groupState[0].permissions).toHaveLength(4);
expect(groupState[0].permissions).toContain('foo');
await waitAndUpdate(wrapper);
expect(grantPermissionToGroup).toHaveBeenCalledWith(apiPayload);
expect(wrapper.state('groups')).toBe(groupState);

(grantPermissionToGroup as jest.Mock).mockRejectedValueOnce({});
instance.grantPermissionToGroup('Anyone', 'bar');
expect(wrapper.state('groups')[0].permissions).toHaveLength(5);
expect(wrapper.state('groups')[0].permissions).toContain('bar');
await waitAndUpdate(wrapper);
expect(wrapper.state('groups')[0].permissions).toHaveLength(4);
expect(wrapper.state('groups')[0].permissions).not.toContain('bar');

instance.revokePermissionFromGroup('Anyone', 'foo');
expect(wrapper.state('groups')[0].permissions).toHaveLength(3);
expect(wrapper.state('groups')[0].permissions).not.toContain('foo');
await waitAndUpdate(wrapper);
expect(revokePermissionFromGroup).toHaveBeenCalledWith(apiPayload);
});

it('should add and remove permission to a user', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
const instance = wrapper.instance();
const apiPayload = {
projectKey: 'my-project',
login: 'user1',
permission: 'foo',
organization: 'foo'
};

instance.grantPermissionToUser('user1', 'foo');
expect(wrapper.state('users')[1].permissions).toHaveLength(1);
expect(wrapper.state('users')[1].permissions).toContain('foo');
await waitAndUpdate(wrapper);
expect(grantPermissionToUser).toHaveBeenCalledWith(apiPayload);

instance.revokePermissionFromUser('user1', 'foo');
expect(wrapper.state('users')[1].permissions).toHaveLength(0);
await waitAndUpdate(wrapper);
expect(revokePermissionFromUser).toHaveBeenCalledWith(apiPayload);
});
});

function shallowRender(props: Partial<App['props']> = {}) {
return shallow<App>(
<App
component={mockComponent()}
fetchOrganization={jest.fn()}
onComponentChange={jest.fn()}
organization={mockOrganization()}
{...props}
/>
);
}

+ 224
- 0
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<div
className="page page-limited"
id="project-permissions-page"
>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="permissions.page"
/>
<PageHeader
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
loadHolders={[Function]}
loading={true}
/>
<div>
<VisibilitySelector
className="big-spacer-top big-spacer-bottom"
onChange={[Function]}
/>
</div>
<AllHoldersList
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
filter="all"
grantPermissionToGroup={[Function]}
grantPermissionToUser={[Function]}
groups={Array []}
onFilterChange={[Function]}
onLoadMore={[Function]}
onPermissionSelect={[Function]}
onQueryChange={[Function]}
query=""
revokePermissionFromGroup={[Function]}
revokePermissionFromUser={[Function]}
users={Array []}
/>
</div>
`;

exports[`should render correctly 2`] = `
<div
className="page page-limited"
id="project-permissions-page"
>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="permissions.page"
/>
<PageHeader
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
loadHolders={[Function]}
loading={false}
/>
<div>
<VisibilitySelector
className="big-spacer-top big-spacer-bottom"
onChange={[Function]}
/>
</div>
<AllHoldersList
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
filter="all"
grantPermissionToGroup={[Function]}
grantPermissionToUser={[Function]}
groups={
Array [
Object {
"name": "Anyone",
"permissions": Array [
"admin",
"codeviewer",
"issueadmin",
],
},
Object {
"description": "SonarSource team",
"id": "1",
"name": "SonarSource",
"permissions": Array [],
},
]
}
groupsPaging={
Object {
"pageIndex": 1,
"pageSize": 100,
"total": 2,
}
}
onFilterChange={[Function]}
onLoadMore={[Function]}
onPermissionSelect={[Function]}
onQueryChange={[Function]}
query=""
revokePermissionFromGroup={[Function]}
revokePermissionFromUser={[Function]}
users={
Array [
Object {
"avatar": "admin-avatar",
"email": "admin@gmail.com",
"login": "admin",
"name": "Admin Admin",
"permissions": Array [
"admin",
],
},
Object {
"avatar": "user-avatar-1",
"email": "user1@gmail.com",
"login": "user1",
"name": "User Number 1",
"permissions": Array [],
},
Object {
"avatar": "user-avatar-2",
"email": "user2@gmail.com",
"login": "user2",
"name": "User Number 2",
"permissions": Array [],
},
]
}
usersPaging={
Object {
"pageIndex": 1,
"pageSize": 100,
"total": 3,
}
}
/>
</div>
`;

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

@@ -148,16 +148,15 @@ export default class HoldersList extends React.PureComponent<Props, State> {
<tbody>
{items.length === 0 && !this.props.loading && this.renderEmpty()}
{itemWithPermissions.map(item => this.renderItem(item, permissions))}
{itemWithPermissions.length > 0 &&
itemWithoutPermissions.length > 0 && (
<>
<tr>
<td className="divider" colSpan={20} />
</tr>
<tr />
{/* Keep correct zebra colors in the table */}
</>
)}
{itemWithPermissions.length > 0 && itemWithoutPermissions.length > 0 && (
<>
<tr>
<td className="divider" colSpan={20} />
</tr>
<tr />
{/* Keep correct zebra colors in the table */}
</>
)}
{itemWithoutPermissions.map(item => this.renderItem(item, permissions))}
</tbody>
</table>

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

@@ -161,14 +161,13 @@ export class App extends React.PureComponent<Props, State> {
<MaintainabilityBox component={component.key} measures={measures!} />
</div>

{subComponents !== undefined &&
totalSubComponents !== undefined && (
<WorstProjects
component={component.key}
subComponents={subComponents}
total={totalSubComponents}
/>
)}
{subComponents !== undefined && totalSubComponents !== undefined && (
<WorstProjects
component={component.key}
subComponents={subComponents}
total={totalSubComponents}
/>
)}
</div>
);
}

+ 16
- 18
server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx View File

@@ -50,24 +50,22 @@ export default function ReleasabilityBox({ component, measures }: Props) {

<RatingFreshness lastChange={lastReleasabilityChange} rating={rating} />

{effort &&
Number(effort) > 0 && (
<div className="portfolio-effort">
<Link
to={getComponentDrilldownUrl({ componentKey: component, metric: 'alert_status' })}>
<span>
<Measure
className="little-spacer-right"
metricKey="projects"
metricType="SHORT_INT"
value={effort}
/>
{Number(effort) === 1 ? 'project' : 'projects'}
</span>
</Link>{' '}
<Level level="ERROR" small={true} />
</div>
)}
{effort && Number(effort) > 0 && (
<div className="portfolio-effort">
<Link to={getComponentDrilldownUrl({ componentKey: component, metric: 'alert_status' })}>
<span>
<Measure
className="little-spacer-right"
metricKey="projects"
metricType="SHORT_INT"
value={effort}
/>
{Number(effort) === 1 ? 'project' : 'projects'}
</span>
</Link>{' '}
<Level level="ERROR" small={true} />
</div>
)}
</div>
);
}

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/actions.ts View File

@@ -49,8 +49,8 @@ export function changeEvent(analysis: string, event: T.AnalysisEvent) {
}
return {
...item,
events: item.events.map(
eventItem => (eventItem.key === event.key ? { ...eventItem, ...event } : eventItem)
events: item.events.map(eventItem =>
eventItem.key === event.key ? { ...eventItem, ...event } : eventItem
)
};
})

+ 3
- 4
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx View File

@@ -113,10 +113,9 @@ export default class GraphsTooltips extends React.PureComponent<Props> {
tooltipIdx={tooltipIdx}
/>
)}
{events &&
events.length > 0 && (
<GraphsTooltipsContentEvents addSeparator={addSeparator} events={events} />
)}
{events && events.length > 0 && (
<GraphsTooltipsContentEvents addSeparator={addSeparator} events={events} />
)}
</table>
</div>
</Popup>

+ 37
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityEventSelectOption-test.tsx View File

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

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

function shallowRender(props: Partial<ProjectActivityEventSelectOption['props']> = {}) {
return shallow(
<ProjectActivityEventSelectOption
onFocus={jest.fn()}
onSelect={jest.fn()}
option={{ label: 'Foo', value: 'foo' }}
{...props}
/>
);
}

+ 19
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityEventSelectOption-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<div
onMouseDown={[Function]}
onMouseEnter={[Function]}
onMouseMove={[Function]}
role="link"
tabIndex={0}
title="Foo"
>
<ProjectEventIcon
className="project-activity-event-icon foo"
/>
<span
className="little-spacer-left"
/>
</div>
`;

+ 15
- 17
server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx View File

@@ -134,24 +134,22 @@ export default class BranchRow extends React.PureComponent<Props, State> {
/>
)}

{this.state.renaming &&
isMainBranch(branchLike) && (
<RenameBranchModal
branch={branchLike}
component={component}
onClose={this.handleRenamingStop}
onRename={this.handleChange}
/>
)}
{this.state.renaming && isMainBranch(branchLike) && (
<RenameBranchModal
branch={branchLike}
component={component}
onClose={this.handleRenamingStop}
onRename={this.handleChange}
/>
)}

{this.state.changingLeak &&
isLongLivingBranch(branchLike) && (
<LeakPeriodForm
branch={branchLike.name}
onClose={this.handleChangingLeakStop}
project={component}
/>
)}
{this.state.changingLeak && isLongLivingBranch(branchLike) && (
<LeakPeriodForm
branch={branchLike.name}
onClose={this.handleChangingLeakStop}
project={component}
/>
)}
</td>
);
}

+ 14
- 16
server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx View File

@@ -110,25 +110,23 @@ export default class SettingForm extends React.PureComponent<Props, State> {
{setting.inherited && (
<div className="modal-field-description">{translate('settings._default')}</div>
)}
{!setting.inherited &&
setting.parentValue && (
<div className="modal-field-description">
{translateWithParameters('settings.default_x', setting.parentValue)}
</div>
)}
{!setting.inherited && setting.parentValue && (
<div className="modal-field-description">
{translateWithParameters('settings.default_x', setting.parentValue)}
</div>
)}
</div>
</div>
<footer className="modal-foot">
{!setting.inherited &&
setting.parentValue && (
<Button
className="pull-left"
disabled={this.state.submitting}
onClick={this.handleResetClick}
type="reset">
{translate('reset_to_default')}
</Button>
)}
{!setting.inherited && setting.parentValue && (
<Button
className="pull-left"
disabled={this.state.submitting}
onClick={this.handleResetClick}
type="reset">
{translate('reset_to_default')}
</Button>
)}
{this.state.submitting && <i className="spinner spacer-right" />}
<SubmitButton disabled={submitDisabled}>{translate('save')}</SubmitButton>
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>

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

@@ -74,21 +74,18 @@ export default function ProjectCardLeak({ height, organization, project }: Props
{hasTags && <TagsList className="spacer-left note" tags={project.tags} />}
</div>
</div>
{project.analysisDate &&
project.leakPeriodDate && (
<div className="project-card-dates note text-right pull-right">
<span className="project-card-leak-date pull-right">
{translateWithParameters('projects.new_code_period_x', formatDuration(periodMs))}
</span>
<DateTimeFormatter date={project.analysisDate}>
{formattedDate => (
<span>
{translateWithParameters('projects.last_analysis_on_x', formattedDate)}
</span>
)}
</DateTimeFormatter>
</div>
)}
{project.analysisDate && project.leakPeriodDate && (
<div className="project-card-dates note text-right pull-right">
<span className="project-card-leak-date pull-right">
{translateWithParameters('projects.new_code_period_x', formatDuration(periodMs))}
</span>
<DateTimeFormatter date={project.analysisDate}>
{formattedDate => (
<span>{translateWithParameters('projects.last_analysis_on_x', formattedDate)}</span>
)}
</DateTimeFormatter>
</div>
)}
</div>

{project.analysisDate && project.leakPeriodDate ? (

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx View File

@@ -105,8 +105,8 @@ it('fetches projects', () => {
});

it('redirects to the saved search', () => {
(get as jest.Mock).mockImplementation(
(key: string) => (key === 'sonarqube.projects.view' ? 'leak' : null)
(get as jest.Mock).mockImplementation((key: string) =>
key === 'sonarqube.projects.view' ? 'leak' : null
);
const replace = jest.fn();
shallowRender({}, jest.fn(), replace);

+ 10
- 8
server/sonar-web/src/main/js/apps/projects/utils.ts View File

@@ -187,14 +187,16 @@ export function fetchProjects(
projects: components
.map(component => {
const componentMeasures: T.Dict<string> = {};
measures.filter(measure => measure.component === component.key).forEach(measure => {
const value = isDiffMetric(measure.metric)
? getPeriodValue(measure, 1)
: measure.value;
if (value !== undefined) {
componentMeasures[measure.metric] = value;
}
});
measures
.filter(measure => measure.component === component.key)
.forEach(measure => {
const value = isDiffMetric(measure.metric)
? getPeriodValue(measure, 1)
: measure.value;
if (value !== undefined) {
componentMeasures[measure.metric] = value;
}
});
return { ...component, measures: componentMeasures };
})
.map(component => {

+ 5
- 7
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx View File

@@ -170,13 +170,11 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S

<footer className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
{!loading &&
!done &&
permissionTemplates && (
<Button disabled={submitting} onClick={this.handleConfirmClick}>
{translate('apply')}
</Button>
)}
{!loading && !done && permissionTemplates && (
<Button disabled={submitting} onClick={this.handleConfirmClick}>
{translate('apply')}
</Button>
)}
<ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
{done ? translate('close') : translate('cancel')}
</ResetButtonLink>

+ 18
- 20
server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx View File

@@ -53,19 +53,18 @@ export default class Header extends React.PureComponent<Props, State> {
<h1 className="page-title">{translate('projects_management')}</h1>

<div className="page-actions">
{!isSonarCloud() &&
organization.projectVisibility && (
<span className="big-spacer-right">
<span className="text-middle">
{translate('organization.default_visibility_of_new_projects')}{' '}
<strong>{translate('visibility', organization.projectVisibility)}</strong>
</span>
<EditButton
className="js-change-visibility spacer-left button-small"
onClick={this.handleChangeVisibilityClick}
/>
{!isSonarCloud() && organization.projectVisibility && (
<span className="big-spacer-right">
<span className="text-middle">
{translate('organization.default_visibility_of_new_projects')}{' '}
<strong>{translate('visibility', organization.projectVisibility)}</strong>
</span>
)}
<EditButton
className="js-change-visibility spacer-left button-small"
onClick={this.handleChangeVisibilityClick}
/>
</span>
)}
{this.props.hasProvisionPermission && (
<Button id="create-project" onClick={this.props.onProjectCreate}>
{translate('qualifiers.create.TRK')}
@@ -75,14 +74,13 @@ export default class Header extends React.PureComponent<Props, State> {

<p className="page-description">{translate('projects_management.page.description')}</p>

{!isSonarCloud() &&
this.state.visibilityForm && (
<ChangeDefaultVisibilityForm
onClose={this.closeVisiblityForm}
onConfirm={this.props.onVisibilityChange}
organization={organization}
/>
)}
{!isSonarCloud() && this.state.visibilityForm && (
<ChangeDefaultVisibilityForm
onClose={this.closeVisiblityForm}
onConfirm={this.props.onVisibilityChange}
organization={organization}
/>
)}
</header>
);
}

+ 3
- 4
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx View File

@@ -160,10 +160,9 @@ class ChangelogContainer extends React.PureComponent<Props, State> {

{this.state.events != null && this.state.events.length === 0 && <ChangelogEmpty />}

{this.state.events != null &&
this.state.events.length > 0 && (
<Changelog events={this.state.events} organization={this.props.organization} />
)}
{this.state.events != null && this.state.events.length > 0 && (
<Changelog events={this.state.events} organization={this.props.organization} />
)}

{shouldDisplayFooter && (
<footer className="text-center spacer-top small">

+ 3
- 5
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx View File

@@ -45,11 +45,9 @@ export default function ProfileDetails(props: Props) {
organization={organization}
profile={profile}
/>
{profile.actions &&
profile.actions.edit &&
!profile.isBuiltIn && (
<ProfilePermissions organization={organization || undefined} profile={profile} />
)}
{profile.actions && profile.actions.edit && !profile.isBuiltIn && (
<ProfilePermissions organization={organization || undefined} profile={profile} />
)}
</div>
<div className="quality-profile-grid-right">
<ProfileInheritance

+ 7
- 11
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx View File

@@ -130,17 +130,13 @@ export default class ProfileInheritance extends React.PureComponent<Props, State

return (
<div className="boxed-group quality-profile-inheritance">
{profile.actions &&
profile.actions.edit &&
!profile.isBuiltIn && (
<div className="boxed-group-actions">
<Button
className="pull-right js-change-parent"
onClick={this.handleChangeParentClick}>
{translate('quality_profiles.change_parent')}
</Button>
</div>
)}
{profile.actions && profile.actions.edit && !profile.isBuiltIn && (
<div className="boxed-group-actions">
<Button className="pull-right js-change-parent" onClick={this.handleChangeParentClick}>
{translate('quality_profiles.change_parent')}
</Button>
</div>
)}

<header className="boxed-group-header">
<h2>{translate('quality_profiles.profile_inheritance')}</h2>

+ 7
- 8
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx View File

@@ -163,14 +163,13 @@ export default class ProfileProjects extends React.PureComponent<Props, State> {
const { profile } = this.props;
return (
<div className="boxed-group quality-profile-projects">
{profile.actions &&
profile.actions.associateProjects && (
<div className="boxed-group-actions">
<Button className="js-change-projects" onClick={this.handleChangeClick}>
{translate('quality_profiles.change_projects')}
</Button>
</div>
)}
{profile.actions && profile.actions.associateProjects && (
<div className="boxed-group-actions">
<Button className="js-change-projects" onClick={this.handleChangeClick}>
{translate('quality_profiles.change_projects')}
</Button>
</div>
)}

<header className="boxed-group-header">
<h2>{translate('projects')}</h2>

+ 26
- 38
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx View File

@@ -49,7 +49,6 @@ interface State {
activatedByType: T.Dict<ByType>;
allByType: T.Dict<ByType>;
compareToSonarWay: { profile: string; profileName: string; missingRuleCount: number } | null;
loading: boolean;
total: number | null;
}

@@ -61,7 +60,6 @@ export default class ProfileRules extends React.PureComponent<Props, State> {
activatedByType: keyBy(TYPES.map(t => ({ val: t, count: null })), 'val'),
allByType: keyBy(TYPES.map(t => ({ val: t, count: null })), 'val'),
compareToSonarWay: null,
loading: true,
total: null
};

@@ -110,7 +108,6 @@ export default class ProfileRules extends React.PureComponent<Props, State> {
}

loadRules() {
this.setState({ loading: true });
Promise.all([this.loadAllRules(), this.loadActivatedRules(), this.loadProfile()]).then(
responses => {
if (this.mounted) {
@@ -120,15 +117,9 @@ export default class ProfileRules extends React.PureComponent<Props, State> {
allByType: keyBy<ByType>(takeFacet(allRules, 'types'), 'val'),
activatedByType: keyBy<ByType>(takeFacet(activatedRules, 'types'), 'val'),
compareToSonarWay: showProfile && showProfile.compareToSonarWay,
loading: false,
total: allRules.total
});
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
}
@@ -187,29 +178,27 @@ export default class ProfileRules extends React.PureComponent<Props, State> {
</tbody>
</table>

{actions.edit &&
!profile.isBuiltIn && (
<div className="text-right big-spacer-top">
<Link className="button js-activate-rules" to={activateMoreUrl}>
{translate('quality_profiles.activate_more')}
</Link>
</div>
)}
{actions.edit && !profile.isBuiltIn && (
<div className="text-right big-spacer-top">
<Link className="button js-activate-rules" to={activateMoreUrl}>
{translate('quality_profiles.activate_more')}
</Link>
</div>
)}

{/* if a user is allowed to `copy` a profile if they are a global or organization admin */}
{/* this user could potentially active more rules if the profile was not built-in */}
{/* in such cases it's better to show the button but disable it with a tooltip */}
{actions.copy &&
profile.isBuiltIn && (
<div className="text-right big-spacer-top">
<DocTooltip
doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-profiles/activate-rules-in-built-in-profile.md')}>
<Button className="disabled js-activate-rules">
{translate('quality_profiles.activate_more')}
</Button>
</DocTooltip>
</div>
)}
{actions.copy && profile.isBuiltIn && (
<div className="text-right big-spacer-top">
<DocTooltip
doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/quality-profiles/activate-rules-in-built-in-profile.md')}>
<Button className="disabled js-activate-rules">
{translate('quality_profiles.activate_more')}
</Button>
</DocTooltip>
</div>
)}
</div>
{profile.activeDeprecatedRuleCount > 0 && (
<ProfileRulesDeprecatedWarning
@@ -218,16 +207,15 @@ export default class ProfileRules extends React.PureComponent<Props, State> {
profile={profile.key}
/>
)}
{compareToSonarWay != null &&
compareToSonarWay.missingRuleCount > 0 && (
<ProfileRulesSonarWayComparison
language={profile.language}
organization={organization}
profile={profile.key}
sonarWayMissingRules={compareToSonarWay.missingRuleCount}
sonarway={compareToSonarWay.profile}
/>
)}
{compareToSonarWay != null && compareToSonarWay.missingRuleCount > 0 && (
<ProfileRulesSonarWayComparison
language={profile.language}
organization={organization}
profile={profile.key}
sonarWayMissingRules={compareToSonarWay.missingRuleCount}
sonarway={compareToSonarWay.profile}
/>
)}
</div>
);
}

+ 15
- 16
server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx View File

@@ -180,22 +180,21 @@ export default class CreateProfileForm extends React.PureComponent<Props, State>
value={selectedLanguage}
/>
</div>
{selectedLanguage &&
profiles.length && (
<div className="modal-field">
<label htmlFor="create-profile-parent">
{translate('quality_profiles.parent')}
</label>
<Select
clearable={true}
id="create-profile-parent"
name="parentKey"
onChange={this.handleParentChange}
options={profiles}
value={this.state.parent || ''}
/>
</div>
)}
{selectedLanguage && profiles.length && (
<div className="modal-field">
<label htmlFor="create-profile-parent">
{translate('quality_profiles.parent')}
</label>
<Select
clearable={true}
id="create-profile-parent"
name="parentKey"
onChange={this.handleParentChange}
options={profiles}
value={this.state.parent || ''}
/>
</div>
)}
{importers.map(importer => (
<div
className="modal-field spacer-bottom js-importer"

+ 15
- 18
server/sonar-web/src/main/js/apps/settings/components/Definition.tsx View File

@@ -42,8 +42,8 @@ import {

interface Props {
cancelChange: (key: string) => void;
changedValue: any;
changeValue: (key: string, value: any) => void;
changedValue: any;
checkValue: (key: string) => boolean;
component?: T.Component;
loading: boolean;
@@ -160,24 +160,21 @@ export class Definition extends React.PureComponent<Props, State> {
</span>
)}

{!loading &&
validationMessage && (
<span className="text-danger">
<AlertErrorIcon className="spacer-right" />
<span>
{translateWithParameters('settings.state.validation_failed', validationMessage)}
</span>
{!loading && validationMessage && (
<span className="text-danger">
<AlertErrorIcon className="spacer-right" />
<span>
{translateWithParameters('settings.state.validation_failed', validationMessage)}
</span>
)}

{!loading &&
!hasError &&
this.state.success && (
<span className="text-success">
<AlertSuccessIcon className="spacer-right" />
{translate('settings.state.saved')}
</span>
)}
</span>
)}

{!loading && !hasError && this.state.success && (
<span className="text-success">
<AlertSuccessIcon className="spacer-right" />
{translate('settings.state.saved')}
</span>
)}
</div>

<Input

+ 5
- 6
server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx View File

@@ -83,12 +83,11 @@ export default class DefinitionActions extends React.PureComponent<Props, State>

return (
<>
{isDefault &&
!hasValueChanged && (
<div className="spacer-top note" style={{ lineHeight: '24px' }}>
{translate('settings._default')}
</div>
)}
{isDefault && !hasValueChanged && (
<div className="spacer-top note" style={{ lineHeight: '24px' }}>
{translate('settings._default')}
</div>
)}
<div className="settings-definition-changes nowrap">
{hasValueChanged && (
<Button

+ 89
- 0
server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx View File

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

const setting: T.Setting = {
key: 'foo',
value: '42',
inherited: true,
definition: {
key: 'foo',
name: 'Foo setting',
description: 'When Foo then Bar',
type: 'INTEGER',
category: 'general',
subCategory: 'SubFoo',
defaultValue: '42',
options: [],
fields: []
}
};

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
});

it('should correctly handle change of value', () => {
const changeValue = jest.fn();
const checkValue = jest.fn();
const wrapper = shallowRender({ changeValue, checkValue });
wrapper.find('Input').prop<Function>('onChange')(5);
expect(changeValue).toHaveBeenCalledWith(setting.definition.key, 5);
expect(checkValue).toHaveBeenCalledWith(setting.definition.key);
});

it('should correctly cancel value change', () => {
const cancelChange = jest.fn();
const passValidation = jest.fn();
const wrapper = shallowRender({ cancelChange, passValidation });
wrapper.find('Input').prop<Function>('onCancel')();
expect(cancelChange).toHaveBeenCalledWith(setting.definition.key);
expect(passValidation).toHaveBeenCalledWith(setting.definition.key);
});

it('should correctly save value change', async () => {
const saveValue = jest.fn().mockResolvedValue({});
const wrapper = shallowRender({ changedValue: 10, saveValue });
wrapper.find('DefinitionActions').prop<Function>('onSave')();
await waitAndUpdate(wrapper);
expect(saveValue).toHaveBeenCalledWith(setting.definition.key, undefined);
expect(wrapper.find('AlertSuccessIcon').exists()).toBe(true);
});

function shallowRender(props: Partial<Definition['props']> = {}) {
return shallow(
<Definition
cancelChange={jest.fn()}
changeValue={jest.fn()}
changedValue={null}
checkValue={jest.fn()}
loading={false}
passValidation={jest.fn()}
resetValue={jest.fn().mockResolvedValue({})}
saveValue={jest.fn().mockResolvedValue({})}
setting={setting}
{...props}
/>
);
}

+ 91
- 0
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<div
className="settings-definition"
data-key="foo"
>
<div
className="settings-definition-left"
>
<h3
className="settings-definition-name"
title="Foo setting"
>
Foo setting
</h3>
<div
className="markdown small spacer-top"
dangerouslySetInnerHTML={
Object {
"__html": "When Foo then Bar",
}
}
/>
<div
className="settings-definition-key note little-spacer-top"
>
settings.key_x.foo
</div>
</div>
<div
className="settings-definition-right"
>
<div
className="settings-definition-state"
/>
<Input
hasValueChanged={false}
onCancel={[Function]}
onChange={[Function]}
onSave={[Function]}
setting={
Object {
"definition": Object {
"category": "general",
"defaultValue": "42",
"description": "When Foo then Bar",
"fields": Array [],
"key": "foo",
"name": "Foo setting",
"options": Array [],
"subCategory": "SubFoo",
"type": "INTEGER",
},
"inherited": true,
"key": "foo",
"value": "42",
}
}
value="42"
/>
<DefinitionActions
changedValue={null}
hasError={false}
hasValueChanged={false}
isDefault={true}
onCancel={[Function]}
onReset={[Function]}
onSave={[Function]}
setting={
Object {
"definition": Object {
"category": "general",
"defaultValue": "42",
"description": "When Foo then Bar",
"fields": Array [],
"key": "foo",
"name": "Foo setting",
"options": Array [],
"subCategory": "SubFoo",
"type": "INTEGER",
},
"inherited": true,
"key": "foo",
"value": "42",
}
}
/>
</div>
</div>
`;

+ 0
- 0
server/sonar-web/src/main/js/apps/settings/encryption/EncryptionApp.tsx View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save