@@ -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", |
@@ -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", |
@@ -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": { |
@@ -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) { |
@@ -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" /> |
@@ -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> |
@@ -28,7 +28,7 @@ jest.mock('../embed-docs-modal/SuggestionsProvider', () => { | |||
return this.props.children; | |||
} | |||
} | |||
return { default: SuggestionsProvider }; | |||
}); | |||
@@ -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 { |
@@ -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} |
@@ -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> | |||
)} |
@@ -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> | |||
); | |||
} |
@@ -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} | |||
/> | |||
)} | |||
</> | |||
); | |||
} |
@@ -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> | |||
); |
@@ -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> | |||
); | |||
@@ -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() |
@@ -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"> |
@@ -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" |
@@ -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} />} | |||
@@ -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> |
@@ -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> | |||
); |
@@ -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> |
@@ -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} | |||
/> | |||
)} | |||
</> | |||
); | |||
} |
@@ -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"> |
@@ -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> |
@@ -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} /> |
@@ -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 && ( |
@@ -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> | |||
); | |||
}; |
@@ -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> | |||
); | |||
}; |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
} | |||
/> |
@@ -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 && |
@@ -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()} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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> | |||
); | |||
} |
@@ -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} | |||
/> | |||
) | |||
); | |||
}; | |||
@@ -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> | |||
); | |||
} |
@@ -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 /> |
@@ -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 |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
</> | |||
); |
@@ -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> | |||
</> | |||
); |
@@ -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 |
@@ -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] | |||
) | |||
); | |||
} |
@@ -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> | |||
</> | |||
); |
@@ -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} />); | |||
} |
@@ -88,7 +88,7 @@ exports[`should render correctly 2`] = ` | |||
count={2} | |||
loadMore={[Function]} | |||
ready={true} | |||
total={2} | |||
total={4} | |||
/> | |||
</div> | |||
</div> |
@@ -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> | |||
)} |
@@ -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> |
@@ -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 = |
@@ -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 |
@@ -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"> |
@@ -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> |
@@ -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> |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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> |
@@ -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> |
@@ -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> | |||
); |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); |
@@ -70,8 +70,9 @@ export function EmptyOverview({ | |||
} | |||
/> | |||
)} | |||
{!hasBranches && | |||
!hasAnalyses && <AnalyzeTutorial component={component} currentUser={currentUser} />} | |||
{!hasBranches && !hasAnalyses && ( | |||
<AnalyzeTutorial component={component} currentUser={currentUser} /> | |||
)} | |||
</> | |||
) : ( | |||
<WarningMessage |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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> |
@@ -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 }; | |||
} | |||
@@ -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 | |||
); | |||
}; | |||
@@ -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} />); | |||
} |
@@ -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> | |||
`; |
@@ -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} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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> |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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 | |||
) | |||
}; | |||
}) |
@@ -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> |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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> | |||
); | |||
} |
@@ -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> |
@@ -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 ? ( |
@@ -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); |
@@ -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 => { |
@@ -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> |
@@ -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> | |||
); | |||
} |
@@ -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"> |
@@ -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 |
@@ -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> |
@@ -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> |
@@ -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> | |||
); | |||
} |
@@ -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" |
@@ -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 |
@@ -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 |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |