Browse Source

SONAR-12512 UI for general settings

tags/8.1.0.31237
Jeremy 4 years ago
parent
commit
487a0c97a1
32 changed files with 1956 additions and 2 deletions
  1. 71
    0
      server/sonar-web/src/main/js/api/__tests__/almSettings-test.ts
  2. 41
    0
      server/sonar-web/src/main/js/api/almSettings.ts
  3. 14
    0
      server/sonar-web/src/main/js/app/types.d.ts
  4. 15
    1
      server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx
  5. 2
    1
      server/sonar-web/src/main/js/apps/settings/components/AdditionalCategoryKeys.ts
  6. 77
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModal.tsx
  7. 147
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModalRenderer.tsx
  8. 62
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/DeleteModal.tsx
  9. 118
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/GithubTab.tsx
  10. 71
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTable.tsx
  11. 73
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTabs.tsx
  12. 80
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PullRequestDecoration.tsx
  13. 93
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/TabRenderer.tsx
  14. 88
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModal-test.tsx
  15. 45
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModalRenderer-test.tsx
  16. 33
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/DeleteModal-test.tsx
  17. 127
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/GithubTab-test.tsx
  18. 41
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTable-test.tsx
  19. 41
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTabs-test.tsx
  20. 66
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PullRequestDecoration-test.tsx
  21. 46
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/TabRenderer-test.tsx
  22. 20
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModal-test.tsx.snap
  23. 144
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModalRenderer-test.tsx.snap
  24. 60
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/DeleteModal-test.tsx.snap
  25. 14
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/GithubTab-test.tsx.snap
  26. 95
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTable-test.tsx.snap
  27. 52
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTabs-test.tsx.snap
  28. 15
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PullRequestDecoration-test.tsx.snap
  29. 154
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/TabRenderer-test.tsx.snap
  30. 12
    0
      server/sonar-web/src/main/js/apps/settings/utils.ts
  31. 12
    0
      server/sonar-web/src/main/js/helpers/testMocks.ts
  32. 27
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 71
- 0
server/sonar-web/src/main/js/api/__tests__/almSettings-test.ts View File

@@ -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);
});
});

+ 41
- 0
server/sonar-web/src/main/js/api/almSettings.ts View File

@@ -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);
}

+ 14
- 0
server/sonar-web/src/main/js/app/types.d.ts View File

@@ -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;

+ 15
- 1
server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx View File

@@ -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 />;
}

+ 2
- 1
server/sonar-web/src/main/js/apps/settings/components/AdditionalCategoryKeys.ts View File

@@ -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';

+ 77
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModal.tsx View File

@@ -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}
/>
);
}
}

+ 147
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModalRenderer.tsx View File

@@ -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>
);
}

+ 62
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/DeleteModal.tsx View File

@@ -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>
);
}

+ 118
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/GithubTab.tsx View File

@@ -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}
/>
);
}
}

+ 71
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTable.tsx View File

@@ -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>
</>
);
}

+ 73
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTabs.tsx View File

@@ -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>
</>
);
}

+ 80
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PullRequestDecoration.tsx View File

@@ -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}
/>
);
}
}

+ 93
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/TabRenderer.tsx View File

@@ -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}
/>
)}
</>
);
}

+ 88
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModal-test.tsx View File

@@ -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}
/>
);
}

+ 45
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModalRenderer-test.tsx View File

@@ -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}
/>
);
}

+ 33
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/DeleteModal-test.tsx View File

@@ -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} />
);
}

+ 127
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/GithubTab-test.tsx View File

@@ -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} />
);
}

+ 41
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTable-test.tsx View File

@@ -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}
/>
);
}

+ 41
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PRDecorationTabs-test.tsx View File

@@ -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}
/>
);
}

+ 66
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/PullRequestDecoration-test.tsx View File

@@ -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 />);
}

+ 46
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/TabRenderer-test.tsx View File

@@ -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}
/>
);
}

+ 20
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModal-test.tsx.snap View File

@@ -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=""
/>
`;

+ 144
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModalRenderer-test.tsx.snap View File

@@ -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>
`;

+ 60
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/DeleteModal-test.tsx.snap View File

@@ -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>
`;

+ 14
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/GithubTab-test.tsx.snap View File

@@ -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]}
/>
`;

+ 95
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTable-test.tsx.snap View File

@@ -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>
`;

+ 52
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTabs-test.tsx.snap View File

@@ -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>
`;

+ 15
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PullRequestDecoration-test.tsx.snap View File

@@ -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]}
/>
`;

+ 154
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/TabRenderer-test.tsx.snap View File

@@ -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>
`;

+ 12
- 0
server/sonar-web/src/main/js/apps/settings/utils.ts View File

@@ -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;

+ 12
- 0
server/sonar-web/src/main/js/helpers/testMocks.ts View File

@@ -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',

+ 27
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -920,6 +920,33 @@ settings.new_code_period.description2=This setting is the default for all projec

settings.languages.select_a_language_placeholder=Select a language

settings.pr_decoration.category=Pull Requests
settings.pr_decoration.title=Pull Requests
settings.pr_decoration.header=Pull Request decoration
settings.pr_decoration.description=When Pull Request decoration is enabled, SonarQube publishes the status of the analysis directly in your ALM Pull requests.
settings.pr_decoration.manage_instances=Manage instances
settings.pr_decoration.github.info=You need to install a Github App with specific settings and permissions to enable Pull Request Decoration on your Organization or Repository. {link}
settings.pr_decoration.table.title=Pull Request decoration configurations
settings.pr_decoration.table.create=Create configuration
settings.pr_decoration.table.column.name=Name
settings.pr_decoration.table.column.github.url=Github instance URL
settings.pr_decoration.table.column.app_id=App ID
settings.pr_decoration.table.column.edit=Edit
settings.pr_decoration.table.column.delete=Delete
settings.pr_decoration.delete.header=Delete configuration
settings.pr_decoration.delete.message=Are you sure you want to delete the {id} configuration?
settings.pr_decoration.delete.info={0} projects will no longer get Pull Request decorations.
settings.pr_decoration.delete.no_info=An unknown number of projects will no longer get Pull Request decorations.
settings.pr_decoration.form.header.create=Create a Pull Request decoration configuration
settings.pr_decoration.form.header.edit=Edit the Pull Request decoration configuration
settings.pr_decoration.form.name=Configuration name
settings.pr_decoration.form.name.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured GitHub App for a project.
settings.pr_decoration.form.url.github=GitHub Enterprise URL
settings.pr_decoration.form.app_id=GitHub App ID
settings.pr_decoration.form.private_key=Private Key
settings.pr_decoration.form.save=Save configuration
settings.pr_decoration.form.cancel=Cancel

property.category.general=General
property.category.general.email=Email
property.category.general.duplications=Duplications

Loading…
Cancel
Save