Browse Source

SONAR-12512 UI project-level form

tags/8.1.0.31237
Jeremy 4 years ago
parent
commit
a578ffdedd
25 changed files with 1286 additions and 137 deletions
  1. 0
    71
      server/sonar-web/src/main/js/api/__tests__/almSettings-test.ts
  2. 25
    5
      server/sonar-web/src/main/js/api/almSettings.ts
  3. 16
    1
      server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx
  4. 1
    0
      server/sonar-web/src/main/js/apps/settings/components/AdditionalCategoryKeys.ts
  5. 8
    8
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModal.tsx
  6. 4
    4
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/AlmPRDecorationFormModalRenderer.tsx
  7. 6
    5
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/GithubTab.tsx
  8. 4
    4
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTable.tsx
  9. 1
    4
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PRDecorationTabs.tsx
  10. 2
    2
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/PullRequestDecoration.tsx
  11. 6
    6
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/TabRenderer.tsx
  12. 2
    2
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/AlmPRDecorationFormModal-test.tsx
  13. 4
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/AlmPRDecorationFormModalRenderer-test.tsx.snap
  14. 0
    6
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTabs-test.tsx.snap
  15. 1
    1
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/TabRenderer-test.tsx.snap
  16. 182
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx
  17. 147
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx
  18. 174
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx
  19. 121
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx
  20. 21
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBinding-test.tsx.snap
  21. 493
    0
      server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap
  22. 2
    2
      server/sonar-web/src/main/js/helpers/testMocks.ts
  23. 56
    0
      server/sonar-web/src/main/js/types/alm-settings.d.ts
  24. 0
    14
      server/sonar-web/src/main/js/types/types.d.ts
  25. 10
    2
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

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

