--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" data-name="logo art" width="342.955" height="318.789"><rect id="backgroundrect" width="100%" height="100%" x="0" y="0" fill="none" stroke="none"/><defs><style>.cls-1{fill:#fc6d26}.cls-2{fill:#e24329}.cls-3{fill:#fca326}</style></defs><g class="currentLayer"><g id="g44"><path id="path46" class="cls-1" d="M339.562 180.724l-18.91-58.12-37.42-115.28a6.47 6.47 0 00-12.27 0l-37.42 115.21h-124.33L71.792 7.324a6.46 6.46 0 00-12.26 0l-37.36 115.21-18.91 58.19a12.88 12.88 0 004.66 14.39l163.47 118.78 163.44-118.78a12.9 12.9 0 004.73-14.39"/></g><g id="g48"><path id="path50" class="cls-2" d="M171.392 313.804l62.16-191.28h-124.29l62.13 191.28z"/></g><g id="g56"><path id="path58" class="cls-1" d="M171.392 313.804l-62.18-191.28h-87l149.18 191.28z"/></g><g id="g64"><path id="path66" class="cls-3" d="M22.142 122.584l-18.91 58.12a12.88 12.88 0 004.66 14.39l163.5 118.8-149.25-191.31z"/></g><g id="g72"><path id="path74" class="cls-2" d="M22.172 122.584h87.11l-37.49-115.2a6.47 6.47 0 00-12.27 0l-37.35 115.2z"/></g><g id="g76"><path id="path78" class="cls-1" d="M171.392 313.804l62.16-191.28h87.14l-149.3 191.28z"/></g><g id="g80"><path id="path82" class="cls-3" d="M320.632 122.584l18.91 58.12a12.85 12.85 0 01-4.66 14.39l-163.49 118.71 149.2-191.22z"/></g><g id="g84"><path id="path86" class="cls-2" d="M320.672 122.584h-87.1l37.42-115.2a6.46 6.46 0 0112.26 0l37.42 115.2z"/></g></g></svg>
\ No newline at end of file
BitbucketProjectAlmBinding,
GithubBindingDefinition,
GithubProjectAlmBinding,
+ GitlabBindingDefinition,
+ GitlabProjectAlmBinding,
ProjectAlmBinding
} from '../types/alm-settings';
return post('/api/alm_settings/update_bitbucket', data).catch(throwGlobalError);
}
+export function createGitlabConfiguration(data: GitlabBindingDefinition) {
+ return post('/api/alm_settings/create_gitlab', data).catch(throwGlobalError);
+}
+
+export function updateGitlabConfiguration(data: GitlabBindingDefinition & { newKey: string }) {
+ return post('/api/alm_settings/update_gitlab', data).catch(throwGlobalError);
+}
+
export function deleteConfiguration(key: string) {
return post('/api/alm_settings/delete', { key }).catch(throwGlobalError);
}
export function setProjectGithubBinding(data: GithubProjectAlmBinding) {
return post('/api/alm_settings/set_github_binding', data).catch(throwGlobalError);
}
+
+export function setProjectGitlabBinding(data: GitlabProjectAlmBinding) {
+ return post('/api/alm_settings/set_gitlab_binding', data).catch(throwGlobalError);
+}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { AlmSettingsBinding } from '../../../../types/alm-settings';
+import { AlmSettingsBinding, ALM_KEYS } from '../../../../types/alm-settings';
import AlmPRDecorationFormModalRenderer from './AlmPRDecorationFormModalRenderer';
interface ChildrenProps<AlmBindingDefinitionType> {
}
interface Props<B> {
+ alm: ALM_KEYS;
children: (props: ChildrenProps<B>) => React.ReactNode;
bindingDefinition: B;
onCancel: () => void;
};
render() {
- const { children, bindingDefinition } = this.props;
+ const { alm, children, bindingDefinition } = this.props;
const { formData } = this.state;
return (
<AlmPRDecorationFormModalRenderer
+ alm={alm}
canSubmit={this.canSubmit}
onCancel={this.props.onCancel}
onSubmit={this.handleFormSubmit}
import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
+import { ALM_KEYS } from '../../../../types/alm-settings';
export interface AlmPRDecorationFormModalProps {
+ alm: ALM_KEYS;
canSubmit: () => boolean;
children: React.ReactNode;
onCancel: () => void;
}
export default function AlmPRDecorationFormModalRenderer(props: AlmPRDecorationFormModalProps) {
- const { children, originalKey } = props;
- const header = translate('settings.pr_decoration.form.header', originalKey ? 'edit' : 'create');
+ const { alm, children, originalKey } = props;
+ const header = translate(
+ 'settings',
+ alm === ALM_KEYS.GITLAB ? 'mr_decoration' : 'pr_decoration',
+ 'form.header',
+ originalKey ? 'edit' : 'create'
+ );
return (
<SimpleModal header={header} onClose={props.onCancel} onSubmit={props.onSubmit} size="medium">
value={formData.key}
/>
<AlmDefinitionFormField
- help={translate('settings.pr_decoration.form.personal_access_token.help')}
+ help={translate('settings.pr_decoration.form.personal_access_token.azure.help')}
id="personal_access_token"
isTextArea={true}
onFieldChange={onFieldChange}
{editedDefinition && (
<AlmPRDecorationFormModal
+ alm={ALM_KEYS.AZURE}
bindingDefinition={editedDefinition}
onCancel={props.onCancel}
onSubmit={props.onSubmit}>
{editedDefinition && (
<AlmPRDecorationFormModal
+ alm={ALM_KEYS.BITBUCKET}
bindingDefinition={editedDefinition}
onCancel={props.onCancel}
onSubmit={props.onSubmit}>
{editedDefinition && (
<AlmPRDecorationFormModal
+ alm={ALM_KEYS.GITHUB}
bindingDefinition={editedDefinition}
onCancel={props.onCancel}
onSubmit={props.onSubmit}>
--- /dev/null
+/*
+ * 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 { translate } from 'sonar-ui-common/helpers/l10n';
+import { GitlabBindingDefinition } from '../../../../types/alm-settings';
+import { AlmDefinitionFormField } from './AlmDefinitionFormField';
+
+export interface GitlabFormModalProps {
+ formData: GitlabBindingDefinition;
+ onFieldChange: (fieldId: keyof GitlabBindingDefinition, value: string) => void;
+}
+
+export function GitlabFormModal(props: GitlabFormModalProps) {
+ const { formData, onFieldChange } = props;
+
+ return (
+ <>
+ <AlmDefinitionFormField
+ autoFocus={true}
+ help={translate('settings.pr_decoration.form.name.gitlab.help')}
+ id="name.gitlab"
+ onFieldChange={onFieldChange}
+ propKey="key"
+ value={formData.key}
+ />
+ <AlmDefinitionFormField
+ help={translate('settings.pr_decoration.form.personal_access_token.gitlab.help')}
+ id="personal_access_token"
+ isTextArea={true}
+ onFieldChange={onFieldChange}
+ propKey="personalAccessToken"
+ value={formData.personalAccessToken}
+ />
+ </>
+ );
+}
+
+export default React.memo(GitlabFormModal);
--- /dev/null
+/*
+ * 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 { createGitlabConfiguration, updateGitlabConfiguration } from '../../../../api/almSettings';
+import { GitlabBindingDefinition } from '../../../../types/alm-settings';
+import GitlabTabRenderer from './GitlabTabRenderer';
+
+interface Props {
+ definitions: GitlabBindingDefinition[];
+ loading: boolean;
+ onDelete: (definitionKey: string) => void;
+ onUpdateDefinitions: () => void;
+}
+
+interface State {
+ editedDefinition?: GitlabBindingDefinition;
+ projectCount?: number;
+}
+
+export default class GitlabTab extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {};
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleEdit = (definitionKey: string) => {
+ const editedDefinition = this.props.definitions.find(d => d.key === definitionKey);
+ this.setState({ editedDefinition });
+ };
+
+ handleSubmit = (config: GitlabBindingDefinition, originalKey: string) => {
+ const call = originalKey
+ ? updateGitlabConfiguration({ newKey: config.key, ...config, key: originalKey })
+ : createGitlabConfiguration(config);
+ return call.then(this.props.onUpdateDefinitions).then(() => {
+ if (this.mounted) {
+ this.setState({ editedDefinition: undefined });
+ }
+ });
+ };
+
+ handleCancel = () => {
+ this.setState({
+ editedDefinition: undefined
+ });
+ };
+
+ handleCreate = () => {
+ this.setState({ editedDefinition: { key: '', personalAccessToken: '' } });
+ };
+
+ render() {
+ const { definitions, loading } = this.props;
+ const { editedDefinition } = this.state;
+ return (
+ <GitlabTabRenderer
+ definitions={definitions}
+ editedDefinition={editedDefinition}
+ loading={loading}
+ onCancel={this.handleCancel}
+ onCreate={this.handleCreate}
+ onDelete={this.props.onDelete}
+ onEdit={this.handleEdit}
+ onSubmit={this.handleSubmit}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { ALM_KEYS, GitlabBindingDefinition } from '../../../../types/alm-settings';
+import AlmPRDecorationFormModal from './AlmPRDecorationFormModal';
+import AlmPRDecorationTable from './AlmPRDecorationTable';
+import GitlabFormModal from './GitlabFormModal';
+import TabHeader from './TabHeader';
+
+export interface GitlabTabRendererProps {
+ editedDefinition?: GitlabBindingDefinition;
+ definitions: GitlabBindingDefinition[];
+ loading: boolean;
+ onCancel: () => void;
+ onCreate: () => void;
+ onDelete: (definitionKey: string) => void;
+ onEdit: (definitionKey: string) => void;
+ onSubmit: (config: GitlabBindingDefinition, originalKey: string) => void;
+}
+
+export default function GitlabTabRenderer(props: GitlabTabRendererProps) {
+ const { definitions, editedDefinition, loading } = props;
+ return (
+ <>
+ <TabHeader
+ alm={ALM_KEYS.GITLAB}
+ definitionCount={definitions.length}
+ onCreate={props.onCreate}
+ />
+
+ <DeferredSpinner loading={loading}>
+ <AlmPRDecorationTable
+ additionalColumnsHeaders={[]}
+ alm={ALM_KEYS.GITLAB}
+ definitions={definitions.map(({ key }) => ({
+ key,
+ additionalColumns: []
+ }))}
+ onDelete={props.onDelete}
+ onEdit={props.onEdit}
+ />
+ </DeferredSpinner>
+
+ {editedDefinition && (
+ <AlmPRDecorationFormModal
+ alm={ALM_KEYS.GITLAB}
+ bindingDefinition={editedDefinition}
+ onCancel={props.onCancel}
+ onSubmit={props.onSubmit}>
+ {childProps => <GitlabFormModal {...childProps} />}
+ </AlmPRDecorationFormModal>
+ )}
+ </>
+ );
+}
import BitbucketTab from './BitbucketTab';
import DeleteModal from './DeleteModal';
import GithubTab from './GithubTab';
+import GitlabTab from './GitlabTab';
export interface PRDecorationTabsProps {
currentAlm: ALM_KEYS;
export const almName = {
[ALM_KEYS.AZURE]: 'Azure DevOps Server',
[ALM_KEYS.BITBUCKET]: 'Bitbucket Server',
- [ALM_KEYS.GITHUB]: 'GitHub'
+ [ALM_KEYS.GITHUB]: 'GitHub',
+ [ALM_KEYS.GITLAB]: 'GitLab'
};
export default function PRDecorationTabs(props: PRDecorationTabsProps) {
<img
alt="github"
className="spacer-right"
+ height={16}
src={`${getBaseUrl()}/images/alm/github.svg`}
- width={16}
/>
{almName[ALM_KEYS.GITHUB]}
</>
<img
alt="bitbucket"
className="spacer-right"
+ height={16}
src={`${getBaseUrl()}/images/alm/bitbucket.svg`}
- width={16}
/>
{almName[ALM_KEYS.BITBUCKET]}
</>
<img
alt="azure"
className="spacer-right"
+ height={16}
src={`${getBaseUrl()}/images/alm/azure.svg`}
- width={16}
/>
{almName[ALM_KEYS.AZURE]}
</>
)
+ },
+ {
+ key: ALM_KEYS.GITLAB,
+ label: (
+ <>
+ <img
+ alt="gitlab"
+ className="spacer-right"
+ height={16}
+ src={`${getBaseUrl()}/images/alm/gitlab.svg`}
+ />
+ {almName[ALM_KEYS.GITLAB]}
+ </>
+ )
}
]}
/>
onUpdateDefinitions={props.onUpdateDefinitions}
/>
)}
+ {currentAlm === ALM_KEYS.GITLAB && (
+ <GitlabTab
+ definitions={definitions.gitlab}
+ loading={loading}
+ onDelete={props.onDelete}
+ onUpdateDefinitions={props.onUpdateDefinitions}
+ />
+ )}
</div>
{definitionKeyForDeletion && (
definitions: {
[ALM_KEYS.AZURE]: [],
[ALM_KEYS.BITBUCKET]: [],
- [ALM_KEYS.GITHUB]: []
+ [ALM_KEYS.GITHUB]: [],
+ [ALM_KEYS.GITLAB]: []
},
loading: true
};
</Alert>
<div className="big-spacer-bottom display-flex-space-between">
- <h4 className="display-inline">{translate('settings.pr_decoration.table.title')}</h4>
+ <h4 className="display-inline">
+ {translate(
+ 'settings',
+ alm === ALM_KEYS.GITLAB ? 'mr_decoration' : 'pr_decoration',
+ 'table.title'
+ )}
+ </h4>
{showButton && (
<Button data-test="settings__alm-create" onClick={props.onCreate}>
{translate('settings.pr_decoration.table.create')}
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import { mockGithubDefinition } from '../../../../../helpers/testMocks';
-import { GithubBindingDefinition } from '../../../../../types/alm-settings';
+import { mockGithubDefinition } from '../../../../../helpers/mocks/alm-settings';
+import { ALM_KEYS, GithubBindingDefinition } from '../../../../../types/alm-settings';
import AlmPRDecorationFormModal from '../AlmPRDecorationFormModal';
it('should render correctly', () => {
) {
return shallow<AlmPRDecorationFormModal<GithubBindingDefinition>>(
<AlmPRDecorationFormModal
+ alm={ALM_KEYS.GITHUB}
bindingDefinition={{ appId: '', key: '', privateKey: '', url: '' }}
onCancel={jest.fn()}
onSubmit={jest.fn()}
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { ALM_KEYS } from '../../../../../types/alm-settings';
import AlmPRDecorationFormModalRenderer, {
AlmPRDecorationFormModalProps
} from '../AlmPRDecorationFormModalRenderer';
function shallowRender(props: Partial<AlmPRDecorationFormModalProps> = {}) {
return shallow(
<AlmPRDecorationFormModalRenderer
+ alm={ALM_KEYS.GITHUB}
canSubmit={jest.fn()}
onCancel={jest.fn()}
onSubmit={jest.fn()}
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockAzureDefinition } from '../../../../../helpers/testMocks';
+import { mockAzureDefinition } from '../../../../../helpers/mocks/alm-settings';
import AzureFormModal, { AzureFormModalProps } from '../AzureFormModal';
it('should render correctly', () => {
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { createAzureConfiguration, updateAzureConfiguration } from '../../../../../api/almSettings';
-import { mockAzureDefinition } from '../../../../../helpers/testMocks';
+import { mockAzureDefinition } from '../../../../../helpers/mocks/alm-settings';
import AzureTab from '../AzureTab';
jest.mock('../../../../../api/almSettings', () => ({
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockAzureDefinition } from '../../../../../helpers/testMocks';
+import { mockAzureDefinition } from '../../../../../helpers/mocks/alm-settings';
import AzureTabRenderer, { AzureTabRendererProps } from '../AzureTabRenderer';
it('should render correctly', () => {
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockBitbucketDefinition } from '../../../../../helpers/testMocks';
+import { mockBitbucketDefinition } from '../../../../../helpers/mocks/alm-settings';
import BitbucketFormModal, { BitbucketFormModalProps } from '../BitbucketFormModal';
it('should render correctly', () => {
createBitbucketConfiguration,
updateBitbucketConfiguration
} from '../../../../../api/almSettings';
-import { mockBitbucketDefinition } from '../../../../../helpers/testMocks';
+import { mockBitbucketDefinition } from '../../../../../helpers/mocks/alm-settings';
import BitbucketTab from '../BitbucketTab';
jest.mock('../../../../../api/almSettings', () => ({
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockBitbucketDefinition } from '../../../../../helpers/testMocks';
+import { mockBitbucketDefinition } from '../../../../../helpers/mocks/alm-settings';
import BitbucketTabRenderer, { BitbucketTabRendererProps } from '../BitbucketTabRenderer';
it('should render correctly', () => {
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockGithubDefinition } from '../../../../../helpers/testMocks';
+import { mockGithubDefinition } from '../../../../../helpers/mocks/alm-settings';
import GithubFormModal, { GithubFormModalProps } from '../GithubFormModal';
it('should render correctly', () => {
createGithubConfiguration,
updateGithubConfiguration
} from '../../../../../api/almSettings';
-import { mockGithubDefinition } from '../../../../../helpers/testMocks';
+import { mockGithubDefinition } from '../../../../../helpers/mocks/alm-settings';
import GithubTab from '../GithubTab';
jest.mock('../../../../../api/almSettings', () => ({
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockGithubDefinition } from '../../../../../helpers/testMocks';
+import { mockGithubDefinition } from '../../../../../helpers/mocks/alm-settings';
import GithubTabRenderer, { GithubTabRendererProps } from '../GithubTabRenderer';
it('should render correctly', () => {
--- /dev/null
+/*
+ * 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 { mockGitlabDefinition } from '../../../../../helpers/mocks/alm-settings';
+import { GitlabFormModal, GitlabFormModalProps } from '../GitlabFormModal';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+ expect(shallowRender({ formData: mockGitlabDefinition() })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<GitlabFormModalProps> = {}) {
+ return shallow(
+ <GitlabFormModal
+ formData={{ key: '', personalAccessToken: '' }}
+ onFieldChange={jest.fn()}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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 {
+ createGitlabConfiguration,
+ updateGitlabConfiguration
+} from '../../../../../api/almSettings';
+import { mockGitlabDefinition } from '../../../../../helpers/mocks/alm-settings';
+import GitlabTab from '../GitlabTab';
+
+jest.mock('../../../../../api/almSettings', () => ({
+ countBindedProjects: jest.fn().mockResolvedValue(2),
+ createGitlabConfiguration: jest.fn().mockResolvedValue({}),
+ deleteConfiguration: jest.fn().mockResolvedValue({}),
+ updateGitlabConfiguration: jest.fn().mockResolvedValue({})
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should handle cancel', async () => {
+ const wrapper = shallowRender();
+
+ wrapper.setState({
+ editedDefinition: mockGitlabDefinition()
+ });
+
+ wrapper.instance().handleCancel();
+
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state().editedDefinition).toBeUndefined();
+});
+
+it('should handle edit', async () => {
+ const config = mockGitlabDefinition();
+ const wrapper = shallowRender({ definitions: [config] });
+ wrapper.instance().handleEdit(config.key);
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().editedDefinition).toEqual(config);
+});
+
+it('should create config', async () => {
+ const onUpdateDefinitions = jest.fn();
+ const config = mockGitlabDefinition();
+ const wrapper = shallowRender({ onUpdateDefinitions });
+ wrapper.setState({ editedDefinition: config });
+
+ await wrapper.instance().handleSubmit(config, '');
+
+ expect(createGitlabConfiguration).toBeCalledWith(config);
+ expect(onUpdateDefinitions).toBeCalled();
+ expect(wrapper.state().editedDefinition).toBeUndefined();
+});
+
+it('should update config', async () => {
+ const onUpdateDefinitions = jest.fn();
+ const config = mockGitlabDefinition();
+ const wrapper = shallowRender({ onUpdateDefinitions });
+ wrapper.setState({ editedDefinition: config });
+
+ await wrapper.instance().handleSubmit(config, 'originalKey');
+
+ expect(updateGitlabConfiguration).toBeCalledWith({
+ newKey: 'foo',
+ ...config,
+ key: 'originalKey'
+ });
+ expect(onUpdateDefinitions).toBeCalled();
+ expect(wrapper.state().editedDefinition).toBeUndefined();
+});
+
+function shallowRender(props: Partial<GitlabTab['props']> = {}) {
+ return shallow<GitlabTab>(
+ <GitlabTab
+ definitions={[]}
+ loading={false}
+ onDelete={jest.fn()}
+ onUpdateDefinitions={jest.fn()}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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 { mockGitlabDefinition } from '../../../../../helpers/mocks/alm-settings';
+import GitlabTabRenderer, { GitlabTabRendererProps } from '../GitlabTabRenderer';
+
+it('should render correctly', () => {
+ expect(shallowRender({ loading: true })).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
+ expect(shallowRender({ editedDefinition: mockGitlabDefinition() })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<GitlabTabRendererProps> = {}) {
+ return shallow(
+ <GitlabTabRenderer
+ definitions={[]}
+ loading={false}
+ onCancel={jest.fn()}
+ onCreate={jest.fn()}
+ onDelete={jest.fn()}
+ onEdit={jest.fn()}
+ onSubmit={jest.fn()}
+ {...props}
+ />
+ );
+}
expect(shallowRender({ loading: true })).toMatchSnapshot();
expect(shallowRender({ definitionKeyForDeletion: 'keyToDelete' })).toMatchSnapshot();
expect(shallowRender({ currentAlm: ALM_KEYS.AZURE })).toMatchSnapshot();
- expect(shallowRender({ currentAlm: ALM_KEYS.GITHUB })).toMatchSnapshot();
+ expect(shallowRender({ currentAlm: ALM_KEYS.BITBUCKET })).toMatchSnapshot();
+ expect(shallowRender({ currentAlm: ALM_KEYS.GITLAB })).toMatchSnapshot();
});
function shallowRender(props: Partial<PRDecorationTabsProps> = {}) {
return shallow(
<PRDecorationTabs
currentAlm={ALM_KEYS.GITHUB}
- definitions={{ azure: [], bitbucket: [], github: [] }}
+ definitions={{ azure: [], bitbucket: [], github: [], gitlab: [] }}
loading={false}
onCancel={jest.fn()}
onConfirmDelete={jest.fn()}
exports[`should render correctly 1`] = `
<AlmPRDecorationFormModalRenderer
+ alm="github"
canSubmit={[Function]}
onCancel={[MockFunction]}
onSubmit={[Function]}
value=""
/>
<AlmDefinitionFormField
- help="settings.pr_decoration.form.personal_access_token.help"
+ help="settings.pr_decoration.form.personal_access_token.azure.help"
id="personal_access_token"
isTextArea={true}
onFieldChange={[MockFunction]}
value="key"
/>
<AlmDefinitionFormField
- help="settings.pr_decoration.form.personal_access_token.help"
+ help="settings.pr_decoration.form.personal_access_token.azure.help"
id="personal_access_token"
isTextArea={true}
onFieldChange={[MockFunction]}
/>
</DeferredSpinner>
<AlmPRDecorationFormModal
+ alm="azure"
bindingDefinition={
Object {
"key": "key",
/>
</DeferredSpinner>
<AlmPRDecorationFormModal
+ alm="bitbucket"
bindingDefinition={
Object {
"key": "key",
/>
</DeferredSpinner>
<AlmPRDecorationFormModal
+ alm="github"
bindingDefinition={
Object {
"appId": "123456",
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <AlmDefinitionFormField
+ autoFocus={true}
+ help="settings.pr_decoration.form.name.gitlab.help"
+ id="name.gitlab"
+ onFieldChange={[MockFunction]}
+ propKey="key"
+ value=""
+ />
+ <AlmDefinitionFormField
+ help="settings.pr_decoration.form.personal_access_token.gitlab.help"
+ id="personal_access_token"
+ isTextArea={true}
+ onFieldChange={[MockFunction]}
+ propKey="personalAccessToken"
+ value=""
+ />
+</Fragment>
+`;
+
+exports[`should render correctly 2`] = `
+<Fragment>
+ <AlmDefinitionFormField
+ autoFocus={true}
+ help="settings.pr_decoration.form.name.gitlab.help"
+ id="name.gitlab"
+ onFieldChange={[MockFunction]}
+ propKey="key"
+ value="foo"
+ />
+ <AlmDefinitionFormField
+ help="settings.pr_decoration.form.personal_access_token.gitlab.help"
+ id="personal_access_token"
+ isTextArea={true}
+ onFieldChange={[MockFunction]}
+ propKey="personalAccessToken"
+ value="foobar"
+ />
+</Fragment>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<GitlabTabRenderer
+ definitions={Array []}
+ loading={false}
+ onCancel={[Function]}
+ onCreate={[Function]}
+ onDelete={[MockFunction]}
+ onEdit={[Function]}
+ onSubmit={[Function]}
+/>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <Connect(withAppState(TabHeader))
+ alm="gitlab"
+ definitionCount={0}
+ onCreate={[MockFunction]}
+ />
+ <DeferredSpinner
+ loading={true}
+ timeout={100}
+ >
+ <AlmPRDecorationTable
+ additionalColumnsHeaders={Array []}
+ alm="gitlab"
+ definitions={Array []}
+ onDelete={[MockFunction]}
+ onEdit={[MockFunction]}
+ />
+ </DeferredSpinner>
+</Fragment>
+`;
+
+exports[`should render correctly 2`] = `
+<Fragment>
+ <Connect(withAppState(TabHeader))
+ alm="gitlab"
+ definitionCount={0}
+ onCreate={[MockFunction]}
+ />
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <AlmPRDecorationTable
+ additionalColumnsHeaders={Array []}
+ alm="gitlab"
+ definitions={Array []}
+ onDelete={[MockFunction]}
+ onEdit={[MockFunction]}
+ />
+ </DeferredSpinner>
+</Fragment>
+`;
+
+exports[`should render correctly 3`] = `
+<Fragment>
+ <Connect(withAppState(TabHeader))
+ alm="gitlab"
+ definitionCount={0}
+ onCreate={[MockFunction]}
+ />
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <AlmPRDecorationTable
+ additionalColumnsHeaders={Array []}
+ alm="gitlab"
+ definitions={Array []}
+ onDelete={[MockFunction]}
+ onEdit={[MockFunction]}
+ />
+ </DeferredSpinner>
+ <AlmPRDecorationFormModal
+ alm="gitlab"
+ bindingDefinition={
+ Object {
+ "key": "foo",
+ "personalAccessToken": "foobar",
+ }
+ }
+ onCancel={[MockFunction]}
+ onSubmit={[MockFunction]}
+ >
+ <Component />
+ </AlmPRDecorationFormModal>
+</Fragment>
+`;
<img
alt="github"
className="spacer-right"
+ height={16}
src="/images/alm/github.svg"
- width={16}
/>
GitHub
</React.Fragment>,
<img
alt="bitbucket"
className="spacer-right"
+ height={16}
src="/images/alm/bitbucket.svg"
- width={16}
/>
Bitbucket Server
</React.Fragment>,
<img
alt="azure"
className="spacer-right"
+ height={16}
src="/images/alm/azure.svg"
- width={16}
/>
Azure DevOps Server
</React.Fragment>,
},
+ Object {
+ "key": "gitlab",
+ "label": <React.Fragment>
+ <img
+ alt="gitlab"
+ className="spacer-right"
+ height={16}
+ src="/images/alm/gitlab.svg"
+ />
+ GitLab
+ </React.Fragment>,
+ },
]
}
/>
<img
alt="github"
className="spacer-right"
+ height={16}
src="/images/alm/github.svg"
- width={16}
/>
GitHub
</React.Fragment>,
<img
alt="bitbucket"
className="spacer-right"
+ height={16}
src="/images/alm/bitbucket.svg"
- width={16}
/>
Bitbucket Server
</React.Fragment>,
<img
alt="azure"
className="spacer-right"
+ height={16}
src="/images/alm/azure.svg"
- width={16}
/>
Azure DevOps Server
</React.Fragment>,
},
+ Object {
+ "key": "gitlab",
+ "label": <React.Fragment>
+ <img
+ alt="gitlab"
+ className="spacer-right"
+ height={16}
+ src="/images/alm/gitlab.svg"
+ />
+ GitLab
+ </React.Fragment>,
+ },
]
}
/>
<img
alt="github"
className="spacer-right"
+ height={16}
src="/images/alm/github.svg"
- width={16}
/>
GitHub
</React.Fragment>,
<img
alt="bitbucket"
className="spacer-right"
+ height={16}
src="/images/alm/bitbucket.svg"
- width={16}
/>
Bitbucket Server
</React.Fragment>,
<img
alt="azure"
className="spacer-right"
+ height={16}
src="/images/alm/azure.svg"
- width={16}
/>
Azure DevOps Server
</React.Fragment>,
},
+ Object {
+ "key": "gitlab",
+ "label": <React.Fragment>
+ <img
+ alt="gitlab"
+ className="spacer-right"
+ height={16}
+ src="/images/alm/gitlab.svg"
+ />
+ GitLab
+ </React.Fragment>,
+ },
]
}
/>
</div>
<BoxedTabs
onSelect={[MockFunction]}
- selected="github"
+ selected="bitbucket"
tabs={
Array [
Object {
<img
alt="github"
className="spacer-right"
+ height={16}
src="/images/alm/github.svg"
- width={16}
/>
GitHub
</React.Fragment>,
<img
alt="bitbucket"
className="spacer-right"
+ height={16}
src="/images/alm/bitbucket.svg"
- width={16}
/>
Bitbucket Server
</React.Fragment>,
<img
alt="azure"
className="spacer-right"
+ height={16}
src="/images/alm/azure.svg"
- width={16}
/>
Azure DevOps Server
</React.Fragment>,
},
+ Object {
+ "key": "gitlab",
+ "label": <React.Fragment>
+ <img
+ alt="gitlab"
+ className="spacer-right"
+ height={16}
+ src="/images/alm/gitlab.svg"
+ />
+ GitLab
+ </React.Fragment>,
+ },
]
}
/>
<div
className="boxed-group boxed-group-inner"
>
- <GithubTab
+ <BitbucketTab
+ definitions={Array []}
+ loading={false}
+ onDelete={[MockFunction]}
+ onUpdateDefinitions={[MockFunction]}
+ />
+ </div>
+</Fragment>
+`;
+
+exports[`should render correctly 5`] = `
+<Fragment>
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ settings.pr_decoration.title
+ </h1>
+ </header>
+ <div
+ className="markdown small spacer-top big-spacer-bottom"
+ >
+ settings.pr_decoration.description
+ </div>
+ <BoxedTabs
+ onSelect={[MockFunction]}
+ selected="gitlab"
+ tabs={
+ Array [
+ Object {
+ "key": "github",
+ "label": <React.Fragment>
+ <img
+ alt="github"
+ className="spacer-right"
+ height={16}
+ src="/images/alm/github.svg"
+ />
+ GitHub
+ </React.Fragment>,
+ },
+ Object {
+ "key": "bitbucket",
+ "label": <React.Fragment>
+ <img
+ alt="bitbucket"
+ className="spacer-right"
+ height={16}
+ src="/images/alm/bitbucket.svg"
+ />
+ Bitbucket Server
+ </React.Fragment>,
+ },
+ Object {
+ "key": "azure",
+ "label": <React.Fragment>
+ <img
+ alt="azure"
+ className="spacer-right"
+ height={16}
+ src="/images/alm/azure.svg"
+ />
+ Azure DevOps Server
+ </React.Fragment>,
+ },
+ Object {
+ "key": "gitlab",
+ "label": <React.Fragment>
+ <img
+ alt="gitlab"
+ className="spacer-right"
+ height={16}
+ src="/images/alm/gitlab.svg"
+ />
+ GitLab
+ </React.Fragment>,
+ },
+ ]
+ }
+ />
+ <div
+ className="boxed-group boxed-group-inner"
+ >
+ <GitlabTab
definitions={Array []}
loading={false}
onDelete={[MockFunction]}
"azure": Array [],
"bitbucket": Array [],
"github": Array [],
+ "gitlab": Array [],
}
}
loading={true}
getProjectAlmBinding,
setProjectAzureBinding,
setProjectBitbucketBinding,
- setProjectGithubBinding
+ setProjectGithubBinding,
+ setProjectGitlabBinding
} from '../../../../api/almSettings';
import throwGlobalError from '../../../../app/utils/throwGlobalError';
import { AlmSettingsInstance, ALM_KEYS, ProjectAlmBinding } from '../../../../types/alm-settings';
const FIELDS_BY_ALM: { [almKey in ALM_KEYS]: Array<keyof T.Omit<ProjectAlmBinding, 'key'>> } = {
[ALM_KEYS.AZURE]: [],
[ALM_KEYS.BITBUCKET]: ['repository', 'slug'],
- [ALM_KEYS.GITHUB]: ['repository']
+ [ALM_KEYS.GITHUB]: ['repository'],
+ [ALM_KEYS.GITLAB]: []
};
export default class PRDecorationBinding extends React.PureComponent<Props, State> {
repository
});
}
+
+ case ALM_KEYS.GITLAB:
+ return setProjectGitlabBinding({
+ almSetting,
+ project
+ });
+
default:
return Promise.reject();
}
--- /dev/null
+/*
+ * 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 {
+ AzureBindingDefinition,
+ BitbucketBindingDefinition,
+ GithubBindingDefinition,
+ GitlabBindingDefinition
+} from '../../types/alm-settings';
+
+export function mockAzureDefinition(
+ overrides: Partial<AzureBindingDefinition> = {}
+): AzureBindingDefinition {
+ return {
+ key: 'key',
+ personalAccessToken: 'asdf1234',
+ ...overrides
+ };
+}
+
+export function mockBitbucketDefinition(
+ overrides: Partial<BitbucketBindingDefinition> = {}
+): BitbucketBindingDefinition {
+ return {
+ key: 'key',
+ personalAccessToken: 'asdf1234',
+ url: 'http://bbs.enterprise.com',
+ ...overrides
+ };
+}
+
+export function mockGithubDefinition(
+ overrides: Partial<GithubBindingDefinition> = {}
+): GithubBindingDefinition {
+ return {
+ key: 'key',
+ url: 'http://github.enterprise.com',
+ appId: '123456',
+ privateKey: 'asdf1234',
+ ...overrides
+ };
+}
+
+export function mockGitlabDefinition(
+ overrides: Partial<GitlabBindingDefinition> = {}
+): GitlabBindingDefinition {
+ return {
+ key: 'foo',
+ personalAccessToken: 'foobar',
+ ...overrides
+ };
+}
import { createStore, Store } from 'redux';
import { DocumentationEntry } from '../apps/documentation/utils';
import { Exporter, Profile } from '../apps/quality-profiles/types';
-import {
- AzureBindingDefinition,
- BitbucketBindingDefinition,
- GithubBindingDefinition
-} from '../types/alm-settings';
export function mockAlmApplication(overrides: Partial<T.AlmApplication> = {}): T.AlmApplication {
return {
};
}
-export function mockAzureDefinition(
- overrides: Partial<AzureBindingDefinition> = {}
-): AzureBindingDefinition {
- return {
- key: 'key',
- personalAccessToken: 'asdf1234',
- ...overrides
- };
-}
-
-export function mockBitbucketDefinition(
- overrides: Partial<BitbucketBindingDefinition> = {}
-): BitbucketBindingDefinition {
- return {
- key: 'key',
- personalAccessToken: 'asdf1234',
- url: 'http://bbs.enterprise.com',
- ...overrides
- };
-}
-
-export function mockGithubDefinition(
- overrides: Partial<GithubBindingDefinition> = {}
-): GithubBindingDefinition {
- return {
- key: 'key',
- url: 'http://github.enterprise.com',
- appId: '123456',
- privateKey: 'asdf1234',
- ...overrides
- };
-}
-
export function mockAnalysis(overrides: Partial<T.Analysis> = {}): T.Analysis {
return {
date: '2017-03-01T09:36:01+0100',
export const enum ALM_KEYS {
AZURE = 'azure',
BITBUCKET = 'bitbucket',
- GITHUB = 'github'
+ GITHUB = 'github',
+ GITLAB = 'gitlab'
}
export interface AlmSettingsBinding {
azure: AzureBindingDefinition[];
bitbucket: BitbucketBindingDefinition[];
github: GithubBindingDefinition[];
+ gitlab: GitlabBindingDefinition[];
}
export interface AzureBindingDefinition extends AlmSettingsBinding {
url: string;
}
+export interface GitlabBindingDefinition extends AlmSettingsBinding {
+ personalAccessToken: string;
+}
+
export interface ProjectAlmBinding {
key: string;
repository?: string;
project: string;
repository: string;
}
+
+export interface GitlabProjectAlmBinding {
+ almSetting: string;
+ project: string;
+}
settings.pr_decoration.azure.info=Accounts that will be used to decorate Pull Requests need Code: Read & Write permission. {link}
settings.pr_decoration.bitbucket.info=Accounts that will be used to decorate Pull Requests need write permission. {link}
settings.pr_decoration.github.info=You need to install a GitHub App with specific settings and permissions to enable Pull Request Decoration on your Organization or Repository. {link}
+settings.pr_decoration.gitlab.info=Accounts that will be used to decorate Merge Requests need comment permissions on projects. The personal key needs the API scope permission. {link}
settings.pr_decoration.table.title=Pull Request Decoration configurations
+settings.mr_decoration.table.title=Merge Request Decoration configurations
settings.pr_decoration.table.empty.azure=Create your first Azure DevOps configuration to enable Pull Request Decoration on your projects.
settings.pr_decoration.table.empty.bitbucket=Create your first Bitbucket configuration to enable Pull Request Decoration on your projects.
settings.pr_decoration.table.empty.github=Create your first GitHub configuration to enable Pull Request Decoration on your organization or repository.
+settings.pr_decoration.table.empty.gitlab=Create your first GitLab configuration to enable Merge Request Decoration on your repository.
settings.pr_decoration.table.create=Create configuration
settings.pr_decoration.table.column.name=Name
settings.pr_decoration.table.column.bitbucket.url=Bitbucket Server URL
settings.pr_decoration.delete.info={0} projects will no longer get Pull Request Decorations.
settings.pr_decoration.delete.no_info=An unknown number of projects will no longer get Pull Request Decorations.
settings.pr_decoration.form.header.create=Create a Pull Request Decoration configuration
+settings.mr_decoration.form.header.create=Create a Merge Request Decoration configuration
settings.pr_decoration.form.header.edit=Edit the Pull Request Decoration configuration
+settings.mr_decoration.form.header.edit=Edit the Merge Request Decoration configuration
settings.pr_decoration.form.name.azure=Configuration name
settings.pr_decoration.form.name.azure.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured Azure instance for a project.
settings.pr_decoration.form.name.bitbucket=Configuration name
settings.pr_decoration.form.name.bitbucket.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured Bitbucket instance for a project.
settings.pr_decoration.form.name.github=Configuration name
settings.pr_decoration.form.name.github.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured GitHub App for a project.
+settings.pr_decoration.form.name.gitlab=Configuration name
+settings.pr_decoration.form.name.gitlab.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured GitLab instance for a project.
settings.pr_decoration.form.url.bitbucket=Bitbucket Server URL
settings.pr_decoration.form.url.bitbucket.help=Example: {example}
settings.pr_decoration.form.url.github=GitHub URL
settings.pr_decoration.form.app_id=GitHub App ID
settings.pr_decoration.form.private_key=Private Key
settings.pr_decoration.form.personal_access_token=Personal Access token
-settings.pr_decoration.form.personal_access_token.help=Token of the user that will be used to decorate the pull requests. Needs authorized scope: "Code (read and write)".
+settings.pr_decoration.form.personal_access_token.azure.help=Token of the user that will be used to decorate the Pull Requests. Needs authorized scope: "Code (read and write)".
+settings.pr_decoration.form.personal_access_token.gitlab.help=Token of the user that will be used to decorate the Merge Requests. Needs API scope authorization.
settings.pr_decoration.form.save=Save configuration
settings.pr_decoration.form.cancel=Cancel