@@ -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); | |||
}); | |||
}); |
@@ -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); | |||
} |
@@ -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} />; | |||
} |
@@ -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'; |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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')} />} |
@@ -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); |
@@ -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 ( | |||
<> |
@@ -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')} |
@@ -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 | |||
}; | |||
@@ -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} | |||
/> |
@@ -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} |
@@ -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 |
@@ -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" | |||
> |
@@ -139,7 +139,7 @@ exports[`should render correctly 3`] = ` | |||
/> | |||
<AlmPRDecorationFormModal | |||
alm="github" | |||
data={ | |||
bindingDefinition={ | |||
Object { | |||
"appId": "123456", | |||
"key": "key", |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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} /> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
`; |
@@ -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> | |||
`; |
@@ -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', |
@@ -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; | |||
} | |||
} |
@@ -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; |
@@ -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 |