@@ -1,71 +0,0 @@
/*
* 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);
});
});

+ 25
- 5
server/sonar-web/src/main/js/api/almSettings.ts View File

@@ -20,15 +20,21 @@
import { getJSON, post } from 'sonar-ui-common/helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';

export function getAlmDefinitions(): Promise<T.AlmSettingsDefinitions> {
export function getAlmDefinitions(): Promise<T.AlmSettingsBindingDefinitions> {
return getJSON('/api/alm_settings/list_definitions').catch(throwGlobalError);
}

export function createGithubConfiguration(data: T.GithubDefinition) {
export function getAlmSettings(project: string): Promise<T.AlmSettingsInstance[]> {
return getJSON('/api/alm_settings/list', { project })
.then(({ almSettings }) => almSettings)
.catch(throwGlobalError);
}

export function createGithubConfiguration(data: T.GithubBindingDefinition) {
return post('/api/alm_settings/create_github', data).catch(throwGlobalError);
}

export function updateGithubConfiguration(data: T.GithubDefinition & { newKey: string }) {
export function updateGithubConfiguration(data: T.GithubBindingDefinition & { newKey: string }) {
return post('/api/alm_settings/update_github', data).catch(throwGlobalError);
}

@@ -36,6 +42,20 @@ 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);
export function countBindedProjects(almSetting: string) {
return getJSON('/api/alm_settings/count_binding', { almSetting })
.then(({ projects }) => projects)
.catch(throwGlobalError);
}

export function getProjectAlmBinding(project: string): Promise<T.ProjectAlmBinding> {
return getJSON('/api/alm_settings/get_github_binding', { project });
}

export function deleteProjectAlmBinding(project: string): Promise<void> {
return post('/api/alm_settings/delete_binding', { project }).catch(throwGlobalError);
}

export function setProjectAlmBinding(data: T.GithubProjectAlmBinding) {
return post('/api/alm_settings/set_github_binding', data).catch(throwGlobalError);
}

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

@@ -24,12 +24,14 @@ import {
ANALYSIS_SCOPE_CATEGORY,
LANGUAGES_CATEGORY,
NEW_CODE_PERIOD_CATEGORY,
PULL_REQUEST_DECORATION_BINDING_CATEGORY,
PULL_REQUEST_DECORATION_CATEGORY
} from './AdditionalCategoryKeys';
import { AnalysisScope } from './AnalysisScope';
import Languages from './Languages';
import NewCodePeriod from './NewCodePeriod';
import PullRequestDecoration from './pullRequestDecoration/PullRequestDecoration';
import PullRequestDecorationBinding from './pullRequestDecorationBinding/PRDecorationBinding';

export interface AdditionalCategoryComponentProps {
parentComponent: T.Component | undefined;
@@ -39,7 +41,7 @@ export interface AdditionalCategoryComponentProps {
export interface AdditionalCategory {
key: string;
name: string;
renderComponent: (props: AdditionalCategoryComponentProps) => JSX.Element;
renderComponent: (props: AdditionalCategoryComponentProps) => React.ReactNode;
availableGlobally: boolean;
availableForProject: boolean;
displayTab: boolean;
@@ -77,6 +79,14 @@ export const ADDITIONAL_CATEGORIES: AdditionalCategory[] = [
availableGlobally: true,
availableForProject: false,
displayTab: true
},
{
key: PULL_REQUEST_DECORATION_BINDING_CATEGORY,
name: translate('settings.pr_decoration.binding.category'),
renderComponent: getPullRequestDecorationBindingComponent,
availableGlobally: false,
availableForProject: true,
displayTab: true
}
];

@@ -95,3 +105,8 @@ function getAnalysisScopeComponent(props: AdditionalCategoryComponentProps) {
function getPullRequestDecorationComponent() {
return <PullRequestDecoration />;
}

function getPullRequestDecorationBindingComponent(props: AdditionalCategoryComponentProps) {
const { parentComponent } = props;
return parentComponent && <PullRequestDecorationBinding component={parentComponent} />;
}

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

@@ -22,3 +22,4 @@ export const ANALYSIS_SCOPE_CATEGORY = 'exclusions';
export const LANGUAGES_CATEGORY = 'languages';
export const NEW_CODE_PERIOD_CATEGORY = 'new_code_period';
export const PULL_REQUEST_DECORATION_CATEGORY = 'pull_request_decoration';
export const PULL_REQUEST_DECORATION_BINDING_CATEGORY = 'pull_request_decoration_binding';

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

@@ -22,23 +22,23 @@ import AlmPRDecorationFormModalRenderer from './AlmPRDecorationFormModalRenderer

interface Props {
alm: string;
data: T.GithubDefinition;
bindingDefinition: T.GithubBindingDefinition;
onCancel: () => void;
onSubmit: (data: T.GithubDefinition, originalKey: string) => void;
onSubmit: (bindingDefinition: T.GithubBindingDefinition, originalKey: string) => void;
}

interface State {
formData: T.GithubDefinition;
formData: T.GithubBindingDefinition;
}

export default class AlmPRDecorationFormModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);

this.state = { formData: props.data };
this.state = { formData: props.bindingDefinition };
}

handleFieldChange = (fieldId: keyof T.GithubDefinition, value: string) => {
handleFieldChange = (fieldId: keyof T.GithubBindingDefinition, value: string) => {
this.setState(({ formData }) => ({
formData: {
...formData,
@@ -48,7 +48,7 @@ export default class AlmPRDecorationFormModal extends React.PureComponent<Props,
};

handleFormSubmit = () => {
this.props.onSubmit(this.state.formData, this.props.data.key);
this.props.onSubmit(this.state.formData, this.props.bindingDefinition.key);
};

canSubmit = () => {
@@ -59,7 +59,7 @@ export default class AlmPRDecorationFormModal extends React.PureComponent<Props,
};

render() {
const { alm, data } = this.props;
const { alm, bindingDefinition } = this.props;
const { formData } = this.state;

return (
@@ -70,7 +70,7 @@ export default class AlmPRDecorationFormModal extends React.PureComponent<Props,
onCancel={this.props.onCancel}
onFieldChange={this.handleFieldChange}
onSubmit={this.handleFormSubmit}
originalKey={data.key}
originalKey={bindingDefinition.key}
/>
);
}

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

@@ -28,7 +28,7 @@ import { ALM_KEYS } from '../../utils';
export interface AlmPRDecorationFormModalProps {
alm: string;
canSubmit: () => boolean;
formData: T.GithubDefinition;
formData: T.GithubBindingDefinition;
onCancel: () => void;
onSubmit: () => void;
onFieldChange: (id: string, value: string) => void;
@@ -37,18 +37,18 @@ export interface AlmPRDecorationFormModalProps {

function renderField(params: {
autoFocus?: boolean;
formData: T.GithubDefinition;
formData: T.GithubBindingDefinition;
help: boolean;
id: string;
isTextArea: boolean;
maxLength: number;
onFieldChange: (id: string, value: string) => void;
propKey: keyof T.GithubDefinition;
propKey: keyof T.GithubBindingDefinition;
}) {
const { autoFocus, formData, help, id, isTextArea, maxLength, onFieldChange, propKey } = params;
return (
<div className="modal-field">
<label htmlFor={id}>
<label className="display-flex-center" htmlFor={id}>
{translate('settings.pr_decoration.form', id)}
<em className="mandatory spacer-right">*</em>
{help && <HelpTooltip overlay={translate('settings.pr_decoration.form', id, 'help')} />}

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

@@ -28,12 +28,12 @@ import { ALM_KEYS } from '../../utils';
import TabRenderer from './TabRenderer';

interface Props {
definitions: T.GithubDefinition[];
definitions: T.GithubBindingDefinition[];
onUpdateDefinitions: () => void;
}

interface State {
definitionInEdition?: T.GithubDefinition;
definitionInEdition?: T.GithubBindingDefinition;
definitionKeyForDeletion?: string;
projectCount?: number;
}
@@ -72,8 +72,9 @@ export default class GithubTab extends React.PureComponent<Props, State> {
this.setState({ definitionInEdition: { key: '', appId: '', url: '', privateKey: '' } });
};

handleDelete = (config: T.GithubDefinition) => {
handleDelete = (config: T.GithubBindingDefinition) => {
this.setState({ definitionKeyForDeletion: config.key });

return countBindedProjects(config.key).then(projectCount => {
if (this.mounted) {
this.setState({ projectCount });
@@ -81,11 +82,11 @@ export default class GithubTab extends React.PureComponent<Props, State> {
});
};

handleEdit = (config: T.GithubDefinition) => {
handleEdit = (config: T.GithubBindingDefinition) => {
this.setState({ definitionInEdition: config });
};

handleSubmit = (config: T.GithubDefinition, originalKey: string) => {
handleSubmit = (config: T.GithubBindingDefinition, originalKey: string) => {
const call = originalKey
? updateGithubConfiguration({ newKey: config.key, ...config, key: originalKey })
: createGithubConfiguration(config);

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

@@ -25,14 +25,14 @@ 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;
definitions: T.GithubBindingDefinition[];
onDelete: (config: T.GithubBindingDefinition) => void;
onEdit: (config: T.GithubBindingDefinition) => void;
}

export default function PRDecorationTable(props: PRDecorationTableProps) {
const { definitions, alm } = props;
const { alm, definitions } = props;

return (
<>

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

@@ -25,8 +25,8 @@ import { almName, ALM_KEYS } from '../../utils';
import GithubTab from './GithubTab';

export interface PRDecorationTabsProps {
definitions: T.AlmSettingsDefinitions;
currentAlm: ALM_KEYS;
definitions: T.AlmSettingsBindingDefinitions;
loading: boolean;
onSelectAlm: (alm: ALM_KEYS) => void;
onUpdateDefinitions: () => void;
@@ -44,9 +44,6 @@ export default function PRDecorationTabs(props: PRDecorationTabsProps) {
<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')}

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

@@ -23,18 +23,18 @@ import { ALM_KEYS } from '../../utils';
import PRDecorationTabs from './PRDecorationTabs';

interface State {
definitions: T.AlmSettingsDefinitions;
currentAlm: ALM_KEYS;
definitions: T.AlmSettingsBindingDefinitions;
loading: boolean;
}

export default class PullRequestDecoration extends React.PureComponent<{}, State> {
mounted = false;
state: State = {
currentAlm: ALM_KEYS.GITHUB,
definitions: {
[ALM_KEYS.GITHUB]: []
},
currentAlm: ALM_KEYS.GITHUB,
loading: true
};


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

@@ -30,15 +30,15 @@ import PRDecorationTable from './PRDecorationTable';

export interface TabRendererProps {
alm: ALM_KEYS;
definitionInEdition?: T.GithubDefinition;
definitionInEdition?: T.GithubBindingDefinition;
definitionKeyForDeletion?: string;
definitions: T.GithubDefinition[];
definitions: T.GithubBindingDefinition[];
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;
onDelete: (config: T.GithubBindingDefinition) => void;
onEdit: (config: T.GithubBindingDefinition) => void;
onSubmit: (config: T.GithubBindingDefinition, originalKey: string) => void;
projectCount?: number;
}

@@ -83,7 +83,7 @@ export default function TabRenderer(props: TabRendererProps) {
{definitionInEdition && (
<AlmPRDecorationFormModal
alm={ALM_KEYS.GITHUB}
data={definitionInEdition}
bindingDefinition={definitionInEdition}
onCancel={props.onCancel}
onSubmit={props.onSubmit}
/>

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

@@ -49,7 +49,7 @@ it('should handle form submit', async () => {
const onSubmit = jest.fn();
const wrapper = shallowRender({
onSubmit,
data: { key: 'originalKey', appId: '', privateKey: '', url: '' }
bindingDefinition: { key: 'originalKey', appId: '', privateKey: '', url: '' }
});
const formData = {
key: 'github instance',
@@ -79,7 +79,7 @@ function shallowRender(props: Partial<AlmPRDecorationFormModal['props']> = {}) {
return shallow<AlmPRDecorationFormModal>(
<AlmPRDecorationFormModal
alm={ALM_KEYS.GITHUB}
data={{ appId: '', key: '', privateKey: '', url: '' }}
bindingDefinition={{ appId: '', key: '', privateKey: '', url: '' }}
onCancel={jest.fn()}
onSubmit={jest.fn()}
{...props}

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

@@ -24,6 +24,7 @@ exports[`should render correctly 1`] = `
className="modal-field"
>
<label
className="display-flex-center"
htmlFor="name"
>
settings.pr_decoration.form.name
@@ -52,6 +53,7 @@ exports[`should render correctly 1`] = `
className="modal-field"
>
<label
className="display-flex-center"
htmlFor="url.github"
>
settings.pr_decoration.form.url.github
@@ -76,6 +78,7 @@ exports[`should render correctly 1`] = `
className="modal-field"
>
<label
className="display-flex-center"
htmlFor="app_id"
>
settings.pr_decoration.form.app_id
@@ -100,6 +103,7 @@ exports[`should render correctly 1`] = `
className="modal-field"
>
<label
className="display-flex-center"
htmlFor="private_key"
>
settings.pr_decoration.form.private_key

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

@@ -17,12 +17,6 @@ exports[`should render correctly 2`] = `
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"
>

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

@@ -139,7 +139,7 @@ exports[`should render correctly 3`] = `
/>
<AlmPRDecorationFormModal
alm="github"
data={
bindingDefinition={
Object {
"appId": "123456",
"key": "key",

+ 182
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx View File

@@ -0,0 +1,182 @@
/*
* 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 {
deleteProjectAlmBinding,
getAlmSettings,
getProjectAlmBinding,
setProjectAlmBinding
} from '../../../../api/almSettings';
import throwGlobalError from '../../../../app/utils/throwGlobalError';
import PRDecorationBindingRenderer from './PRDecorationBindingRenderer';

interface Props {
component: T.Component;
}

interface State {
formData: T.GithubBinding;
hasBinding: boolean;
instances: T.AlmSettingsInstance[];
isValid: boolean;
loading: boolean;
saving: boolean;
success: boolean;
}

export default class PRDecorationBinding extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
formData: {
key: '',
repository: ''
},
hasBinding: false,
instances: [],
isValid: false,
loading: true,
saving: false,
success: false
};

componentDidMount() {
this.mounted = true;
this.fetchDefinitions();
}

componentWillUnmount() {
this.mounted = false;
}

fetchDefinitions = () => {
const project = this.props.component.key;
return Promise.all([getAlmSettings(project), this.getProjectBinding(project)])
.then(([instances, data]) => {
if (this.mounted) {
this.setState(({ formData }) => ({
formData: data || formData,
hasBinding: Boolean(data),
instances,
isValid: this.validateForm(),
loading: false
}));

if (!data && instances.length === 1) {
this.handleFieldChange('key', instances[0].key);
}
}
})
.catch(() => {
if (this.mounted) {
this.setState({ loading: false });
}
});
};

getProjectBinding(project: string) {
return getProjectAlmBinding(project).catch((response: Response) => {
if (response && response.status === 404) {
return Promise.resolve(undefined);
}
return throwGlobalError(response);
});
}

catchError = () => {
if (this.mounted) {
this.setState({ saving: false });
}
};

handleReset = () => {
const { component } = this.props;
this.setState({ saving: true });
deleteProjectAlmBinding(component.key)
.then(() => {
if (this.mounted) {
this.setState({
formData: {
key: '',
repository: ''
},
hasBinding: false,
saving: false,
success: true
});
}
})
.catch(this.catchError);
};

handleSubmit = () => {
this.setState({ saving: true });
const {
formData: { key, repository }
} = this.state;

if (key && repository) {
setProjectAlmBinding({
almSetting: key,
project: this.props.component.key,
repository
})
.then(() => {
if (this.mounted) {
this.setState({
hasBinding: true,
saving: false,
success: true
});
}
})
.catch(this.catchError);
}
};

handleFieldChange = (id: keyof T.GithubBinding, value: string) => {
this.setState(({ formData: formdata }) => ({
formData: {
...formdata,
[id]: value
},
isValid: this.validateForm(),
success: false
}));
};

validateForm = () => {
const { formData } = this.state;
return Object.values(formData).reduce(
(result: boolean, value) => result && Boolean(value),
true
);
};

render() {
return (
<PRDecorationBindingRenderer
onFieldChange={this.handleFieldChange}
onReset={this.handleReset}
onSubmit={this.handleSubmit}
{...this.state}
/>
);
}
}

+ 147
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.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 { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Button, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Select from 'sonar-ui-common/components/controls/Select';
import AlertSuccessIcon from 'sonar-ui-common/components/icons/AlertSuccessIcon';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';

export interface PRDecorationBindingRendererProps {
formData: T.GithubBinding;
hasBinding: boolean;
instances: T.AlmSettingsInstance[];
isValid: boolean;
loading: boolean;
onFieldChange: (id: keyof T.GithubBinding, value: string) => void;
onReset: () => void;
onSubmit: () => void;
saving: boolean;
success: boolean;
}

export default function PRDecorationBindingRenderer(props: PRDecorationBindingRendererProps) {
const {
formData: { repository, key },
hasBinding,
instances,
isValid,
loading,
saving,
success
} = props;

if (loading) {
return <DeferredSpinner />;
}

if (instances.length < 1) {
return (
<div>
<Alert className="spacer-top huge-spacer-bottom" variant="info">
<FormattedMessage
defaultMessage={translate('settings.pr_decoration.binding.no_bindings')}
id="settings.pr_decoration.binding.no_bindings"
values={{
link: (
<Link to="/documentation/analysis/pull-request/#pr-decoration">
{translate('learn_more')}
</Link>
)
}}
/>
</Alert>
</div>
);
}

return (
<div>
<header className="page-header">
<h1 className="page-title">{translate('settings.pr_decoration.binding.title')}</h1>
</header>

<div className="markdown small spacer-top big-spacer-bottom">
{translate('settings.pr_decoration.binding.description')}
</div>

<form
onSubmit={(event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
props.onSubmit();
}}>
<div className="form-field">
<label htmlFor="name">
{translate('settings.pr_decoration.binding.form.name')}
<em className="mandatory spacer-right">*</em>
</label>
<Select
className="abs-width-400"
clearable={false}
id="name"
onChange={({ value }: { value: string }) => props.onFieldChange('key', value)}
options={instances.map(v => ({ value: v.key, label: `${v.key} — ${v.url}` }))}
searchable={false}
value={key}
/>
</div>

{key && (
<div className="form-field">
<label htmlFor="repository">
{translate('settings.pr_decoration.binding.form.repository')}
<em className="mandatory spacer-right">*</em>
</label>
<input
className="input-super-large"
id="repository"
maxLength={256}
name="repository"
onChange={e => props.onFieldChange('repository', e.currentTarget.value)}
type="text"
value={repository}
/>
</div>
)}

<div className="display-flex-center">
<DeferredSpinner className="spacer-right" loading={saving} />
<SubmitButton className="spacer-right" disabled={saving || !isValid}>
{translate('save')}
</SubmitButton>
{hasBinding && (
<Button className="spacer-right" onClick={props.onReset}>
{translate('reset_verb')}
</Button>
)}
{!saving && success && (
<span className="text-success">
<AlertSuccessIcon className="spacer-right" />
{translate('settings.state.saved')}
</span>
)}
</div>
</form>
</div>
);
}

+ 174
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx View File

@@ -0,0 +1,174 @@
/*
* 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 {
deleteProjectAlmBinding,
getAlmSettings,
getProjectAlmBinding,
setProjectAlmBinding
} from '../../../../../api/almSettings';
import { mockComponent } from '../../../../../helpers/testMocks';
import PRDecorationBinding from '../PRDecorationBinding';

jest.mock('../../../../../api/almSettings', () => ({
getAlmSettings: jest.fn().mockResolvedValue([]),
getProjectAlmBinding: jest.fn().mockResolvedValue(undefined),
setProjectAlmBinding: jest.fn().mockResolvedValue(undefined),
deleteProjectAlmBinding: jest.fn().mockResolvedValue(undefined)
}));

const PROJECT_KEY = 'project-key';

beforeEach(() => {
jest.clearAllMocks();
});

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should fill selects and fill formdata', async () => {
const url = 'github.com';
const instances = [{ key: 'instance1', url, alm: 'github' }];
const formdata = {
key: 'instance1',
repository: 'account/repo'
};
(getAlmSettings as jest.Mock).mockResolvedValueOnce(instances);
(getProjectAlmBinding as jest.Mock).mockResolvedValueOnce(formdata);

const wrapper = shallowRender();
await waitAndUpdate(wrapper);

expect(wrapper.state().hasBinding).toBe(true);
expect(wrapper.state().loading).toBe(false);
expect(wrapper.state().formData).toEqual(formdata);
});

it('should preselect url and key if only 1 item', async () => {
const instances = [{ key: 'instance1', url: 'github.enterprise.com', alm: 'github' }];
(getAlmSettings as jest.Mock).mockResolvedValueOnce(instances);
(getProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status: 404 });

const wrapper = shallowRender();
await waitAndUpdate(wrapper);

expect(wrapper.state().formData).toEqual({
key: instances[0].key,
repository: ''
});
});

const formData = {
key: 'whatever',
repository: 'something/else'
};

it('should handle reset', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.setState({ formData });

wrapper.instance().handleReset();
await waitAndUpdate(wrapper);

expect(deleteProjectAlmBinding).toBeCalledWith(PROJECT_KEY);
expect(wrapper.state().formData).toEqual({ key: '', repository: '' });
expect(wrapper.state().hasBinding).toBe(false);
});

it('should handle submit', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.setState({ formData });

wrapper.instance().handleSubmit();
await waitAndUpdate(wrapper);

expect(setProjectAlmBinding).toBeCalledWith({
almSetting: formData.key,
project: PROJECT_KEY,
repository: formData.repository
});
expect(wrapper.state().hasBinding).toBe(true);
expect(wrapper.state().success).toBe(true);
});

it('should handle failures gracefully', async () => {
(getProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status: 500 });
(setProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status: 500 });
(deleteProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status: 500 });

const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.setState({ formData });

wrapper.instance().handleSubmit();
await waitAndUpdate(wrapper);
wrapper.instance().handleReset();
});

it('should handle field changes', async () => {
const url = 'git.enterprise.com';
const repository = 'my/repo';
const instances = [
{ key: 'instance1', url, alm: 'github' },
{ key: 'instance2', url, alm: 'github' },
{ key: 'instance3', url: 'otherurl', alm: 'github' }
];
(getAlmSettings as jest.Mock).mockResolvedValueOnce(instances);
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

wrapper.instance().handleFieldChange('key', 'instance2');
await waitAndUpdate(wrapper);
expect(wrapper.state().formData).toEqual({
key: 'instance2',
repository: ''
});

wrapper.instance().handleFieldChange('repository', repository);
await waitAndUpdate(wrapper);
expect(wrapper.state().formData).toEqual({
key: 'instance2',
repository
});
});

it('should validate form', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

expect(wrapper.instance().validateForm()).toBe(false);

wrapper.setState({ formData: { key: '', repository: 'c' } });
expect(wrapper.instance().validateForm()).toBe(false);

wrapper.setState({ formData: { key: 'a', repository: 'c' } });
expect(wrapper.instance().validateForm()).toBe(true);
});

function shallowRender(props: Partial<PRDecorationBinding['props']> = {}) {
return shallow<PRDecorationBinding>(
<PRDecorationBinding component={mockComponent({ key: PROJECT_KEY })} {...props} />
);
}

+ 121
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx View File

@@ -0,0 +1,121 @@
/*
* 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 PRDecorationBindingRenderer, {
PRDecorationBindingRendererProps
} from '../PRDecorationBindingRenderer';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ loading: false })).toMatchSnapshot();
});

it('should render single instance correctly', () => {
const singleInstance = {
key: 'single',
url: 'http://single.url',
alm: 'github'
};
expect(
shallowRender({
loading: false,
instances: [singleInstance]
})
).toMatchSnapshot();
});

it('should render multiple instances correctly', () => {
const urls = ['http://github.enterprise.com', 'http://github2.enterprise.com'];
const instances = [
{
alm: 'github',
key: 'i1',
url: urls[0]
},
{
alm: 'github',
key: 'i2',
url: urls[0]
},
{
alm: 'github',
key: 'i3',
url: urls[1]
}
];

//unfilled
expect(
shallowRender({
instances,
loading: false
})
).toMatchSnapshot();

// filled
expect(
shallowRender({
formData: {
key: 'Github - main instance',
repository: 'account/repo'
},
hasBinding: true,
instances,
loading: false
})
).toMatchSnapshot();
});

it('should display action state correctly', () => {
const urls = ['http://url.com'];
const instances = [{ key: 'key', url: urls[0], alm: 'github' }];

expect(shallowRender({ instances, loading: false, saving: true })).toMatchSnapshot();
expect(shallowRender({ instances, loading: false, success: true })).toMatchSnapshot();
expect(
shallowRender({
instances,
isValid: true,
loading: false
})
).toMatchSnapshot();
});

function shallowRender(props: Partial<PRDecorationBindingRendererProps> = {}) {
return shallow(
<PRDecorationBindingRenderer
formData={{
key: '',
repository: ''
}}
hasBinding={false}
instances={[]}
isValid={false}
loading={true}
onFieldChange={jest.fn()}
onReset={jest.fn()}
onSubmit={jest.fn()}
saving={false}
success={false}
{...props}
/>
);
}

+ 21
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBinding-test.tsx.snap View File

@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<PRDecorationBindingRenderer
formData={
Object {
"key": "",
"repository": "",
}
}
hasBinding={false}
instances={Array []}
isValid={false}
loading={true}
onFieldChange={[Function]}
onReset={[Function]}
onSubmit={[Function]}
saving={false}
success={false}
/>
`;

+ 493
- 0
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap View File

@@ -0,0 +1,493 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display action state correctly 1`] = `
<div>
<header
className="page-header"
>
<h1
className="page-title"
>
settings.pr_decoration.binding.title
</h1>
</header>
<div
className="markdown small spacer-top big-spacer-bottom"
>
settings.pr_decoration.binding.description
</div>
<form
onSubmit={[Function]}
>
<div
className="form-field"
>
<label
htmlFor="name"
>
settings.pr_decoration.binding.form.name
<em
className="mandatory spacer-right"
>
*
</em>
</label>
<Select
className="abs-width-400"
clearable={false}
id="name"
onChange={[Function]}
options={
Array [
Object {
"label": "key — http://url.com",
"value": "key",
},
]
}
searchable={false}
value=""
/>
</div>
<div
className="display-flex-center"
>
<DeferredSpinner
className="spacer-right"
loading={true}
timeout={100}
/>
<SubmitButton
className="spacer-right"
disabled={true}
>
save
</SubmitButton>
</div>
</form>
</div>
`;

exports[`should display action state correctly 2`] = `
<div>
<header
className="page-header"
>
<h1
className="page-title"
>
settings.pr_decoration.binding.title
</h1>
</header>
<div
className="markdown small spacer-top big-spacer-bottom"
>
settings.pr_decoration.binding.description
</div>
<form
onSubmit={[Function]}
>
<div
className="form-field"
>
<label
htmlFor="name"
>
settings.pr_decoration.binding.form.name
<em
className="mandatory spacer-right"
>
*
</em>
</label>
<Select
className="abs-width-400"
clearable={false}
id="name"
onChange={[Function]}
options={
Array [
Object {
"label": "key — http://url.com",
"value": "key",
},
]
}
searchable={false}
value=""
/>
</div>
<div
className="display-flex-center"
>
<DeferredSpinner
className="spacer-right"
loading={false}
timeout={100}
/>
<SubmitButton
className="spacer-right"
disabled={true}
>
save
</SubmitButton>
<span
className="text-success"
>
<AlertSuccessIcon
className="spacer-right"
/>
settings.state.saved
</span>
</div>
</form>
</div>
`;

exports[`should display action state correctly 3`] = `
<div>
<header
className="page-header"
>
<h1
className="page-title"
>
settings.pr_decoration.binding.title
</h1>
</header>
<div
className="markdown small spacer-top big-spacer-bottom"
>
settings.pr_decoration.binding.description
</div>
<form
onSubmit={[Function]}
>
<div
className="form-field"
>
<label
htmlFor="name"
>
settings.pr_decoration.binding.form.name
<em
className="mandatory spacer-right"
>
*
</em>
</label>
<Select
className="abs-width-400"
clearable={false}
id="name"
onChange={[Function]}
options={
Array [
Object {
"label": "key — http://url.com",
"value": "key",
},
]
}
searchable={false}
value=""
/>
</div>
<div
className="display-flex-center"
>
<DeferredSpinner
className="spacer-right"
loading={false}
timeout={100}
/>
<SubmitButton
className="spacer-right"
disabled={false}
>
save
</SubmitButton>
</div>
</form>
</div>
`;

exports[`should render correctly 1`] = `
<DeferredSpinner
timeout={100}
/>
`;

exports[`should render correctly 2`] = `
<div>
<Alert
className="spacer-top huge-spacer-bottom"
variant="info"
>
<FormattedMessage
defaultMessage="settings.pr_decoration.binding.no_bindings"
id="settings.pr_decoration.binding.no_bindings"
values={
Object {
"link": <Link
onlyActiveOnIndex={false}
style={Object {}}
to="/documentation/analysis/pull-request/#pr-decoration"
>
learn_more
</Link>,
}
}
/>
</Alert>
</div>
`;

exports[`should render multiple instances correctly 1`] = `
<div>
<header
className="page-header"
>
<h1
className="page-title"
>
settings.pr_decoration.binding.title
</h1>
</header>
<div
className="markdown small spacer-top big-spacer-bottom"
>
settings.pr_decoration.binding.description
</div>
<form
onSubmit={[Function]}
>
<div
className="form-field"
>
<label
htmlFor="name"
>
settings.pr_decoration.binding.form.name
<em
className="mandatory spacer-right"
>
*
</em>
</label>
<Select
className="abs-width-400"
clearable={false}
id="name"
onChange={[Function]}
options={
Array [
Object {
"label": "i1 — http://github.enterprise.com",
"value": "i1",
},
Object {
"label": "i2 — http://github.enterprise.com",
"value": "i2",
},
Object {
"label": "i3 — http://github2.enterprise.com",
"value": "i3",
},
]
}
searchable={false}
value=""
/>
</div>
<div
className="display-flex-center"
>
<DeferredSpinner
className="spacer-right"
loading={false}
timeout={100}
/>
<SubmitButton
className="spacer-right"
disabled={true}
>
save
</SubmitButton>
</div>
</form>
</div>
`;

exports[`should render multiple instances correctly 2`] = `
<div>
<header
className="page-header"
>
<h1
className="page-title"
>
settings.pr_decoration.binding.title
</h1>
</header>
<div
className="markdown small spacer-top big-spacer-bottom"
>
settings.pr_decoration.binding.description
</div>
<form
onSubmit={[Function]}
>
<div
className="form-field"
>
<label
htmlFor="name"
>
settings.pr_decoration.binding.form.name
<em
className="mandatory spacer-right"
>
*
</em>
</label>
<Select
className="abs-width-400"
clearable={false}
id="name"
onChange={[Function]}
options={
Array [
Object {
"label": "i1 — http://github.enterprise.com",
"value": "i1",
},
Object {
"label": "i2 — http://github.enterprise.com",
"value": "i2",
},
Object {
"label": "i3 — http://github2.enterprise.com",
"value": "i3",
},
]
}
searchable={false}
value="Github - main instance"
/>
</div>
<div
className="form-field"
>
<label
htmlFor="repository"
>
settings.pr_decoration.binding.form.repository
<em
className="mandatory spacer-right"
>
*
</em>
</label>
<input
className="input-super-large"
id="repository"
maxLength={256}
name="repository"
onChange={[Function]}
type="text"
value="account/repo"
/>
</div>
<div
className="display-flex-center"
>
<DeferredSpinner
className="spacer-right"
loading={false}
timeout={100}
/>
<SubmitButton
className="spacer-right"
disabled={true}
>
save
</SubmitButton>
<Button
className="spacer-right"
onClick={[MockFunction]}
>
reset_verb
</Button>
</div>
</form>
</div>
`;

exports[`should render single instance correctly 1`] = `
<div>
<header
className="page-header"
>
<h1
className="page-title"
>
settings.pr_decoration.binding.title
</h1>
</header>
<div
className="markdown small spacer-top big-spacer-bottom"
>
settings.pr_decoration.binding.description
</div>
<form
onSubmit={[Function]}
>
<div
className="form-field"
>
<label
htmlFor="name"
>
settings.pr_decoration.binding.form.name
<em
className="mandatory spacer-right"
>
*
</em>
</label>
<Select
className="abs-width-400"
clearable={false}
id="name"
onChange={[Function]}
options={
Array [
Object {
"label": "single — http://single.url",
"value": "single",
},
]
}
searchable={false}
value=""
/>
</div>
<div
className="display-flex-center"
>
<DeferredSpinner
className="spacer-right"
loading={false}
timeout={100}
/>
<SubmitButton
className="spacer-right"
disabled={true}
>
save
</SubmitButton>
</div>
</form>
</div>
`;

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

@@ -51,8 +51,8 @@ export function mockAlmOrganization(overrides: Partial<T.AlmOrganization> = {}):
}

export function mockGithubDefinition(
overrides: Partial<T.GithubDefinition> = {}
): T.GithubDefinition {
overrides: Partial<T.GithubBindingDefinition> = {}
): T.GithubBindingDefinition {
return {
key: 'key',
url: 'http:alm.enterprise.com',

+ 56
- 0
server/sonar-web/src/main/js/types/alm-settings.d.ts View File

@@ -0,0 +1,56 @@
/*
* 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.
*/
declare namespace T {
export interface AlmSettingsBinding {
key: string;
url: string;
}

export interface AlmSettingsInstance extends AlmSettingsBinding {
alm: string;
}

export interface AlmSettingsBindingDefinitions {
github: GithubBindingDefinition[];
}

export interface GithubBindingDefinition extends AlmSettingsBinding {
appId: string;
privateKey: string;
}

export interface ProjectAlmBinding {
key: string;
alm: string;
url: string;
repository: string;
}

export interface GithubProjectAlmBinding {
almSetting: string;
project: string;
repository: string;
}

export interface GithubBinding {
key: string;
repository?: string;
}
}

server/sonar-web/src/main/js/app/types.d.ts → server/sonar-web/src/main/js/types/types.d.ts View File

@@ -30,20 +30,6 @@ 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;

+ 10
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -921,8 +921,7 @@ 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.title=Pull Requests 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}
@@ -947,6 +946,15 @@ settings.pr_decoration.form.private_key=Private Key
settings.pr_decoration.form.save=Save configuration
settings.pr_decoration.form.cancel=Cancel

settings.pr_decoration.binding.category=Pull Request decoration
settings.pr_decoration.binding.no_bindings=This feature requires to be enabled in the global settings. {link}
settings.pr_decoration.binding.title=Pull Request decoration
settings.pr_decoration.binding.description=Enable Pull Request decoration for this project.
settings.pr_decoration.binding.form.url=Project location
settings.pr_decoration.binding.form.name=Configuration name
settings.pr_decoration.binding.form.repository=Repository identifier
settings.pr_decoration.binding.form.repository.help=This is the path of your repository. Example: {example}

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

Loading…
Cancel
Save