diff options
128 files changed, 3891 insertions, 1376 deletions
diff --git a/server/sonar-docs/package.json b/server/sonar-docs/package.json index 0ff9c9a7f0e..f85cb95950c 100644 --- a/server/sonar-docs/package.json +++ b/server/sonar-docs/package.json @@ -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", diff --git a/server/sonar-vsts/package.json b/server/sonar-vsts/package.json index cd3f7b93433..0b2d4c227c9 100644 --- a/server/sonar-vsts/package.json +++ b/server/sonar-vsts/package.json @@ -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", diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 62aa05a7824..a53eaeabce7 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -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": { diff --git a/server/sonar-web/scripts/utils/getMessages.js b/server/sonar-web/scripts/utils/getMessages.js index 261442ff178..9c2ccd5553a 100644 --- a/server/sonar-web/scripts/utils/getMessages.js +++ b/server/sonar-web/scripts/utils/getMessages.js @@ -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) { diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index a0c57751631..b5294b59414 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -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" /> diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx index 814f7fc1c66..56354293a3f 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/GlobalContainer-test.tsx index 3c7c7492e29..ff7aba0434b 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/GlobalContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/GlobalContainer-test.tsx @@ -28,7 +28,7 @@ jest.mock('../embed-docs-modal/SuggestionsProvider', () => { return this.props.children; } } - + return { default: SuggestionsProvider }; }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx index aabed93c940..0905f0143ef 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx @@ -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 { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx index 045c005f322..a2a793eeaf6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx index f72338ef2e8..08ca22507ba 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx @@ -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> )} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx index 958a537094f..d106c1236d6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx index b8a7c30bffb..01401510728 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx @@ -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} + /> + )} </> ); } diff --git a/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx b/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx index 648cafe3b77..b577dd086b0 100644 --- a/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx +++ b/server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx @@ -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> ); diff --git a/server/sonar-web/src/main/js/app/components/search/Search.tsx b/server/sonar-web/src/main/js/app/components/search/Search.tsx index 9d358998335..299dacc1f58 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.tsx +++ b/server/sonar-web/src/main/js/app/components/search/Search.tsx @@ -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> ); diff --git a/server/sonar-web/src/main/js/apps/about/routes.ts b/server/sonar-web/src/main/js/apps/about/routes.ts index ab744f1bec6..b0eba5f6853 100644 --- a/server/sonar-web/src/main/js/apps/about/routes.ts +++ b/server/sonar-web/src/main/js/apps/about/routes.ts @@ -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() diff --git a/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx b/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx index 079826ff6de..000c9dd86cb 100644 --- a/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx +++ b/server/sonar-web/src/main/js/apps/account/profile/Profile.tsx @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx index db2f74440d6..4b154caabde 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx @@ -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" diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx index da727a0c7ab..98db6667d97 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx @@ -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} />} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx index a1146103f79..eff62fff02a 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/code/components/App.tsx b/server/sonar-web/src/main/js/apps/code/components/App.tsx index 1c030f377a2..48efaf6c677 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/App.tsx @@ -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> ); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx index d5264e69c5d..ac5b6325bb9 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx index 508c9bfc621..4a7fe801eb5 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChange.tsx @@ -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} + /> + )} </> ); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx index a7df07d3130..922c6bb563c 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/BulkChangeModal.tsx @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx index a39e77d2398..2a766236a32 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx index b56478416fe..a2a66f8050a 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetails.tsx @@ -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} /> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx index 966a96ca51c..c0aceb9a116 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsCustomRules.tsx @@ -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">: </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">: </span> + <span className="value" title={param.defaultValue}> + {param.defaultValue} + </span> + </div> + ))} </td> {this.props.canChange && ( diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx index f112a6edd95..58dae28f955 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsProfiles.tsx @@ -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> ); }; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx index b48cbaa6d9c..faa9b262d8b 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx @@ -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> ); }; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChange-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChange-test.tsx new file mode 100644 index 00000000000..1a22fdf85b2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/BulkChange-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx new file mode 100644 index 00000000000..3d2baaff55f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleDetails-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChange-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChange-test.tsx.snap new file mode 100644 index 00000000000..2cc9fb4b0d5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/BulkChange-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap new file mode 100644 index 00000000000..46c661b1a68 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetails-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx index e47aaf7ac34..ff9052a42d4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx @@ -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> } /> diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx index 5b9929b9447..1bfe27a0c5a 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx @@ -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 && diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx index d8b3a9c5075..53fc28de992 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx @@ -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()} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx new file mode 100644 index 00000000000..85b1cca1f08 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureContent-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap new file mode 100644 index 00000000000..d25d9196349 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureContent-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx index 52fc3caa4b1..3574f0008d5 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx index 3574939f942..ae48a9c3838 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx @@ -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} + /> + ) ); }; diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx index 4df6b9d29d8..08ad2ed676f 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx index 1ab5ef85948..a08e71d5859 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx @@ -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 /> diff --git a/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx b/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx index 33d9fcc8d79..e86d9e0f131 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx index 43c64e49837..ad302695458 100644 --- a/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx b/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx index ff5fc8b7b47..7990ca2075c 100644 --- a/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/custom-measures/components/App.tsx @@ -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> </> ); diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx index 37402728681..be55d08a49b 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/App.tsx @@ -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> </> ); diff --git a/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx b/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx index 6c8da043c69..3ad55e020d4 100644 --- a/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx +++ b/server/sonar-web/src/main/js/apps/custom-metrics/components/Item.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/documentation/utils.ts b/server/sonar-web/src/main/js/apps/documentation/utils.ts index ce04d9d2e8c..88b831c7325 100644 --- a/server/sonar-web/src/main/js/apps/documentation/utils.ts +++ b/server/sonar-web/src/main/js/apps/documentation/utils.ts @@ -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] ) ); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/App.tsx b/server/sonar-web/src/main/js/apps/groups/components/App.tsx index 26f28e0365e..02d96255587 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/App.tsx @@ -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> </> ); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx index f8576bb6039..0c4e78abd65 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx @@ -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} />); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap index 4f4ffb932d6..11190d8cf61 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap @@ -88,7 +88,7 @@ exports[`should render correctly 2`] = ` count={2} loadMore={[Function]} ready={true} - total={2} + total={4} /> </div> </div> diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx index 8e079deebbe..8fe9d110644 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx @@ -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> )} diff --git a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx index 7700efb6cf1..0b9cac8f4bb 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx index 58bcdbaad93..fbaa4feb644 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.tsx @@ -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 = diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx index 21e9148a4bf..be24582def8 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/PluginActions.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/PluginActions.tsx index b33c490d370..996d73d4931 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/components/PluginActions.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/components/PluginActions.tsx @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx index 621ad310048..827b17aad26 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/components/PluginChangeLog.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdates.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdates.tsx index 6857a3d70ce..2f7d243cdb7 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdates.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/components/PluginUpdates.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginChangeLog-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginChangeLog-test.tsx new file mode 100644 index 00000000000..30a3dacd6da --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginChangeLog-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginChangeLog-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginChangeLog-test.tsx.snap new file mode 100644 index 00000000000..c45bedc60a9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginChangeLog-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx index beb8fe92be5..fa4f38355fb 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx index 4859e2f2ee5..66bd49ce5cd 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx index 89fe2e97ce9..a520f22a579 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx @@ -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> ); diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx index ae0fae4bcbf..5440ef11efe 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx index 5f38c424a8f..c191b573b52 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx @@ -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> ); diff --git a/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx b/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx index 256a38d66f3..155ec59ce3a 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx @@ -70,8 +70,9 @@ export function EmptyOverview({ } /> )} - {!hasBranches && - !hasAnalyses && <AnalyzeTutorial component={component} currentUser={currentUser} />} + {!hasBranches && !hasAnalyses && ( + <AnalyzeTutorial component={component} currentUser={currentUser} /> + )} </> ) : ( <WarningMessage diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx index 709754c92b0..1a1117177a8 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx index 7af63eba6fb..279dfca0c78 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/ApplicationQualityGate.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.tsx b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.tsx index c02add35b60..fb39c06bf48 100644 --- a/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.tsx +++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx index f3eb1a147d9..67b1ab5d959 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/Form.tsx @@ -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 }; } diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx index b2c8473d413..f9808246cb9 100644 --- a/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx @@ -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 ); }; diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/App-test.tsx new file mode 100644 index 00000000000..b1440190203 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/App-test.tsx @@ -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} />); +} diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/__snapshots__/App-test.tsx.snap new file mode 100644 index 00000000000..e204f06b3de --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/__tests__/__snapshots__/App-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx index 054cd538963..6beabeebc1f 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx new file mode 100644 index 00000000000..47839ce1be2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/App-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap new file mode 100644 index 00000000000..0cacc59dfc9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/__snapshots__/App-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx index 5ad0b29ba3b..839351b2e64 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx index 7c18623172a..eb8b5f89ec1 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/App.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx index fdffce0d3dc..4fb6e1ad37a 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/ReleasabilityBox.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/actions.ts b/server/sonar-web/src/main/js/apps/projectActivity/actions.ts index 74489067d1a..8e0ed07ac0f 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/actions.ts +++ b/server/sonar-web/src/main/js/apps/projectActivity/actions.ts @@ -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 ) }; }) diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx index 31fbc3e5b15..ef8e1fc1bee 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityEventSelectOption-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityEventSelectOption-test.tsx new file mode 100644 index 00000000000..15bf4126525 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityEventSelectOption-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityEventSelectOption-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityEventSelectOption-test.tsx.snap new file mode 100644 index 00000000000..a31966320b7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityEventSelectOption-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx index 60011b07ab1..f75fcfb9461 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx index 6cfbcee0d44..e27eda543b5 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx index b448451b597..cf0bce1ac04 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx @@ -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 ? ( diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index adeed9fb8ba..fac7ae2096c 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/apps/projects/utils.ts b/server/sonar-web/src/main/js/apps/projects/utils.ts index 80e034ded29..7f977c72b9c 100644 --- a/server/sonar-web/src/main/js/apps/projects/utils.ts +++ b/server/sonar-web/src/main/js/apps/projects/utils.ts @@ -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 => { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx index 86e8b4e1dce..17342a6a0e3 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx index 887d8de5ded..4aa4f650acf 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx index 186d1648353..6124d4fcdc9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx index c206f54272b..a2befeebb08 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx index 968de095f73..7e41590a2ea 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx index 65171137d33..072e5626aae 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx index b9060b042c0..e3afc0d7641 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx index 7b19310ca2f..e129058fb20 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx @@ -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" diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx index 7d6fdc5b50c..d17d9ead8a1 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx b/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx index a4892c225bf..b7aae5bb572 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx new file mode 100644 index 00000000000..c614101d39f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap new file mode 100644 index 00000000000..3ee6ede5338 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Definition-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionApp.tsx b/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionApp.tsx index 4f952017b84..3769c5bdedf 100644 --- a/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionApp.tsx +++ b/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionApp.tsx @@ -77,13 +77,9 @@ export default class EncryptionApp extends React.PureComponent<{}, State> { <DeferredSpinner loading={loading} /> </header> - {!loading && - !secretKeyAvailable && ( - <GenerateSecretKeyForm - generateSecretKey={this.generateSecretKey} - secretKey={secretKey} - /> - )} + {!loading && !secretKeyAvailable && ( + <GenerateSecretKeyForm generateSecretKey={this.generateSecretKey} secretKey={secretKey} /> + )} {secretKeyAvailable && <EncryptionForm generateSecretKey={this.generateSecretKey} />} </div> diff --git a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx index cb758e0b7b5..ef812abae95 100644 --- a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx @@ -167,8 +167,9 @@ export default class PageActions extends React.PureComponent<Props, State> { {translate('system.restart_server')} </Button> )} - {this.props.canRestart && - this.state.openRestartForm && <RestartForm onClose={this.handleServerRestartClose} />} + {this.props.canRestart && this.state.openRestartForm && ( + <RestartForm onClose={this.handleServerRestartClose} /> + )} {this.state.openLogsLevelForm && ( <ChangeLogLevelForm infoMsg={translate( diff --git a/server/sonar-web/src/main/js/apps/tutorials/routes.ts b/server/sonar-web/src/main/js/apps/tutorials/routes.ts index 7dd394424c5..d779c891054 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/routes.ts +++ b/server/sonar-web/src/main/js/apps/tutorials/routes.ts @@ -23,9 +23,8 @@ import { isSonarCloud } from '../../helpers/system'; const routes = [ { indexRoute: { - component: lazyLoad( - () => - isSonarCloud() ? import('./onboarding/OnboardingPage') : import('./ProjectOnboardingPage') + component: lazyLoad(() => + isSonarCloud() ? import('./onboarding/OnboardingPage') : import('./ProjectOnboardingPage') ) } } diff --git a/server/sonar-web/src/main/js/apps/tutorials/utils.ts b/server/sonar-web/src/main/js/apps/tutorials/utils.ts index 59d9e9c75d2..d689b874efd 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/utils.ts +++ b/server/sonar-web/src/main/js/apps/tutorials/utils.ts @@ -39,6 +39,6 @@ export function isLanguageConfigured(config?: LanguageConfig) { return isJavaConfigured || isDotNetConfigured || isCFamilyConfigured || isOtherConfigured; } -export function quote(os: string): ((s: string) => string) { +export function quote(os: string): (s: string) => string { return os === 'win' ? (s: string) => `"${s}"` : (s: string) => s; } diff --git a/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx b/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx index 6fec17f995c..807e3a32ae4 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx @@ -65,15 +65,11 @@ export default class UserGroups extends React.PureComponent<Props, State> { </li> ))} <li className="little-spacer-bottom"> - {groups.length > GROUPS_LIMIT && - !this.state.showMore && ( - <a - className="js-user-more-groups spacer-right" - href="#" - onClick={this.toggleShowMore}> - {translateWithParameters('more_x', groups.length - limit)} - </a> - )} + {groups.length > GROUPS_LIMIT && !this.state.showMore && ( + <a className="js-user-more-groups spacer-right" href="#" onClick={this.toggleShowMore}> + {translateWithParameters('more_x', groups.length - limit)} + </a> + )} <ButtonIcon className="js-user-groups button-small" onClick={this.handleOpenForm} diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx index 48b93275fb1..9d6c85ebecf 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx @@ -35,10 +35,9 @@ export default function UserListItemIdentity({ identityProvider, user }: Props) <span className="js-user-login note little-spacer-left">{user.login}</span> </div> {user.email && <div className="js-user-email little-spacer-top">{user.email}</div>} - {!user.local && - user.externalProvider !== 'sonarqube' && ( - <ExternalProvider identityProvider={identityProvider} user={user} /> - )} + {!user.local && user.externalProvider !== 'sonarqube' && ( + <ExternalProvider identityProvider={identityProvider} user={user} /> + )} </td> ); } diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.tsx b/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.tsx index 475f3a1737a..5b19d4cf57d 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.tsx @@ -185,14 +185,13 @@ interface ValueProps { export function UsersSelectSearchValue({ children, value }: ValueProps) { return ( <div className="Select-value" title={value ? value.name : ''}> - {value && - value.login && ( - <div className="Select-value-label"> - <Avatar hash={value.avatar} name={value.name} size={AVATAR_SIZE} /> - <strong className="spacer-left">{children}</strong> - <span className="note little-spacer-left">{value.login}</span> - </div> - )} + {value && value.login && ( + <div className="Select-value-label"> + <Avatar hash={value.avatar} name={value.name} size={AVATAR_SIZE} /> + <strong className="spacer-left">{children}</strong> + <span className="note little-spacer-left">{value.login}</span> + </div> + )} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx index 2f1223cf2f0..304f28eb2f7 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx @@ -176,17 +176,17 @@ export default class Action extends React.PureComponent<Props, State> { {this.renderTabs()} - {showParams && - action.params && ( - <Params - params={action.params} - showDeprecated={this.props.showDeprecated} - showInternal={this.props.showInternal} - /> - )} - - {showResponse && - action.hasResponseExample && <ResponseExample action={action} domain={domain} />} + {showParams && action.params && ( + <Params + params={action.params} + showDeprecated={this.props.showDeprecated} + showInternal={this.props.showInternal} + /> + )} + + {showResponse && action.hasResponseExample && ( + <ResponseExample action={action} domain={domain} /> + )} {showChangelog && <ActionChangelog changelog={action.changelog} />} </div> diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx index 7a83bc387b8..5cf2d7c0a90 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx @@ -46,20 +46,17 @@ export default class Params extends React.PureComponent<Props> { </div> )} - {this.props.showDeprecated && - param.deprecatedKey && ( - <div className="little-spacer-top"> - <code>{param.deprecatedKey}</code> - </div> - )} + {this.props.showDeprecated && param.deprecatedKey && ( + <div className="little-spacer-top"> + <code>{param.deprecatedKey}</code> + </div> + )} - {this.props.showDeprecated && - param.deprecatedKey && - param.deprecatedKeySince && ( - <div className="little-spacer-top"> - <DeprecatedBadge since={param.deprecatedKeySince} /> - </div> - )} + {this.props.showDeprecated && param.deprecatedKey && param.deprecatedKeySince && ( + <div className="little-spacer-top"> + <DeprecatedBadge since={param.deprecatedKeySince} /> + </div> + )} <div className="note little-spacer-top">{param.required ? 'required' : 'optional'}</div> diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx index 3d66a1e1126..dcf495329d4 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx @@ -97,11 +97,8 @@ export default class App extends React.PureComponent<Props, State> { return updateWebhook(data).then(() => { if (this.mounted) { this.setState(({ webhooks }) => ({ - webhooks: webhooks.map( - webhook => - webhook.key === data.webhook - ? { ...webhook, name: data.name, url: data.url } - : webhook + webhooks: webhooks.map(webhook => + webhook.key === data.webhook ? { ...webhook, name: data.name, url: data.url } : webhook ) })); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx index f35ea78319e..90c2b80ff22 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx @@ -128,13 +128,12 @@ export default class DuplicationPopup extends React.PureComponent<Props> { {duplication.file.projectName} </Link> </div> - {duplication.file.subProject && - duplication.file.subProjectName && ( - <div className="component-name-parent"> - <QualifierIcon className="little-spacer-right" qualifier="BRC" /> - {duplication.file.subProjectName} - </div> - )} + {duplication.file.subProject && duplication.file.subProjectName && ( + <div className="component-name-parent"> + <QualifierIcon className="little-spacer-right" qualifier="BRC" /> + {duplication.file.subProjectName} + </div> + )} </> )} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx index 7955f4df686..34da94c78b8 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx @@ -141,14 +141,13 @@ export default class Line extends React.PureComponent<Props> { /> ))} - {this.props.displayIssues && - !this.props.displayAllIssues && ( - <LineIssuesIndicator - issues={this.props.issues} - line={line} - onClick={this.handleIssuesIndicatorClick} - /> - )} + {this.props.displayIssues && !this.props.displayAllIssues && ( + <LineIssuesIndicator + issues={this.props.issues} + line={line} + onClick={this.handleIssuesIndicatorClick} + /> + )} <LineCode branchLike={this.props.branchLike} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx index f60496935b4..af7a7b7200a 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx @@ -225,20 +225,19 @@ export default class LineCode extends React.PureComponent<Props, State> { <div className="source-line-code-inner"> <pre ref={node => (this.codeNode = node)}>{renderedTokens}</pre> </div> - {showIssues && - issues.length > 0 && ( - <LineIssuesList - branchLike={this.props.branchLike} - displayIssueLocationsCount={this.props.displayIssueLocationsCount} - displayIssueLocationsLink={this.props.displayIssueLocationsLink} - issuePopup={this.props.issuePopup} - issues={issues} - onIssueChange={this.props.onIssueChange} - onIssueClick={onIssueSelect} - onIssuePopupToggle={this.props.onIssuePopupToggle} - selectedIssue={selectedIssue} - /> - )} + {showIssues && issues.length > 0 && ( + <LineIssuesList + branchLike={this.props.branchLike} + displayIssueLocationsCount={this.props.displayIssueLocationsCount} + displayIssueLocationsLink={this.props.displayIssueLocationsLink} + issuePopup={this.props.issuePopup} + issues={issues} + onIssueChange={this.props.onIssueChange} + onIssueClick={onIssueSelect} + onIssuePopupToggle={this.props.onIssuePopupToggle} + selectedIssue={selectedIssue} + /> + )} </td> ); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx index 9f6834cfa65..c7486388662 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx @@ -189,61 +189,54 @@ export default class MeasuresOverlay extends React.PureComponent<Props, State> { {this.renderBigMeasure(measures.violations)} {this.renderBigMeasure(measures.sqale_index)} </div> - {measures.violations && - !!measures.violations.value && ( - <> - {typesFacet && ( - <div className="measures"> - <div className="measures-list"> - {sortBy(typesFacet, f => ISSUE_TYPES.indexOf(f.val)).map(f => ( - <div className="measure measure-one-line" key={f.val}> - <span className="measure-name"> - <IssueTypeIcon className="little-spacer-right" query={f.val} /> - {translate('issue.type', f.val)} - </span> - <span className="measure-value"> - {formatMeasure(f.count, 'SHORT_INT')} - </span> - </div> - ))} - </div> + {measures.violations && !!measures.violations.value && ( + <> + {typesFacet && ( + <div className="measures"> + <div className="measures-list"> + {sortBy(typesFacet, f => ISSUE_TYPES.indexOf(f.val)).map(f => ( + <div className="measure measure-one-line" key={f.val}> + <span className="measure-name"> + <IssueTypeIcon className="little-spacer-right" query={f.val} /> + {translate('issue.type', f.val)} + </span> + <span className="measure-value">{formatMeasure(f.count, 'SHORT_INT')}</span> + </div> + ))} </div> - )} - {severitiesFacet && ( - <div className="measures"> - <div className="measures-list"> - {sortBy(severitiesFacet, f => SEVERITIES.indexOf(f.val)).map(f => ( - <div className="measure measure-one-line" key={f.val}> - <span className="measure-name"> - <SeverityHelper severity={f.val} /> - </span> - <span className="measure-value"> - {formatMeasure(f.count, 'SHORT_INT')} - </span> - </div> - ))} - </div> + </div> + )} + {severitiesFacet && ( + <div className="measures"> + <div className="measures-list"> + {sortBy(severitiesFacet, f => SEVERITIES.indexOf(f.val)).map(f => ( + <div className="measure measure-one-line" key={f.val}> + <span className="measure-name"> + <SeverityHelper severity={f.val} /> + </span> + <span className="measure-value">{formatMeasure(f.count, 'SHORT_INT')}</span> + </div> + ))} </div> - )} - {tagsFacet && ( - <div className="measures"> - <div className="measures-list"> - {tagsFacet.map(f => ( - <div className="measure measure-one-line" key={f.val}> - <span className="measure-name"> - <TagsIcon className="little-spacer-right" /> - {f.val} - </span> - <span className="measure-value"> - {formatMeasure(f.count, 'SHORT_INT')} - </span> - </div> - ))} - </div> + </div> + )} + {tagsFacet && ( + <div className="measures"> + <div className="measures-list"> + {tagsFacet.map(f => ( + <div className="measure measure-one-line" key={f.val}> + <span className="measure-name"> + <TagsIcon className="little-spacer-right" /> + {f.val} + </span> + <span className="measure-value">{formatMeasure(f.count, 'SHORT_INT')}</span> + </div> + ))} </div> - )} - </> - )} + </div> + )} + </> + )} </div> </div> ); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx index d3e5ff08330..0abc29938c7 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx @@ -91,7 +91,8 @@ jest.mock('../../../../api/measures', () => ({ { metric: 'security_remediation_effort', value: '0' }, { metric: 'statements', value: '3' }, { metric: 'skipped_tests', value: '0' }, - { metric: 'test_failures', value: '0' } + { metric: 'test_failures', value: '0' }, + { metric: 'violations', value: '1' } ]) })); @@ -129,6 +130,7 @@ jest.mock('../../../../api/metrics', () => ({ { key: 'statements', type: 'INT', domain: 'Size' }, { key: 'skipped_tests', type: 'INT', domain: 'Tests' }, { key: 'test_failures', type: 'INT', domain: 'Tests' }, + { key: 'violations', type: 'INT', domain: 'Issues' }, // next two must be filtered out { key: 'data', type: 'DATA' }, { key: 'hidden', hidden: true } @@ -155,13 +157,7 @@ const branchLike: T.ShortLivingBranch = { }; it('should render source file', async () => { - const wrapper = shallow( - <MeasuresOverlay - branchLike={branchLike} - onClose={jest.fn()} - sourceViewerFile={sourceViewerFile} - /> - ); + const wrapper = shallowRender(); await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); @@ -170,13 +166,18 @@ it('should render source file', async () => { }); it('should render test file', async () => { - const wrapper = shallow( + const wrapper = shallowRender({ sourceViewerFile: { ...sourceViewerFile, q: 'UTS' } }); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<MeasuresOverlay['props']> = {}) { + return shallow( <MeasuresOverlay branchLike={branchLike} onClose={jest.fn()} - sourceViewerFile={{ ...sourceViewerFile, q: 'UTS' }} + sourceViewerFile={sourceViewerFile} + {...props} /> ); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap index 94cea5cdf1b..2472c038a73 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap @@ -166,6 +166,25 @@ exports[`should render source file 1`] = ` > <div className="measure measure-big" + data-metric="violations" + > + <span + className="measure-value" + > + <Measure + metricKey="violations" + metricType="INT" + value="1" + /> + </span> + <span + className="measure-name" + > + violations + </span> + </div> + <div + className="measure measure-big" data-metric="sqale_index" > <span @@ -184,6 +203,226 @@ exports[`should render source file 1`] = ` </span> </div> </div> + <div + className="measures" + > + <div + className="measures-list" + > + <div + className="measure measure-one-line" + key="BUG" + > + <span + className="measure-name" + > + <IssueTypeIcon + className="little-spacer-right" + query="BUG" + /> + issue.type.BUG + </span> + <span + className="measure-value" + > + 1 + </span> + </div> + <div + className="measure measure-one-line" + key="VULNERABILITY" + > + <span + className="measure-name" + > + <IssueTypeIcon + className="little-spacer-right" + query="VULNERABILITY" + /> + issue.type.VULNERABILITY + </span> + <span + className="measure-value" + > + 0 + </span> + </div> + <div + className="measure measure-one-line" + key="CODE_SMELL" + > + <span + className="measure-name" + > + <IssueTypeIcon + className="little-spacer-right" + query="CODE_SMELL" + /> + issue.type.CODE_SMELL + </span> + <span + className="measure-value" + > + 2 + </span> + </div> + </div> + </div> + <div + className="measures" + > + <div + className="measures-list" + > + <div + className="measure measure-one-line" + key="BLOCKER" + > + <span + className="measure-name" + > + <SeverityHelper + severity="BLOCKER" + /> + </span> + <span + className="measure-value" + > + 5 + </span> + </div> + <div + className="measure measure-one-line" + key="CRITICAL" + > + <span + className="measure-name" + > + <SeverityHelper + severity="CRITICAL" + /> + </span> + <span + className="measure-value" + > + 4 + </span> + </div> + <div + className="measure measure-one-line" + key="MAJOR" + > + <span + className="measure-name" + > + <SeverityHelper + severity="MAJOR" + /> + </span> + <span + className="measure-value" + > + 1 + </span> + </div> + <div + className="measure measure-one-line" + key="MINOR" + > + <span + className="measure-name" + > + <SeverityHelper + severity="MINOR" + /> + </span> + <span + className="measure-value" + > + 3 + </span> + </div> + <div + className="measure measure-one-line" + key="INFO" + > + <span + className="measure-name" + > + <SeverityHelper + severity="INFO" + /> + </span> + <span + className="measure-value" + > + 2 + </span> + </div> + </div> + </div> + <div + className="measures" + > + <div + className="measures-list" + > + <div + className="measure measure-one-line" + key="bad-practice" + > + <span + className="measure-name" + > + <TagsIcon + className="little-spacer-right" + /> + bad-practice + </span> + <span + className="measure-value" + > + 1 + </span> + </div> + <div + className="measure measure-one-line" + key="cert" + > + <span + className="measure-name" + > + <TagsIcon + className="little-spacer-right" + /> + cert + </span> + <span + className="measure-value" + > + 3 + </span> + </div> + <div + className="measure measure-one-line" + key="design" + > + <span + className="measure-name" + > + <TagsIcon + className="little-spacer-right" + /> + design + </span> + <span + className="measure-value" + > + 1 + </span> + </div> + </div> + </div> </div> </div> <div @@ -524,6 +763,25 @@ exports[`should render source file 2`] = ` > <div className="measure measure-big" + data-metric="violations" + > + <span + className="measure-value" + > + <Measure + metricKey="violations" + metricType="INT" + value="1" + /> + </span> + <span + className="measure-name" + > + violations + </span> + </div> + <div + className="measure measure-big" data-metric="sqale_index" > <span @@ -542,6 +800,226 @@ exports[`should render source file 2`] = ` </span> </div> </div> + <div + className="measures" + > + <div + className="measures-list" + > + <div + className="measure measure-one-line" + key="BUG" + > + <span + className="measure-name" + > + <IssueTypeIcon + className="little-spacer-right" + query="BUG" + /> + issue.type.BUG + </span> + <span + className="measure-value" + > + 1 + </span> + </div> + <div + className="measure measure-one-line" + key="VULNERABILITY" + > + <span + className="measure-name" + > + <IssueTypeIcon + className="little-spacer-right" + query="VULNERABILITY" + /> + issue.type.VULNERABILITY + </span> + <span + className="measure-value" + > + 0 + </span> + </div> + <div + className="measure measure-one-line" + key="CODE_SMELL" + > + <span + className="measure-name" + > + <IssueTypeIcon + className="little-spacer-right" + query="CODE_SMELL" + /> + issue.type.CODE_SMELL + </span> + <span + className="measure-value" + > + 2 + </span> + </div> + </div> + </div> + <div + className="measures" + > + <div + className="measures-list" + > + <div + className="measure measure-one-line" + key="BLOCKER" + > + <span + className="measure-name" + > + <SeverityHelper + severity="BLOCKER" + /> + </span> + <span + className="measure-value" + > + 5 + </span> + </div> + <div + className="measure measure-one-line" + key="CRITICAL" + > + <span + className="measure-name" + > + <SeverityHelper + severity="CRITICAL" + /> + </span> + <span + className="measure-value" + > + 4 + </span> + </div> + <div + className="measure measure-one-line" + key="MAJOR" + > + <span + className="measure-name" + > + <SeverityHelper + severity="MAJOR" + /> + </span> + <span + className="measure-value" + > + 1 + </span> + </div> + <div + className="measure measure-one-line" + key="MINOR" + > + <span + className="measure-name" + > + <SeverityHelper + severity="MINOR" + /> + </span> + <span + className="measure-value" + > + 3 + </span> + </div> + <div + className="measure measure-one-line" + key="INFO" + > + <span + className="measure-name" + > + <SeverityHelper + severity="INFO" + /> + </span> + <span + className="measure-value" + > + 2 + </span> + </div> + </div> + </div> + <div + className="measures" + > + <div + className="measures-list" + > + <div + className="measure measure-one-line" + key="bad-practice" + > + <span + className="measure-name" + > + <TagsIcon + className="little-spacer-right" + /> + bad-practice + </span> + <span + className="measure-value" + > + 1 + </span> + </div> + <div + className="measure measure-one-line" + key="cert" + > + <span + className="measure-name" + > + <TagsIcon + className="little-spacer-right" + /> + cert + </span> + <span + className="measure-value" + > + 3 + </span> + </div> + <div + className="measure measure-one-line" + key="design" + > + <span + className="measure-name" + > + <TagsIcon + className="little-spacer-right" + /> + design + </span> + <span + className="measure-value" + > + 1 + </span> + </div> + </div> + </div> </div> </div> <div @@ -1156,6 +1634,19 @@ exports[`should render source file 2`] = ` } /> <MeasuresOverlayMeasure + key="violations" + measure={ + Object { + "metric": Object { + "domain": "Issues", + "key": "violations", + "type": "INT", + }, + "value": "1", + } + } + /> + <MeasuresOverlayMeasure key="wont_fix_issues" measure={ Object { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts index 6bd78badd50..6e0ad25582f 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts @@ -48,11 +48,10 @@ export function splitByTokens(code: string, rootClassName = ''): Token[] { export function highlightSymbol(tokens: Token[], symbol: string): Token[] { const symbolRegExp = new RegExp(`\\b${symbol}\\b`); - return tokens.map( - token => - symbolRegExp.test(token.className) - ? { ...token, className: `${token.className} highlighted` } - : token + return tokens.map(token => + symbolRegExp.test(token.className) + ? { ...token, className: `${token.className} highlighted` } + : token ); } diff --git a/server/sonar-web/src/main/js/components/charts/LineChart.tsx b/server/sonar-web/src/main/js/components/charts/LineChart.tsx index 39aaf3d2f82..a937fa7e7fd 100644 --- a/server/sonar-web/src/main/js/components/charts/LineChart.tsx +++ b/server/sonar-web/src/main/js/components/charts/LineChart.tsx @@ -74,11 +74,13 @@ export default class LineChart extends React.PureComponent<Props> { return null; } - const points = this.props.data.filter(point => point.y != null).map((point, index) => { - const x = xScale(point.x); - const y = yScale(point.y || 0); - return <circle className="line-chart-point" cx={x} cy={y} key={index} r="3" />; - }); + const points = this.props.data + .filter(point => point.y != null) + .map((point, index) => { + const x = xScale(point.x); + const y = yScale(point.y || 0); + return <circle className="line-chart-point" cx={x} cy={y} key={index} r="3" />; + }); return <g>{points}</g>; } diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx index e2870be58d0..ab0d53888b6 100644 --- a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx +++ b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx @@ -314,11 +314,9 @@ export default class MultiSelect extends React.PureComponent<Props, State> { renderLabel={renderLabel} /> )} - {!showNewElement && - selectedElements.length < 1 && - unselectedElements.length < 1 && ( - <li className="spacer-left">{translateWithParameters('no_results_for_x', query)}</li> - )} + {!showNewElement && selectedElements.length < 1 && unselectedElements.length < 1 && ( + <li className="spacer-left">{translateWithParameters('no_results_for_x', query)}</li> + )} </ul> {footerNode} </div> diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx index d1f85357ac5..7917b491ff8 100644 --- a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx +++ b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx @@ -66,7 +66,7 @@ export default function Tooltip(props: Props) { } export class TooltipInner extends React.Component<Props, State> { - throttledPositionTooltip: (() => void); + throttledPositionTooltip: () => void; mouseEnterTimeout?: number; mouseLeaveTimeout?: number; tooltipNode?: HTMLElement | null; diff --git a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx b/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx index 0845c888738..a10f486cbe9 100644 --- a/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx +++ b/server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx @@ -299,12 +299,11 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S showMore={this.showFullList} total={sortedItems.length} /> - {mightHaveMoreResults && - this.state.showFullList && ( - <Alert className="spacer-top" variant="warning"> - {translate('facet_might_have_more_results')} - </Alert> - )} + {mightHaveMoreResults && this.state.showFullList && ( + <Alert className="spacer-top" variant="warning"> + {translate('facet_might_have_more_results')} + </Alert> + )} </> ); } @@ -407,14 +406,13 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S /> <DeferredSpinner loading={this.props.fetching} /> - {this.props.open && - !disabled && ( - <> - {this.renderSearch()} - {showList ? this.renderList() : this.renderSearchResults()} - <MultipleSelectionHint options={Object.keys(stats).length} values={values.length} /> - </> - )} + {this.props.open && !disabled && ( + <> + {this.renderSearch()} + {showList ? this.renderList() : this.renderSearchResults()} + <MultipleSelectionHint options={Object.keys(stats).length} values={values.length} /> + </> + )} </FacetBox> ); } diff --git a/server/sonar-web/src/main/js/components/facet/ListStyleFacetFooter.tsx b/server/sonar-web/src/main/js/components/facet/ListStyleFacetFooter.tsx index ccafff05264..f469684eeac 100644 --- a/server/sonar-web/src/main/js/components/facet/ListStyleFacetFooter.tsx +++ b/server/sonar-web/src/main/js/components/facet/ListStyleFacetFooter.tsx @@ -59,12 +59,11 @@ export default class ListStyleFacetFooter extends React.PureComponent<Props> { </a> )} - {this.props.showLess && - allShown && ( - <a className="spacer-left text-muted" href="#" onClick={this.handleShowLessClick}> - {translate('show_less')} - </a> - )} + {this.props.showLess && allShown && ( + <a className="spacer-left text-muted" href="#" onClick={this.handleShowLessClick}> + {translate('show_less')} + </a> + )} </footer> ); } diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.tsx b/server/sonar-web/src/main/js/components/issue/IssueView.tsx index 06d2eb4173d..f5cb65b0241 100644 --- a/server/sonar-web/src/main/js/components/issue/IssueView.tsx +++ b/server/sonar-web/src/main/js/components/issue/IssueView.tsx @@ -97,19 +97,18 @@ export default class IssueView extends React.PureComponent<Props> { onChange={this.props.onChange} togglePopup={this.props.togglePopup} /> - {issue.comments && - issue.comments.length > 0 && ( - <div className="issue-comments"> - {issue.comments.map(comment => ( - <IssueCommentLine - comment={comment} - key={comment.key} - onDelete={this.deleteComment} - onEdit={this.editComment} - /> - ))} - </div> - )} + {issue.comments && issue.comments.length > 0 && ( + <div className="issue-comments"> + {issue.comments.map(comment => ( + <IssueCommentLine + comment={comment} + key={comment.key} + onDelete={this.deleteComment} + onEdit={this.editComment} + /> + ))} + </div> + )} {hasCheckbox && ( <> <Checkbox diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx new file mode 100644 index 00000000000..59795351516 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/__tests__/IssueView-test.tsx @@ -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 IssueView from '../IssueView'; +import { mockIssue } from '../../../helpers/testMocks'; + +it('should render correctly', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<IssueView['props']> = {}) { + return shallow( + <IssueView + issue={mockIssue(false, { + comments: [ + { + key: '1', + htmlText: 'My comment', + markdown: 'My comment', + updatable: false, + createdAt: '2017-07-05T09:33:29+0200', + author: 'admin', + authorLogin: 'admin', + authorName: 'Admin', + authorAvatar: 'admin-avatar', + authorActive: true + } + ] + })} + onAssign={jest.fn()} + onChange={jest.fn()} + onClick={jest.fn()} + selected={true} + togglePopup={jest.fn()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap new file mode 100644 index 00000000000..b76291398cd --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/__tests__/__snapshots__/IssueView-test.tsx.snap @@ -0,0 +1,137 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<div + className="issue selected" + data-issue="AVsae-CQS-9G3txfbFN2" + onClick={[Function]} + role="listitem" + tabIndex={0} +> + <IssueTitleBar + issue={ + Object { + "actions": Array [], + "comments": Array [ + Object { + "author": "admin", + "authorActive": true, + "authorAvatar": "admin-avatar", + "authorLogin": "admin", + "authorName": "Admin", + "createdAt": "2017-07-05T09:33:29+0200", + "htmlText": "My comment", + "key": "1", + "markdown": "My comment", + "updatable": false, + }, + ], + "component": "main.js", + "componentLongName": "main.js", + "componentQualifier": "FIL", + "componentUuid": "foo1234", + "creationDate": "2017-03-01T09:36:01+0100", + "flows": Array [], + "fromHotspot": false, + "key": "AVsae-CQS-9G3txfbFN2", + "line": 25, + "message": "Reduce the number of conditional operators (4) used in the expression", + "organization": "myorg", + "project": "myproject", + "projectKey": "foo", + "projectName": "Foo", + "projectOrganization": "org", + "rule": "javascript:S1067", + "ruleName": "foo", + "secondaryLocations": Array [], + "severity": "MAJOR", + "status": "OPEN", + "textRange": Object { + "endLine": 26, + "endOffset": 15, + "startLine": 25, + "startOffset": 0, + }, + "transitions": Array [], + "type": "BUG", + } + } + togglePopup={[MockFunction]} + /> + <IssueActionsBar + issue={ + Object { + "actions": Array [], + "comments": Array [ + Object { + "author": "admin", + "authorActive": true, + "authorAvatar": "admin-avatar", + "authorLogin": "admin", + "authorName": "Admin", + "createdAt": "2017-07-05T09:33:29+0200", + "htmlText": "My comment", + "key": "1", + "markdown": "My comment", + "updatable": false, + }, + ], + "component": "main.js", + "componentLongName": "main.js", + "componentQualifier": "FIL", + "componentUuid": "foo1234", + "creationDate": "2017-03-01T09:36:01+0100", + "flows": Array [], + "fromHotspot": false, + "key": "AVsae-CQS-9G3txfbFN2", + "line": 25, + "message": "Reduce the number of conditional operators (4) used in the expression", + "organization": "myorg", + "project": "myproject", + "projectKey": "foo", + "projectName": "Foo", + "projectOrganization": "org", + "rule": "javascript:S1067", + "ruleName": "foo", + "secondaryLocations": Array [], + "severity": "MAJOR", + "status": "OPEN", + "textRange": Object { + "endLine": 26, + "endOffset": 15, + "startLine": 25, + "startOffset": 0, + }, + "transitions": Array [], + "type": "BUG", + } + } + onAssign={[MockFunction]} + onChange={[MockFunction]} + togglePopup={[MockFunction]} + /> + <div + className="issue-comments" + > + <IssueCommentLine + comment={ + Object { + "author": "admin", + "authorActive": true, + "authorAvatar": "admin-avatar", + "authorLogin": "admin", + "authorName": "Admin", + "createdAt": "2017-07-05T09:33:29+0200", + "htmlText": "My comment", + "key": "1", + "markdown": "My comment", + "updatable": false, + } + } + key="1" + onDelete={[Function]} + onEdit={[Function]} + /> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx index c8da6cf250b..cdacdb47572 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx @@ -138,14 +138,13 @@ export default class IssueActionsBar extends React.PureComponent<Props, State> { /> </li> )} - {!isSecurityHotspot && - issue.effort && ( - <li className="issue-meta"> - <span className="issue-meta-label"> - {translateWithParameters('issue.x_effort', issue.effort)} - </span> - </li> - )} + {!isSecurityHotspot && issue.effort && ( + <li className="issue-meta"> + <span className="issue-meta-label"> + {translateWithParameters('issue.x_effort', issue.effort)} + </span> + </li> + )} {canComment && ( <IssueCommentAction commentAutoTriggered={this.state.commentAutoTriggered} diff --git a/server/sonar-web/src/main/js/components/nav/NavBar.tsx b/server/sonar-web/src/main/js/components/nav/NavBar.tsx index 35a4239c53e..af7cc7e7d0a 100644 --- a/server/sonar-web/src/main/js/components/nav/NavBar.tsx +++ b/server/sonar-web/src/main/js/components/nav/NavBar.tsx @@ -36,7 +36,7 @@ interface State { } export default class NavBar extends React.PureComponent<Props, State> { - throttledFollowHorizontalScroll: (() => void); + throttledFollowHorizontalScroll: () => void; constructor(props: Props) { super(props); diff --git a/server/sonar-web/src/main/js/components/workspace/Workspace.tsx b/server/sonar-web/src/main/js/components/workspace/Workspace.tsx index 4647079df4b..38c419c105c 100644 --- a/server/sonar-web/src/main/js/components/workspace/Workspace.tsx +++ b/server/sonar-web/src/main/js/components/workspace/Workspace.tsx @@ -133,8 +133,8 @@ export default class Workspace extends React.PureComponent<{}, State> { if (this.mounted) { const { key, name, qualifier } = details; this.setState((state: State) => ({ - components: state.components.map( - component => (component.key === key ? { ...component, name, qualifier } : component) + components: state.components.map(component => + component.key === key ? { ...component, name, qualifier } : component ) })); } diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 76f9a528015..030f3b09e2d 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -119,6 +119,29 @@ export function mockComponent(overrides: Partial<T.Component> = {}): T.Component }; } +export function mockComponentMeasure( + file = false, + overrides: Partial<T.ComponentMeasure> = {} +): T.ComponentMeasure { + if (file) { + return { + key: 'foo:src/index.tsx', + name: 'index.tsx', + qualifier: 'FIL', + path: 'src/index.tsx', + measures: [{ metric: 'bugs', value: '1', bestValue: false }], + ...overrides + }; + } + return { + key: 'foo', + name: 'Foo', + qualifier: 'TRK', + measures: [{ metric: 'bugs', value: '12', bestValue: false }], + ...overrides + }; +} + export function mockQualityGateStatusCondition( overrides: Partial<T.QualityGateStatusCondition> = {} ): T.QualityGateStatusCondition { @@ -354,6 +377,39 @@ export function mockRule(overrides: Partial<T.Rule> = {}): T.Rule { } as T.Rule; } +export function mockRuleDetails(overrides: Partial<T.RuleDetails> = {}): T.RuleDetails { + return { + key: 'squid:S1337', + repo: 'squid', + name: '".equals()" should not be used to test the values of "Atomic" classes', + createdAt: '2014-12-16T17:26:54+0100', + htmlDesc: '', + mdDesc: '', + severity: 'MAJOR', + status: 'READY', + isTemplate: false, + tags: [], + sysTags: ['multi-threading'], + lang: 'java', + langName: 'Java', + params: [], + defaultDebtRemFnType: 'CONSTANT_ISSUE', + defaultDebtRemFnOffset: '5min', + debtOverloaded: false, + debtRemFnType: 'CONSTANT_ISSUE', + debtRemFnOffset: '5min', + defaultRemFnType: 'CONSTANT_ISSUE', + defaultRemFnBaseEffort: '5min', + remFnType: 'CONSTANT_ISSUE', + remFnBaseEffort: '5min', + remFnOverloaded: false, + scope: 'MAIN', + isExternal: false, + type: 'BUG', + ...overrides + }; +} + export function mockShortLivingBranch( overrides: Partial<T.ShortLivingBranch> = {} ): T.ShortLivingBranch { |