@@ -0,0 +1,71 @@ | |||
/* | |||
* 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 { getJSON } from 'sonar-ui-common/helpers/request'; | |||
import { getAlmOrganization } from '../alm-integration'; | |||
jest.useFakeTimers(); | |||
jest.mock('sonar-ui-common/helpers/request', () => ({ | |||
...jest.requireActual('sonar-ui-common/helpers/request'), | |||
getJSON: jest.fn() | |||
})); | |||
jest.mock('../../app/utils/throwGlobalError', () => ({ | |||
default: jest.fn().mockImplementation(r => Promise.reject(r)) | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllTimers(); | |||
jest.clearAllMocks(); | |||
}); | |||
describe('getAlmOrganization', () => { | |||
it('should return the organization', () => { | |||
const response = { almOrganization: { key: 'foo', name: 'Foo' } }; | |||
(getJSON as jest.Mock).mockResolvedValue(response); | |||
return expect(getAlmOrganization({ installationId: 'foo' })).resolves.toEqual(response); | |||
}); | |||
it('should reject with an error', () => { | |||
const error = { status: 401 }; | |||
(getJSON as jest.Mock).mockRejectedValue(error); | |||
return expect(getAlmOrganization({ installationId: 'foo' })).rejects.toEqual(error); | |||
}); | |||
it('should try until getting the organization', async () => { | |||
(getJSON as jest.Mock).mockRejectedValue({ status: 404 }); | |||
const spy = jest.fn(); | |||
getAlmOrganization({ installationId: 'foo' }).then(spy); | |||
for (let i = 1; i < 5; i++) { | |||
expect(getJSON).toBeCalledTimes(i); | |||
expect(spy).not.toBeCalled(); | |||
await new Promise(setImmediate); | |||
jest.runAllTimers(); | |||
} | |||
expect(getJSON).toBeCalledTimes(5); | |||
expect(spy).not.toBeCalled(); | |||
const response = { almOrganization: { key: 'foo', name: 'Foo' } }; | |||
(getJSON as jest.Mock).mockResolvedValue(response); | |||
await new Promise(setImmediate); | |||
jest.runAllTimers(); | |||
expect(getJSON).toBeCalledTimes(6); | |||
await new Promise(setImmediate); | |||
expect(spy).toBeCalledWith(response); | |||
}); | |||
}); |
@@ -0,0 +1,41 @@ | |||
/* | |||
* 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 { getJSON, post } from 'sonar-ui-common/helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export function getAlmDefinitions(): Promise<T.AlmSettingsDefinitions> { | |||
return getJSON('/api/alm_settings/list_definitions').catch(throwGlobalError); | |||
} | |||
export function createGithubConfiguration(data: T.GithubDefinition) { | |||
return post('/api/alm_settings/create_github', data).catch(throwGlobalError); | |||
} | |||
export function updateGithubConfiguration(data: T.GithubDefinition & { newKey: string }) { | |||
return post('/api/alm_settings/update_github', data).catch(throwGlobalError); | |||
} | |||
export function deleteConfiguration(key: string) { | |||
return post('/api/alm_settings/delete', { key }).catch(throwGlobalError); | |||
} | |||
export function countBindedProjects(instance: string) { | |||
return getJSON('/api/alm_settings/count_binding', { instance }).catch(throwGlobalError); | |||
} |
@@ -30,6 +30,20 @@ declare namespace T { | |||
installationUrl: string; | |||
} | |||
export interface AlmSettingsDefinitions { | |||
github: GithubDefinition[]; | |||
} | |||
export interface BaseAlmDefinition { | |||
key: string; | |||
url: string; | |||
} | |||
export interface GithubDefinition extends BaseAlmDefinition { | |||
appId: string; | |||
privateKey: string; | |||
} | |||
export interface AlmOrganization extends OrganizationBase { | |||
almUrl: string; | |||
key: string; |
@@ -23,11 +23,13 @@ import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { | |||
ANALYSIS_SCOPE_CATEGORY, | |||
LANGUAGES_CATEGORY, | |||
NEW_CODE_PERIOD_CATEGORY | |||
NEW_CODE_PERIOD_CATEGORY, | |||
PULL_REQUEST_DECORATION_CATEGORY | |||
} from './AdditionalCategoryKeys'; | |||
import { AnalysisScope } from './AnalysisScope'; | |||
import Languages from './Languages'; | |||
import NewCodePeriod from './NewCodePeriod'; | |||
import PullRequestDecoration from './pullRequestDecoration/PullRequestDecoration'; | |||
export interface AdditionalCategoryComponentProps { | |||
parentComponent: T.Component | undefined; | |||
@@ -67,6 +69,14 @@ export const ADDITIONAL_CATEGORIES: AdditionalCategory[] = [ | |||
availableGlobally: true, | |||
availableForProject: true, | |||
displayTab: false | |||
}, | |||
{ | |||
key: PULL_REQUEST_DECORATION_CATEGORY, | |||
name: translate('property.category.pull_request'), | |||
renderComponent: getPullRequestDecorationComponent, | |||
availableGlobally: true, | |||
availableForProject: false, | |||
displayTab: true | |||
} | |||
]; | |||
@@ -81,3 +91,7 @@ function getNewCodePeriodComponent() { | |||
function getAnalysisScopeComponent(props: AdditionalCategoryComponentProps) { | |||
return <AnalysisScope {...props} />; | |||
} | |||
function getPullRequestDecorationComponent() { | |||
return <PullRequestDecoration />; | |||
} |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
export const ANALYSIS_SCOPE_CATEGORY = 'exclusions'; | |||
export const LANGUAGES_CATEGORY = 'languages'; | |||
export const NEW_CODE_PERIOD_CATEGORY = 'new_code_period'; | |||
export const ANALYSIS_SCOPE_CATEGORY = 'exclusions'; | |||
export const PULL_REQUEST_DECORATION_CATEGORY = 'pull_request_decoration'; |
@@ -0,0 +1,77 @@ | |||
/* | |||
* 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 AlmPRDecorationFormModalRenderer from './AlmPRDecorationFormModalRenderer'; | |||
interface Props { | |||
alm: string; | |||
data: T.GithubDefinition; | |||
onCancel: () => void; | |||
onSubmit: (data: T.GithubDefinition, originalKey: string) => void; | |||
} | |||
interface State { | |||
formData: T.GithubDefinition; | |||
} | |||
export default class AlmPRDecorationFormModal extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { formData: props.data }; | |||
} | |||
handleFieldChange = (fieldId: keyof T.GithubDefinition, value: string) => { | |||
this.setState(({ formData }) => ({ | |||
formData: { | |||
...formData, | |||
[fieldId]: value | |||
} | |||
})); | |||
}; | |||
handleFormSubmit = () => { | |||
this.props.onSubmit(this.state.formData, this.props.data.key); | |||
}; | |||
canSubmit = () => { | |||
return Object.values(this.state.formData).reduce( | |||
(result, value) => result && value.length > 0, | |||
true | |||
); | |||
}; | |||
render() { | |||
const { alm, data } = this.props; | |||
const { formData } = this.state; | |||
return ( | |||
<AlmPRDecorationFormModalRenderer | |||
alm={alm} | |||
canSubmit={this.canSubmit} | |||
formData={formData} | |||
onCancel={this.props.onCancel} | |||
onFieldChange={this.handleFieldChange} | |||
onSubmit={this.handleFormSubmit} | |||
originalKey={data.key} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,147 @@ | |||
/* | |||
* 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 { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip'; | |||
import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { ALM_KEYS } from '../../utils'; | |||
export interface AlmPRDecorationFormModalProps { | |||
alm: string; | |||
canSubmit: () => boolean; | |||
formData: T.GithubDefinition; | |||
onCancel: () => void; | |||
onSubmit: () => void; | |||
onFieldChange: (id: string, value: string) => void; | |||
originalKey: string; | |||
} | |||
function renderField(params: { | |||
autoFocus?: boolean; | |||
formData: T.GithubDefinition; | |||
help: boolean; | |||
id: string; | |||
isTextArea: boolean; | |||
maxLength: number; | |||
onFieldChange: (id: string, value: string) => void; | |||
propKey: keyof T.GithubDefinition; | |||
}) { | |||
const { autoFocus, formData, help, id, isTextArea, maxLength, onFieldChange, propKey } = params; | |||
return ( | |||
<div className="modal-field"> | |||
<label htmlFor={id}> | |||
{translate('settings.pr_decoration.form', id)} | |||
<em className="mandatory spacer-right">*</em> | |||
{help && <HelpTooltip overlay={translate('settings.pr_decoration.form', id, 'help')} />} | |||
</label> | |||
{isTextArea ? ( | |||
<textarea | |||
className="settings-large-input" | |||
id="privateKey" | |||
maxLength={maxLength} | |||
onChange={e => onFieldChange(propKey, e.currentTarget.value)} | |||
required={true} | |||
rows={5} | |||
value={formData[propKey]} | |||
/> | |||
) : ( | |||
<input | |||
autoFocus={autoFocus} | |||
className="input-super-large" | |||
id={id} | |||
maxLength={maxLength} | |||
name={id} | |||
onChange={e => onFieldChange(propKey, e.currentTarget.value)} | |||
size={50} | |||
type="text" | |||
value={formData[propKey]} | |||
/> | |||
)} | |||
</div> | |||
); | |||
} | |||
export default function AlmPRDecorationFormModalRenderer(props: AlmPRDecorationFormModalProps) { | |||
const { alm, formData, onFieldChange, originalKey } = props; | |||
const header = translate('settings.pr_decoration.form.header', originalKey ? 'edit' : 'create'); | |||
return ( | |||
<SimpleModal header={header} onClose={props.onCancel} onSubmit={props.onSubmit} size="medium"> | |||
{({ onCloseClick, onFormSubmit, submitting }) => ( | |||
<form className="views-form" onSubmit={onFormSubmit}> | |||
<div className="modal-head"> | |||
<h2>{header}</h2> | |||
</div> | |||
<div className="modal-body modal-container"> | |||
{renderField({ | |||
autoFocus: true, | |||
id: 'name', | |||
formData, | |||
propKey: 'key', | |||
maxLength: 40, | |||
onFieldChange, | |||
help: true, | |||
isTextArea: false | |||
})} | |||
{renderField({ | |||
id: `url.${alm}`, | |||
formData, | |||
propKey: 'url', | |||
maxLength: 2000, | |||
onFieldChange, | |||
help: false, | |||
isTextArea: false | |||
})} | |||
{alm === ALM_KEYS.GITHUB && | |||
renderField({ | |||
id: 'app_id', | |||
formData, | |||
propKey: 'appId', | |||
maxLength: 80, | |||
onFieldChange, | |||
help: false, | |||
isTextArea: false | |||
})} | |||
{renderField({ | |||
id: 'private_key', | |||
formData, | |||
propKey: 'privateKey', | |||
maxLength: 2000, | |||
onFieldChange, | |||
help: false, | |||
isTextArea: true | |||
})} | |||
</div> | |||
<div className="modal-foot"> | |||
<DeferredSpinner className="spacer-right" loading={submitting} /> | |||
<SubmitButton disabled={submitting || !props.canSubmit()}> | |||
{translate('settings.pr_decoration.form.save')} | |||
</SubmitButton> | |||
<ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink> | |||
</div> | |||
</form> | |||
)} | |||
</SimpleModal> | |||
); | |||
} |
@@ -0,0 +1,62 @@ | |||
/* | |||
* 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 { FormattedMessage } from 'react-intl'; | |||
import ConfirmModal from 'sonar-ui-common/components/controls/ConfirmModal'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
export interface DeleteModalProps { | |||
id: string; | |||
projectCount?: number; | |||
onDelete: (id: string) => void; | |||
onCancel: () => void; | |||
} | |||
function showProjectCountWarning(projectCount?: number) { | |||
if (projectCount === undefined) { | |||
return <p>{translate('settings.pr_decoration.delete.no_info')}</p>; | |||
} | |||
return projectCount ? ( | |||
<p>{translateWithParameters('settings.pr_decoration.delete.info', projectCount)} </p> | |||
) : null; | |||
} | |||
export default function DeleteModal({ id, onDelete, onCancel, projectCount }: DeleteModalProps) { | |||
return ( | |||
<ConfirmModal | |||
confirmButtonText={translate('delete')} | |||
confirmData={id} | |||
header={translate('settings.pr_decoration.delete.header')} | |||
onClose={onCancel} | |||
onConfirm={onDelete}> | |||
<> | |||
<p className="spacer-bottom"> | |||
<FormattedMessage | |||
defaultMessage={translate('settings.pr_decoration.delete.message')} | |||
id="settings.pr_decoration.delete.message" | |||
values={{ id: <b>{id}</b> }} | |||
/> | |||
</p> | |||
{showProjectCountWarning(projectCount)} | |||
</> | |||
</ConfirmModal> | |||
); | |||
} |
@@ -0,0 +1,118 @@ | |||
/* | |||
* 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 { | |||
countBindedProjects, | |||
createGithubConfiguration, | |||
deleteConfiguration, | |||
updateGithubConfiguration | |||
} from '../../../../api/almSettings'; | |||
import { ALM_KEYS } from '../../utils'; | |||
import TabRenderer from './TabRenderer'; | |||
interface Props { | |||
definitions: T.GithubDefinition[]; | |||
onUpdateDefinitions: () => void; | |||
} | |||
interface State { | |||
definitionInEdition?: T.GithubDefinition; | |||
definitionKeyForDeletion?: string; | |||
projectCount?: number; | |||
} | |||
export default class GithubTab extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = {}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleCancel = () => { | |||
this.setState({ | |||
definitionKeyForDeletion: undefined, | |||
definitionInEdition: undefined, | |||
projectCount: undefined | |||
}); | |||
}; | |||
deleteConfiguration = (id: string) => { | |||
return deleteConfiguration(id) | |||
.then(this.props.onUpdateDefinitions) | |||
.then(() => { | |||
if (this.mounted) { | |||
this.setState({ definitionKeyForDeletion: undefined }); | |||
} | |||
}); | |||
}; | |||
handleCreate = () => { | |||
this.setState({ definitionInEdition: { key: '', appId: '', url: '', privateKey: '' } }); | |||
}; | |||
handleDelete = (config: T.GithubDefinition) => { | |||
this.setState({ definitionKeyForDeletion: config.key }); | |||
return countBindedProjects(config.key).then(projectCount => { | |||
if (this.mounted) { | |||
this.setState({ projectCount }); | |||
} | |||
}); | |||
}; | |||
handleEdit = (config: T.GithubDefinition) => { | |||
this.setState({ definitionInEdition: config }); | |||
}; | |||
handleSubmit = (config: T.GithubDefinition, originalKey: string) => { | |||
const call = originalKey | |||
? updateGithubConfiguration({ newKey: config.key, ...config, key: originalKey }) | |||
: createGithubConfiguration(config); | |||
return call.then(this.props.onUpdateDefinitions).then(() => { | |||
if (this.mounted) { | |||
this.setState({ definitionInEdition: undefined }); | |||
} | |||
}); | |||
}; | |||
render() { | |||
const { definitions } = this.props; | |||
const { definitionKeyForDeletion, definitionInEdition, projectCount } = this.state; | |||
return ( | |||
<TabRenderer | |||
alm={ALM_KEYS.GITHUB} | |||
definitionInEdition={definitionInEdition} | |||
definitionKeyForDeletion={definitionKeyForDeletion} | |||
definitions={definitions} | |||
onCancel={this.handleCancel} | |||
onConfirmDelete={this.deleteConfiguration} | |||
onCreate={this.handleCreate} | |||
onDelete={this.handleDelete} | |||
onEdit={this.handleEdit} | |||
onSubmit={this.handleSubmit} | |||
projectCount={projectCount} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,71 @@ | |||
/* | |||
* 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 { ButtonIcon } from 'sonar-ui-common/components/controls/buttons'; | |||
import DeleteIcon from 'sonar-ui-common/components/icons/DeleteIcon'; | |||
import EditIcon from 'sonar-ui-common/components/icons/EditIcon'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { ALM_KEYS } from '../../utils'; | |||
export interface PRDecorationTableProps { | |||
definitions: T.GithubDefinition[]; | |||
alm: ALM_KEYS; | |||
onDelete: (config: T.GithubDefinition) => void; | |||
onEdit: (config: T.GithubDefinition) => void; | |||
} | |||
export default function PRDecorationTable(props: PRDecorationTableProps) { | |||
const { definitions, alm } = props; | |||
return ( | |||
<> | |||
<table className="data zebra spacer-bottom"> | |||
<thead> | |||
<tr> | |||
<th>{translate('settings.pr_decoration.table.column.name')}</th> | |||
<th>{translate(`settings.pr_decoration.table.column.${alm}.url`)}</th> | |||
<th>{translate('settings.pr_decoration.table.column.app_id')}</th> | |||
<th className="thin">{translate('settings.pr_decoration.table.column.edit')}</th> | |||
<th className="thin">{translate('settings.pr_decoration.table.column.delete')}</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{definitions.map(definition => ( | |||
<tr key={definition.key}> | |||
<td>{definition.key}</td> | |||
<td>{definition.url}</td> | |||
<td>{definition.appId}</td> | |||
<td> | |||
<ButtonIcon onClick={() => props.onEdit(definition)}> | |||
<EditIcon /> | |||
</ButtonIcon> | |||
</td> | |||
<td> | |||
<ButtonIcon onClick={() => props.onDelete(definition)}> | |||
<DeleteIcon /> | |||
</ButtonIcon> | |||
</td> | |||
</tr> | |||
))} | |||
</tbody> | |||
</table> | |||
</> | |||
); | |||
} |
@@ -0,0 +1,73 @@ | |||
/* | |||
* 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 BoxedTabs from 'sonar-ui-common/components/controls/BoxedTabs'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { almName, ALM_KEYS } from '../../utils'; | |||
import GithubTab from './GithubTab'; | |||
export interface PRDecorationTabsProps { | |||
definitions: T.AlmSettingsDefinitions; | |||
currentAlm: ALM_KEYS; | |||
loading: boolean; | |||
onSelectAlm: (alm: ALM_KEYS) => void; | |||
onUpdateDefinitions: () => void; | |||
} | |||
export default function PRDecorationTabs(props: PRDecorationTabsProps) { | |||
const { definitions, currentAlm, loading } = props; | |||
if (loading) { | |||
return <DeferredSpinner />; | |||
} | |||
return ( | |||
<> | |||
<header className="page-header"> | |||
<h1 className="page-title">{translate('settings.pr_decoration.title')}</h1> | |||
</header> | |||
<h3 className="settings-definition-name" title={translate('settings.pr_decoration.header')}> | |||
{translate('settings.pr_decoration.header')} | |||
</h3> | |||
<div className="markdown small spacer-top big-spacer-bottom"> | |||
{translate('settings.pr_decoration.description')} | |||
</div> | |||
<BoxedTabs | |||
onSelect={props.onSelectAlm} | |||
selected={currentAlm} | |||
tabs={[ | |||
{ | |||
key: ALM_KEYS.GITHUB, | |||
label: almName[ALM_KEYS.GITHUB] | |||
} | |||
]} | |||
/> | |||
<div className="boxed-group boxed-group-inner"> | |||
<GithubTab | |||
definitions={definitions.github} | |||
onUpdateDefinitions={props.onUpdateDefinitions} | |||
/> | |||
</div> | |||
</> | |||
); | |||
} |
@@ -0,0 +1,80 @@ | |||
/* | |||
* 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 { getAlmDefinitions } from '../../../../api/almSettings'; | |||
import { ALM_KEYS } from '../../utils'; | |||
import PRDecorationTabs from './PRDecorationTabs'; | |||
interface State { | |||
definitions: T.AlmSettingsDefinitions; | |||
currentAlm: ALM_KEYS; | |||
loading: boolean; | |||
} | |||
export default class PullRequestDecoration extends React.PureComponent<{}, State> { | |||
mounted = false; | |||
state: State = { | |||
definitions: { | |||
[ALM_KEYS.GITHUB]: [] | |||
}, | |||
currentAlm: ALM_KEYS.GITHUB, | |||
loading: true | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchPullRequestDecorationSetting(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchPullRequestDecorationSetting = () => { | |||
return getAlmDefinitions() | |||
.then(definitions => { | |||
if (this.mounted) { | |||
this.setState({ | |||
definitions, | |||
loading: false | |||
}); | |||
} | |||
}) | |||
.catch(() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}); | |||
}; | |||
handleSelectAlm = (currentAlm: ALM_KEYS) => { | |||
this.setState({ currentAlm }); | |||
}; | |||
render() { | |||
return ( | |||
<PRDecorationTabs | |||
onSelectAlm={this.handleSelectAlm} | |||
onUpdateDefinitions={this.fetchPullRequestDecorationSetting} | |||
{...this.state} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,93 @@ | |||
/* | |||
* 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 { FormattedMessage } from 'react-intl'; | |||
import { Link } from 'react-router'; | |||
import { Button } from 'sonar-ui-common/components/controls/buttons'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { ALM_KEYS } from '../../utils'; | |||
import AlmPRDecorationFormModal from './AlmPRDecorationFormModal'; | |||
import DeleteModal from './DeleteModal'; | |||
import PRDecorationTable from './PRDecorationTable'; | |||
export interface TabRendererProps { | |||
alm: ALM_KEYS; | |||
definitionInEdition?: T.GithubDefinition; | |||
definitionKeyForDeletion?: string; | |||
definitions: T.GithubDefinition[]; | |||
onCancel: () => void; | |||
onConfirmDelete: (id: string) => void; | |||
onCreate: () => void; | |||
onDelete: (config: T.GithubDefinition) => void; | |||
onEdit: (config: T.GithubDefinition) => void; | |||
onSubmit: (config: T.GithubDefinition, originalKey: string) => void; | |||
projectCount?: number; | |||
} | |||
export default function TabRenderer(props: TabRendererProps) { | |||
const { alm, definitions, definitionKeyForDeletion, definitionInEdition, projectCount } = props; | |||
return ( | |||
<> | |||
<Alert className="spacer-top huge-spacer-bottom" variant="info"> | |||
<FormattedMessage | |||
defaultMessage={translate(`settings.pr_decoration.${alm}.info`)} | |||
id={`settings.pr_decoration.${alm}.info`} | |||
values={{ | |||
link: ( | |||
<Link to="/documentation/analysis/pull-request/#pr-decoration"> | |||
{translate('learn_more')} | |||
</Link> | |||
) | |||
}} | |||
/> | |||
</Alert> | |||
<div className="big-spacer-bottom display-flex-space-between"> | |||
<h4 className="display-inline">{translate('settings.pr_decoration.table.title')}</h4> | |||
<Button onClick={props.onCreate}>{translate('settings.pr_decoration.table.create')}</Button> | |||
</div> | |||
<PRDecorationTable | |||
alm={alm} | |||
definitions={definitions} | |||
onDelete={props.onDelete} | |||
onEdit={props.onEdit} | |||
/> | |||
{definitionKeyForDeletion && ( | |||
<DeleteModal | |||
id={definitionKeyForDeletion} | |||
onCancel={props.onCancel} | |||
onDelete={props.onConfirmDelete} | |||
projectCount={projectCount} | |||
/> | |||
)} | |||
{definitionInEdition && ( | |||
<AlmPRDecorationFormModal | |||
alm={ALM_KEYS.GITHUB} | |||
data={definitionInEdition} | |||
onCancel={props.onCancel} | |||
onSubmit={props.onSubmit} | |||
/> | |||
)} | |||
</> | |||
); | |||
} |
@@ -0,0 +1,88 @@ | |||
/* | |||
* 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { mockGithubDefinition } from '../../../../../helpers/testMocks'; | |||
import { ALM_KEYS } from '../../../utils'; | |||
import AlmPRDecorationFormModal from '../AlmPRDecorationFormModal'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should handle field changes', () => { | |||
const wrapper = shallowRender(); | |||
const formData = { | |||
key: 'github - example', | |||
url: 'http://github.com', | |||
appId: '34812568251', | |||
privateKey: 'gs7df9g7d9fsg7x9df7g9xdg' | |||
}; | |||
wrapper.instance().handleFieldChange('key', formData.key); | |||
wrapper.instance().handleFieldChange('url', formData.url); | |||
wrapper.instance().handleFieldChange('appId', formData.appId); | |||
wrapper.instance().handleFieldChange('privateKey', formData.privateKey); | |||
expect(wrapper.state()).toEqual({ formData }); | |||
}); | |||
it('should handle form submit', async () => { | |||
const onSubmit = jest.fn(); | |||
const wrapper = shallowRender({ | |||
onSubmit, | |||
data: { key: 'originalKey', appId: '', privateKey: '', url: '' } | |||
}); | |||
const formData = { | |||
key: 'github instance', | |||
url: 'http://github.enterprise.com', | |||
appId: '34812568251', | |||
privateKey: 'gs7df9g7d9fsg7x9df7g9xdg' | |||
}; | |||
wrapper.setState({ formData }); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleFormSubmit(); | |||
expect(onSubmit).toHaveBeenCalledWith(formData, 'originalKey'); | |||
}); | |||
it('should (dis)allow submit by validating its state', async () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper.instance().canSubmit()).toBe(false); | |||
wrapper.setState({ formData: mockGithubDefinition() }); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.instance().canSubmit()).toBe(true); | |||
}); | |||
function shallowRender(props: Partial<AlmPRDecorationFormModal['props']> = {}) { | |||
return shallow<AlmPRDecorationFormModal>( | |||
<AlmPRDecorationFormModal | |||
alm={ALM_KEYS.GITHUB} | |||
data={{ appId: '', key: '', privateKey: '', url: '' }} | |||
onCancel={jest.fn()} | |||
onSubmit={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,45 @@ | |||
/* | |||
* 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockGithubDefinition } from '../../../../../helpers/testMocks'; | |||
import { ALM_KEYS } from '../../../utils'; | |||
import AlmPRDecorationFormModalRenderer, { | |||
AlmPRDecorationFormModalProps | |||
} from '../AlmPRDecorationFormModalRenderer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender().dive()).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<AlmPRDecorationFormModalProps> = {}) { | |||
return shallow( | |||
<AlmPRDecorationFormModalRenderer | |||
alm={ALM_KEYS.GITHUB} | |||
canSubmit={jest.fn()} | |||
formData={mockGithubDefinition()} | |||
onCancel={jest.fn()} | |||
onFieldChange={jest.fn()} | |||
onSubmit={jest.fn()} | |||
originalKey="" | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,33 @@ | |||
/* | |||
* 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import DeleteModal, { DeleteModalProps } from '../DeleteModal'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ projectCount: undefined })).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<DeleteModalProps> = {}) { | |||
return shallow( | |||
<DeleteModal id="1" onCancel={jest.fn()} onDelete={jest.fn()} projectCount={4} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,127 @@ | |||
/* | |||
* 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { | |||
createGithubConfiguration, | |||
deleteConfiguration, | |||
updateGithubConfiguration | |||
} from '../../../../../api/almSettings'; | |||
import { mockGithubDefinition } from '../../../../../helpers/testMocks'; | |||
import GithubTab from '../GithubTab'; | |||
jest.mock('../../../../../api/almSettings', () => ({ | |||
countBindedProjects: jest.fn().mockResolvedValue(2), | |||
createGithubConfiguration: jest.fn().mockResolvedValue({}), | |||
deleteConfiguration: jest.fn().mockResolvedValue({}), | |||
updateGithubConfiguration: jest.fn().mockResolvedValue({}) | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should handle cancel', async () => { | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ | |||
definitionKeyForDeletion: '12321', | |||
definitionInEdition: mockGithubDefinition() | |||
}); | |||
wrapper.instance().handleCancel(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().definitionKeyForDeletion).toBeUndefined(); | |||
expect(wrapper.state().definitionInEdition).toBeUndefined(); | |||
}); | |||
it('should delete config', async () => { | |||
const onUpdateDefinitions = jest.fn(); | |||
const wrapper = shallowRender({ onUpdateDefinitions }); | |||
wrapper.setState({ definitionKeyForDeletion: '123' }); | |||
await wrapper | |||
.instance() | |||
.deleteConfiguration('123') | |||
.then(() => { | |||
expect(deleteConfiguration).toBeCalledWith('123'); | |||
expect(onUpdateDefinitions).toBeCalled(); | |||
expect(wrapper.state().definitionKeyForDeletion).toBeUndefined(); | |||
}); | |||
}); | |||
it('should create config', async () => { | |||
const onUpdateDefinitions = jest.fn(); | |||
const config = { | |||
key: 'new conf', | |||
url: 'ewrqewr', | |||
appId: '3742985', | |||
privateKey: 'rt7r78ew6t87ret' | |||
}; | |||
const wrapper = shallowRender({ onUpdateDefinitions }); | |||
wrapper.setState({ definitionInEdition: config }); | |||
await wrapper | |||
.instance() | |||
.handleSubmit(config, '') | |||
.then(() => { | |||
expect(createGithubConfiguration).toBeCalledWith(config); | |||
expect(onUpdateDefinitions).toBeCalled(); | |||
expect(wrapper.state().definitionInEdition).toBeUndefined(); | |||
}); | |||
}); | |||
it('should update config', async () => { | |||
const onUpdateDefinitions = jest.fn(); | |||
const config = { | |||
key: 'new conf', | |||
url: 'ewrqewr', | |||
appId: '3742985', | |||
privateKey: 'rt7r78ew6t87ret' | |||
}; | |||
const wrapper = shallowRender({ onUpdateDefinitions }); | |||
wrapper.setState({ definitionInEdition: config }); | |||
await wrapper | |||
.instance() | |||
.handleSubmit(config, 'originalKey') | |||
.then(() => { | |||
expect(updateGithubConfiguration).toBeCalledWith({ | |||
newKey: 'new conf', | |||
...config, | |||
key: 'originalKey' | |||
}); | |||
expect(onUpdateDefinitions).toBeCalled(); | |||
expect(wrapper.state().definitionInEdition).toBeUndefined(); | |||
}); | |||
}); | |||
function shallowRender(props: Partial<GithubTab['props']> = {}) { | |||
return shallow<GithubTab>( | |||
<GithubTab definitions={[]} onUpdateDefinitions={jest.fn()} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,41 @@ | |||
/* | |||
* 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockGithubDefinition } from '../../../../../helpers/testMocks'; | |||
import { ALM_KEYS } from '../../../utils'; | |||
import PRDecorationTable, { PRDecorationTableProps } from '../PRDecorationTable'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ definitions: [mockGithubDefinition()] })).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<PRDecorationTableProps> = {}) { | |||
return shallow( | |||
<PRDecorationTable | |||
alm={ALM_KEYS.GITHUB} | |||
definitions={[]} | |||
onDelete={jest.fn()} | |||
onEdit={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,41 @@ | |||
/* | |||
* 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { ALM_KEYS } from '../../../utils'; | |||
import PRDecorationTabs, { PRDecorationTabsProps } from '../PRDecorationTabs'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ loading: false })).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<PRDecorationTabsProps> = {}) { | |||
return shallow( | |||
<PRDecorationTabs | |||
currentAlm={ALM_KEYS.GITHUB} | |||
definitions={{ github: [] }} | |||
loading={true} | |||
onSelectAlm={jest.fn()} | |||
onUpdateDefinitions={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,66 @@ | |||
/* | |||
* 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { getAlmDefinitions } from '../../../../../api/almSettings'; | |||
import { ALM_KEYS } from '../../../utils'; | |||
import PullRequestDecoration from '../PullRequestDecoration'; | |||
jest.mock('../../../../../api/almSettings', () => ({ | |||
getAlmDefinitions: jest.fn().mockResolvedValue({ github: [] }) | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should handle alm selection', async () => { | |||
const wrapper = shallowRender(); | |||
wrapper.setState({ currentAlm: ALM_KEYS.BITBUCKET }); | |||
wrapper.instance().handleSelectAlm(ALM_KEYS.GITHUB); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().currentAlm).toBe(ALM_KEYS.GITHUB); | |||
}); | |||
it('should fetch settings', async () => { | |||
const wrapper = shallowRender(); | |||
await wrapper | |||
.instance() | |||
.fetchPullRequestDecorationSetting() | |||
.then(() => { | |||
expect(getAlmDefinitions).toBeCalled(); | |||
expect(wrapper.state().definitions).toEqual({ github: [] }); | |||
expect(wrapper.state().loading).toBe(false); | |||
}); | |||
}); | |||
function shallowRender() { | |||
return shallow<PullRequestDecoration>(<PullRequestDecoration />); | |||
} |
@@ -0,0 +1,46 @@ | |||
/* | |||
* 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockGithubDefinition } from '../../../../../helpers/testMocks'; | |||
import { ALM_KEYS } from '../../../utils'; | |||
import TabRenderer, { TabRendererProps } from '../TabRenderer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ definitionKeyForDeletion: '123' })).toMatchSnapshot(); | |||
expect(shallowRender({ definitionInEdition: mockGithubDefinition() })).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<TabRendererProps> = {}) { | |||
return shallow( | |||
<TabRenderer | |||
alm={ALM_KEYS.GITHUB} | |||
definitions={[]} | |||
onCancel={jest.fn()} | |||
onConfirmDelete={jest.fn()} | |||
onCreate={jest.fn()} | |||
onDelete={jest.fn()} | |||
onEdit={jest.fn()} | |||
onSubmit={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,20 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<AlmPRDecorationFormModalRenderer | |||
alm="github" | |||
canSubmit={[Function]} | |||
formData={ | |||
Object { | |||
"appId": "", | |||
"key": "", | |||
"privateKey": "", | |||
"url": "", | |||
} | |||
} | |||
onCancel={[MockFunction]} | |||
onFieldChange={[Function]} | |||
onSubmit={[Function]} | |||
originalKey="" | |||
/> | |||
`; |
@@ -0,0 +1,144 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Modal | |||
contentLabel="settings.pr_decoration.form.header.create" | |||
onRequestClose={[MockFunction]} | |||
size="medium" | |||
> | |||
<form | |||
className="views-form" | |||
onSubmit={[Function]} | |||
> | |||
<div | |||
className="modal-head" | |||
> | |||
<h2> | |||
settings.pr_decoration.form.header.create | |||
</h2> | |||
</div> | |||
<div | |||
className="modal-body modal-container" | |||
> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="name" | |||
> | |||
settings.pr_decoration.form.name | |||
<em | |||
className="mandatory spacer-right" | |||
> | |||
* | |||
</em> | |||
<HelpTooltip | |||
overlay="settings.pr_decoration.form.name.help" | |||
/> | |||
</label> | |||
<input | |||
autoFocus={true} | |||
className="input-super-large" | |||
id="name" | |||
maxLength={40} | |||
name="name" | |||
onChange={[Function]} | |||
size={50} | |||
type="text" | |||
value="key" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="url.github" | |||
> | |||
settings.pr_decoration.form.url.github | |||
<em | |||
className="mandatory spacer-right" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
className="input-super-large" | |||
id="url.github" | |||
maxLength={2000} | |||
name="url.github" | |||
onChange={[Function]} | |||
size={50} | |||
type="text" | |||
value="http:alm.enterprise.com" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="app_id" | |||
> | |||
settings.pr_decoration.form.app_id | |||
<em | |||
className="mandatory spacer-right" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
className="input-super-large" | |||
id="app_id" | |||
maxLength={80} | |||
name="app_id" | |||
onChange={[Function]} | |||
size={50} | |||
type="text" | |||
value="123456" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="private_key" | |||
> | |||
settings.pr_decoration.form.private_key | |||
<em | |||
className="mandatory spacer-right" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<textarea | |||
className="settings-large-input" | |||
id="privateKey" | |||
maxLength={2000} | |||
onChange={[Function]} | |||
required={true} | |||
rows={5} | |||
value="asdf1234" | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="modal-foot" | |||
> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
settings.pr_decoration.form.save | |||
</SubmitButton> | |||
<ResetButtonLink | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</ResetButtonLink> | |||
</div> | |||
</form> | |||
</Modal> | |||
`; |
@@ -0,0 +1,60 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<ConfirmModal | |||
confirmButtonText="delete" | |||
confirmData="1" | |||
header="settings.pr_decoration.delete.header" | |||
onClose={[MockFunction]} | |||
onConfirm={[MockFunction]} | |||
> | |||
<p | |||
className="spacer-bottom" | |||
> | |||
<FormattedMessage | |||
defaultMessage="settings.pr_decoration.delete.message" | |||
id="settings.pr_decoration.delete.message" | |||
values={ | |||
Object { | |||
"id": <b> | |||
1 | |||
</b>, | |||
} | |||
} | |||
/> | |||
</p> | |||
<p> | |||
settings.pr_decoration.delete.info.4 | |||
</p> | |||
</ConfirmModal> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<ConfirmModal | |||
confirmButtonText="delete" | |||
confirmData="1" | |||
header="settings.pr_decoration.delete.header" | |||
onClose={[MockFunction]} | |||
onConfirm={[MockFunction]} | |||
> | |||
<p | |||
className="spacer-bottom" | |||
> | |||
<FormattedMessage | |||
defaultMessage="settings.pr_decoration.delete.message" | |||
id="settings.pr_decoration.delete.message" | |||
values={ | |||
Object { | |||
"id": <b> | |||
1 | |||
</b>, | |||
} | |||
} | |||
/> | |||
</p> | |||
<p> | |||
settings.pr_decoration.delete.no_info | |||
</p> | |||
</ConfirmModal> | |||
`; |
@@ -0,0 +1,14 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<TabRenderer | |||
alm="github" | |||
definitions={Array []} | |||
onCancel={[Function]} | |||
onConfirmDelete={[Function]} | |||
onCreate={[Function]} | |||
onDelete={[Function]} | |||
onEdit={[Function]} | |||
onSubmit={[Function]} | |||
/> | |||
`; |
@@ -0,0 +1,95 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<table | |||
className="data zebra spacer-bottom" | |||
> | |||
<thead> | |||
<tr> | |||
<th> | |||
settings.pr_decoration.table.column.name | |||
</th> | |||
<th> | |||
settings.pr_decoration.table.column.github.url | |||
</th> | |||
<th> | |||
settings.pr_decoration.table.column.app_id | |||
</th> | |||
<th | |||
className="thin" | |||
> | |||
settings.pr_decoration.table.column.edit | |||
</th> | |||
<th | |||
className="thin" | |||
> | |||
settings.pr_decoration.table.column.delete | |||
</th> | |||
</tr> | |||
</thead> | |||
<tbody /> | |||
</table> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<Fragment> | |||
<table | |||
className="data zebra spacer-bottom" | |||
> | |||
<thead> | |||
<tr> | |||
<th> | |||
settings.pr_decoration.table.column.name | |||
</th> | |||
<th> | |||
settings.pr_decoration.table.column.github.url | |||
</th> | |||
<th> | |||
settings.pr_decoration.table.column.app_id | |||
</th> | |||
<th | |||
className="thin" | |||
> | |||
settings.pr_decoration.table.column.edit | |||
</th> | |||
<th | |||
className="thin" | |||
> | |||
settings.pr_decoration.table.column.delete | |||
</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr | |||
key="key" | |||
> | |||
<td> | |||
key | |||
</td> | |||
<td> | |||
http:alm.enterprise.com | |||
</td> | |||
<td> | |||
123456 | |||
</td> | |||
<td> | |||
<ButtonIcon | |||
onClick={[Function]} | |||
> | |||
<EditIcon /> | |||
</ButtonIcon> | |||
</td> | |||
<td> | |||
<ButtonIcon | |||
onClick={[Function]} | |||
> | |||
<DeleteIcon /> | |||
</ButtonIcon> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</Fragment> | |||
`; |
@@ -0,0 +1,52 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<DeferredSpinner | |||
timeout={100} | |||
/> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<Fragment> | |||
<header | |||
className="page-header" | |||
> | |||
<h1 | |||
className="page-title" | |||
> | |||
settings.pr_decoration.title | |||
</h1> | |||
</header> | |||
<h3 | |||
className="settings-definition-name" | |||
title="settings.pr_decoration.header" | |||
> | |||
settings.pr_decoration.header | |||
</h3> | |||
<div | |||
className="markdown small spacer-top big-spacer-bottom" | |||
> | |||
settings.pr_decoration.description | |||
</div> | |||
<BoxedTabs | |||
onSelect={[MockFunction]} | |||
selected="github" | |||
tabs={ | |||
Array [ | |||
Object { | |||
"key": "github", | |||
"label": "Github Enterprise", | |||
}, | |||
] | |||
} | |||
/> | |||
<div | |||
className="boxed-group boxed-group-inner" | |||
> | |||
<GithubTab | |||
definitions={Array []} | |||
onUpdateDefinitions={[MockFunction]} | |||
/> | |||
</div> | |||
</Fragment> | |||
`; |
@@ -0,0 +1,15 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<PRDecorationTabs | |||
currentAlm="github" | |||
definitions={ | |||
Object { | |||
"github": Array [], | |||
} | |||
} | |||
loading={true} | |||
onSelectAlm={[Function]} | |||
onUpdateDefinitions={[Function]} | |||
/> | |||
`; |
@@ -0,0 +1,154 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<Alert | |||
className="spacer-top huge-spacer-bottom" | |||
variant="info" | |||
> | |||
<FormattedMessage | |||
defaultMessage="settings.pr_decoration.github.info" | |||
id="settings.pr_decoration.github.info" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation/analysis/pull-request/#pr-decoration" | |||
> | |||
learn_more | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
<div | |||
className="big-spacer-bottom display-flex-space-between" | |||
> | |||
<h4 | |||
className="display-inline" | |||
> | |||
settings.pr_decoration.table.title | |||
</h4> | |||
<Button | |||
onClick={[MockFunction]} | |||
> | |||
settings.pr_decoration.table.create | |||
</Button> | |||
</div> | |||
<PRDecorationTable | |||
alm="github" | |||
definitions={Array []} | |||
onDelete={[MockFunction]} | |||
onEdit={[MockFunction]} | |||
/> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<Fragment> | |||
<Alert | |||
className="spacer-top huge-spacer-bottom" | |||
variant="info" | |||
> | |||
<FormattedMessage | |||
defaultMessage="settings.pr_decoration.github.info" | |||
id="settings.pr_decoration.github.info" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation/analysis/pull-request/#pr-decoration" | |||
> | |||
learn_more | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
<div | |||
className="big-spacer-bottom display-flex-space-between" | |||
> | |||
<h4 | |||
className="display-inline" | |||
> | |||
settings.pr_decoration.table.title | |||
</h4> | |||
<Button | |||
onClick={[MockFunction]} | |||
> | |||
settings.pr_decoration.table.create | |||
</Button> | |||
</div> | |||
<PRDecorationTable | |||
alm="github" | |||
definitions={Array []} | |||
onDelete={[MockFunction]} | |||
onEdit={[MockFunction]} | |||
/> | |||
<DeleteModal | |||
id="123" | |||
onCancel={[MockFunction]} | |||
onDelete={[MockFunction]} | |||
/> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly 3`] = ` | |||
<Fragment> | |||
<Alert | |||
className="spacer-top huge-spacer-bottom" | |||
variant="info" | |||
> | |||
<FormattedMessage | |||
defaultMessage="settings.pr_decoration.github.info" | |||
id="settings.pr_decoration.github.info" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation/analysis/pull-request/#pr-decoration" | |||
> | |||
learn_more | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
<div | |||
className="big-spacer-bottom display-flex-space-between" | |||
> | |||
<h4 | |||
className="display-inline" | |||
> | |||
settings.pr_decoration.table.title | |||
</h4> | |||
<Button | |||
onClick={[MockFunction]} | |||
> | |||
settings.pr_decoration.table.create | |||
</Button> | |||
</div> | |||
<PRDecorationTable | |||
alm="github" | |||
definitions={Array []} | |||
onDelete={[MockFunction]} | |||
onEdit={[MockFunction]} | |||
/> | |||
<AlmPRDecorationFormModal | |||
alm="github" | |||
data={ | |||
Object { | |||
"appId": "123456", | |||
"key": "key", | |||
"privateKey": "asdf1234", | |||
"url": "http:alm.enterprise.com", | |||
} | |||
} | |||
onCancel={[MockFunction]} | |||
onSubmit={[MockFunction]} | |||
/> | |||
</Fragment> | |||
`; |
@@ -22,6 +22,18 @@ import { hasMessage, translate } from 'sonar-ui-common/helpers/l10n'; | |||
export const DEFAULT_CATEGORY = 'general'; | |||
export enum ALM_KEYS { | |||
BITBUCKET = 'bitbucket', | |||
GITHUB = 'github', | |||
AZURE_DEVOPS = 'azure_devops' | |||
} | |||
export const almName = { | |||
[ALM_KEYS.AZURE_DEVOPS]: 'Azure DevOps Server', | |||
[ALM_KEYS.BITBUCKET]: 'Bitbucket Server', | |||
[ALM_KEYS.GITHUB]: 'Github Enterprise' | |||
}; | |||
export type DefaultSpecializedInputProps = T.Omit<DefaultInputProps, 'setting'> & { | |||
isDefault: boolean; | |||
name: string; |
@@ -50,6 +50,18 @@ export function mockAlmOrganization(overrides: Partial<T.AlmOrganization> = {}): | |||
}; | |||
} | |||
export function mockGithubDefinition( | |||
overrides: Partial<T.GithubDefinition> = {} | |||
): T.GithubDefinition { | |||
return { | |||
key: 'key', | |||
url: 'http:alm.enterprise.com', | |||
appId: '123456', | |||
privateKey: 'asdf1234', | |||
...overrides | |||
}; | |||
} | |||
export function mockAnalysis(overrides: Partial<T.Analysis> = {}): T.Analysis { | |||
return { | |||
date: '2017-03-01T09:36:01+0100', |
@@ -920,6 +920,33 @@ settings.new_code_period.description2=This setting is the default for all projec | |||
settings.languages.select_a_language_placeholder=Select a language | |||
settings.pr_decoration.category=Pull Requests | |||
settings.pr_decoration.title=Pull Requests | |||
settings.pr_decoration.header=Pull Request decoration | |||
settings.pr_decoration.description=When Pull Request decoration is enabled, SonarQube publishes the status of the analysis directly in your ALM Pull requests. | |||
settings.pr_decoration.manage_instances=Manage instances | |||
settings.pr_decoration.github.info=You need to install a Github App with specific settings and permissions to enable Pull Request Decoration on your Organization or Repository. {link} | |||
settings.pr_decoration.table.title=Pull Request decoration configurations | |||
settings.pr_decoration.table.create=Create configuration | |||
settings.pr_decoration.table.column.name=Name | |||
settings.pr_decoration.table.column.github.url=Github instance URL | |||
settings.pr_decoration.table.column.app_id=App ID | |||
settings.pr_decoration.table.column.edit=Edit | |||
settings.pr_decoration.table.column.delete=Delete | |||
settings.pr_decoration.delete.header=Delete configuration | |||
settings.pr_decoration.delete.message=Are you sure you want to delete the {id} configuration? | |||
settings.pr_decoration.delete.info={0} projects will no longer get Pull Request decorations. | |||
settings.pr_decoration.delete.no_info=An unknown number of projects will no longer get Pull Request decorations. | |||
settings.pr_decoration.form.header.create=Create a Pull Request decoration configuration | |||
settings.pr_decoration.form.header.edit=Edit the Pull Request decoration configuration | |||
settings.pr_decoration.form.name=Configuration name | |||
settings.pr_decoration.form.name.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured GitHub App for a project. | |||
settings.pr_decoration.form.url.github=GitHub Enterprise URL | |||
settings.pr_decoration.form.app_id=GitHub App ID | |||
settings.pr_decoration.form.private_key=Private Key | |||
settings.pr_decoration.form.save=Save configuration | |||
settings.pr_decoration.form.cancel=Cancel | |||
property.category.general=General | |||
property.category.general.email=Email | |||
property.category.general.duplications=Duplications |