aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main
diff options
context:
space:
mode:
authorJeremy <jeremy.davis@sonarsource.com>2019-10-16 17:15:54 +0200
committersonartech <sonartech@sonarsource.com>2019-11-06 10:04:26 +0100
commit487a0c97a1f2ab30cebf330238de3f3b7f3893ab (patch)
tree4b919bbe8f95067cd3abf0c215558f637105efa6 /server/sonar-web/src/main
parentab573e2a2597785da80ac461a1c0a3722e5dfdbc (diff)
downloadsonarqube-487a0c97a1f2ab30cebf330238de3f3b7f3893ab.tar.gz
sonarqube-487a0c97a1f2ab30cebf330238de3f3b7f3893ab.zip
SONAR-12512 UI for general settings
Diffstat (limited to 'server/sonar-web/src/main')
-rw-r--r--server/sonar-web/src/main/js/api/__tests__/almSettings-test.ts71
-rw-r--r--server/sonar-web/src/main/js/api/almSettings.ts41
-rw-r--r--server/sonar-web/src/main/js/app/types.d.ts14
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx16
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/AdditionalCategoryKeys.ts3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModal.tsx77
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModalRenderer.tsx147
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/DeleteModal.tsx62
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/GithubTab.tsx118
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTable.tsx71
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTabs.tsx73
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PullRequestDecoration.tsx80
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/TabRenderer.tsx93
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModal-test.tsx88
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModalRenderer-test.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/DeleteModal-test.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/GithubTab-test.tsx127
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTable-test.tsx41
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTabs-test.tsx41
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PullRequestDecoration-test.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/TabRenderer-test.tsx46
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModal-test.tsx.snap20
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModalRenderer-test.tsx.snap144
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/DeleteModal-test.tsx.snap60
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/GithubTab-test.tsx.snap14
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTable-test.tsx.snap95
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTabs-test.tsx.snap52
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PullRequestDecoration-test.tsx.snap15
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/TabRenderer-test.tsx.snap154
-rw-r--r--server/sonar-web/src/main/js/apps/settings/utils.ts12
-rw-r--r--server/sonar-web/src/main/js/helpers/testMocks.ts12
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',