diff options
author | Jeremy <jeremy.davis@sonarsource.com> | 2019-10-16 17:15:54 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2019-11-06 10:04:26 +0100 |
commit | 487a0c97a1f2ab30cebf330238de3f3b7f3893ab (patch) | |
tree | 4b919bbe8f95067cd3abf0c215558f637105efa6 /server/sonar-web/src/main | |
parent | ab573e2a2597785da80ac461a1c0a3722e5dfdbc (diff) | |
download | sonarqube-487a0c97a1f2ab30cebf330238de3f3b7f3893ab.tar.gz sonarqube-487a0c97a1f2ab30cebf330238de3f3b7f3893ab.zip |
SONAR-12512 UI for general settings
Diffstat (limited to 'server/sonar-web/src/main')
31 files changed, 1929 insertions, 2 deletions
diff --git a/server/sonar-web/src/main/js/api/__tests__/almSettings-test.ts b/server/sonar-web/src/main/js/api/__tests__/almSettings-test.ts new file mode 100644 index 00000000000..d6eb4c8e014 --- /dev/null +++ b/server/sonar-web/src/main/js/api/__tests__/almSettings-test.ts @@ -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); + }); +}); diff --git a/server/sonar-web/src/main/js/api/almSettings.ts b/server/sonar-web/src/main/js/api/almSettings.ts new file mode 100644 index 00000000000..9e0e18b35be --- /dev/null +++ b/server/sonar-web/src/main/js/api/almSettings.ts @@ -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); +} diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts index a0b919a894e..760e38b982b 100644 --- a/server/sonar-web/src/main/js/app/types.d.ts +++ b/server/sonar-web/src/main/js/app/types.d.ts @@ -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; diff --git a/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx b/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx index 08cf7741c58..8902f88ace1 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx @@ -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 />; +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategoryKeys.ts b/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategoryKeys.ts index d343d0ee0ab..742ebb58c89 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategoryKeys.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategoryKeys.ts @@ -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'; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModal.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModal.tsx new file mode 100644 index 00000000000..781f3f068d7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModal.tsx @@ -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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModalRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModalRenderer.tsx new file mode 100644 index 00000000000..d110c3c327a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModalRenderer.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/DeleteModal.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/DeleteModal.tsx new file mode 100644 index 00000000000..4741fa0194e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/DeleteModal.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/GithubTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/GithubTab.tsx new file mode 100644 index 00000000000..f29f9e7229a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/GithubTab.tsx @@ -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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTable.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTable.tsx new file mode 100644 index 00000000000..fa55a74ba97 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTable.tsx @@ -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> + </> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTabs.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTabs.tsx new file mode 100644 index 00000000000..a6c9a98c468 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTabs.tsx @@ -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> + </> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PullRequestDecoration.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PullRequestDecoration.tsx new file mode 100644 index 00000000000..0e7e1228ad5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PullRequestDecoration.tsx @@ -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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/TabRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/TabRenderer.tsx new file mode 100644 index 00000000000..33ef1972669 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/TabRenderer.tsx @@ -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} + /> + )} + </> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModal-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModal-test.tsx new file mode 100644 index 00000000000..cf568dd4466 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModal-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModalRenderer-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModalRenderer-test.tsx new file mode 100644 index 00000000000..1b457d46c0d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModalRenderer-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/DeleteModal-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/DeleteModal-test.tsx new file mode 100644 index 00000000000..0f84b245812 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/DeleteModal-test.tsx @@ -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} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/GithubTab-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/GithubTab-test.tsx new file mode 100644 index 00000000000..c2c9b969be0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/GithubTab-test.tsx @@ -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} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTable-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTable-test.tsx new file mode 100644 index 00000000000..8fd9643c437 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTable-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTabs-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTabs-test.tsx new file mode 100644 index 00000000000..23c9b71351b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTabs-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PullRequestDecoration-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PullRequestDecoration-test.tsx new file mode 100644 index 00000000000..abcb76f2613 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PullRequestDecoration-test.tsx @@ -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 />); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/TabRenderer-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/TabRenderer-test.tsx new file mode 100644 index 00000000000..eca16766b67 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/TabRenderer-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModal-test.tsx.snap new file mode 100644 index 00000000000..3a6b897ff39 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModal-test.tsx.snap @@ -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="" +/> +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModalRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModalRenderer-test.tsx.snap new file mode 100644 index 00000000000..845d7e29527 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModalRenderer-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/DeleteModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/DeleteModal-test.tsx.snap new file mode 100644 index 00000000000..bb13dab20ed --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/DeleteModal-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/GithubTab-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/GithubTab-test.tsx.snap new file mode 100644 index 00000000000..30354a81600 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/GithubTab-test.tsx.snap @@ -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]} +/> +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTable-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTable-test.tsx.snap new file mode 100644 index 00000000000..ee16bf33ea7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTable-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTabs-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTabs-test.tsx.snap new file mode 100644 index 00000000000..ae1c38b986b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTabs-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PullRequestDecoration-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PullRequestDecoration-test.tsx.snap new file mode 100644 index 00000000000..c11cd3f8e94 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PullRequestDecoration-test.tsx.snap @@ -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]} +/> +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/TabRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/TabRenderer-test.tsx.snap new file mode 100644 index 00000000000..86357d441d9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/TabRenderer-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/settings/utils.ts b/server/sonar-web/src/main/js/apps/settings/utils.ts index 997b649e124..118cff7ac85 100644 --- a/server/sonar-web/src/main/js/apps/settings/utils.ts +++ b/server/sonar-web/src/main/js/apps/settings/utils.ts @@ -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; diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index e6b2119daf1..060282ce6f0 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -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', |