name: string;
project: string;
organization?: string;
+ visibility?: T.Visibility;
}): Promise<{ project: ProjectBase }> {
return postJSON('/api/projects/create', data).catch(throwGlobalError);
}
transition: all 0.2s ease;
}
+.card-plan.animated {
+ height: 0;
+ border-width: 0;
+ overflow: hidden;
+}
+
+.card-plan.animated.open {
+ height: 210px;
+ border-width: 1px;
+}
+
.card-plan.highlight {
box-shadow: var(--defaultShadow);
}
{showManualTab || !almApplication ? (
<ManualProjectCreate
currentUser={currentUser}
+ fetchMyOrganizations={this.props.fetchMyOrganizations}
onProjectCreate={this.handleProjectCreate}
organization={state.organization}
userOrganizations={userOrganizations.filter(
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+.manual-project-create {
+ max-width: 650px;
+}
+
+.manual-project-create .visibility-select-option {
+ margin-left: 0 !important;
+ margin-bottom: var(--gridSize);
+ display: flex;
+ align-items: center;
+ font-size: var(--mediumFontSize);
+}
+
+.manual-project-create .visibility-details {
+ display: block;
+ margin: var(--gridSize) 0;
+}
+
+.manual-project-create .visibility-select-wrapper {
+ padding: var(--gridSize) 0 calc(2 * var(--gridSize)) 0;
+}
+
+.manual-project-create .button {
+ margin-top: var(--gridSize);
+}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as classNames from 'classnames';
import OrganizationInput from './OrganizationInput';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { SubmitButton } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
import ProjectKeyInput from '../components/ProjectKeyInput';
import ProjectNameInput from '../components/ProjectNameInput';
+import VisibilitySelector from '../../../components/common/VisibilitySelector';
+import { isSonarCloud } from '../../../helpers/system';
+import UpgradeOrganizationBox from '../components/UpgradeOrganizationBox';
+import './ManualProjectCreate.css';
interface Props {
currentUser: T.LoggedInUser;
+ fetchMyOrganizations: () => Promise<void>;
onProjectCreate: (projectKeys: string[]) => void;
organization?: string;
userOrganizations: T.Organization[];
interface State {
projectName?: string;
projectKey?: string;
- selectedOrganization: string;
+ selectedOrganization?: T.Organization;
+ selectedVisibility?: T.Visibility;
submitting: boolean;
}
-type ValidState = State & Required<Pick<State, 'projectName' | 'projectKey'>>;
+type ValidState = State &
+ Required<Pick<State, 'projectName' | 'projectKey' | 'selectedOrganization'>>;
export default class ManualProjectCreate extends React.PureComponent<Props, State> {
mounted = false;
this.mounted = false;
}
+ canChoosePrivate = (selectedOrganization: T.Organization | undefined) => {
+ return Boolean(selectedOrganization && selectedOrganization.subscription === 'PAID');
+ };
+
canSubmit(state: State): state is ValidState {
return Boolean(state.projectKey && state.projectName && state.selectedOrganization);
}
- getInitialSelectedOrganization(props: Props) {
+ getInitialSelectedOrganization = (props: Props) => {
if (props.organization) {
- return props.organization;
+ return this.getOrganization(props.organization);
} else if (props.userOrganizations.length === 1) {
- return props.userOrganizations[0].key;
+ return props.userOrganizations[0];
} else {
- return '';
+ return undefined;
}
- }
+ };
+
+ getOrganization = (organizationKey: string) => {
+ return this.props.userOrganizations.find(({ key }: T.Organization) => key === organizationKey);
+ };
handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
createProject({
project: state.projectKey,
name: state.projectName,
- organization: state.selectedOrganization
+ organization: state.selectedOrganization.key,
+ visibility: this.state.selectedVisibility
}).then(
({ project }) => this.props.onProjectCreate([project.key]),
() => {
};
handleOrganizationSelect = ({ key }: T.Organization) => {
- this.setState({ selectedOrganization: key });
+ const selectedOrganization = this.getOrganization(key);
+ let { selectedVisibility } = this.state;
+
+ if (selectedVisibility === undefined) {
+ selectedVisibility = this.canChoosePrivate(selectedOrganization) ? 'private' : 'public';
+ }
+
+ this.setState({
+ selectedOrganization,
+ selectedVisibility
+ });
+ };
+
+ handleOrganizationUpgrade = () => {
+ this.props.fetchMyOrganizations().then(
+ () => {
+ this.setState(prevState => {
+ if (prevState.selectedOrganization) {
+ const selectedOrganization = this.getOrganization(prevState.selectedOrganization.key);
+ return {
+ selectedOrganization
+ };
+ }
+ return null;
+ });
+ },
+ () => {}
+ );
};
handleProjectNameChange = (projectName?: string) => {
this.setState({ projectKey });
};
+ handleVisibilityChange = (selectedVisibility: T.Visibility) => {
+ this.setState({ selectedVisibility });
+ };
+
render() {
- const { submitting } = this.state;
+ const { selectedOrganization, submitting } = this.state;
+ const canChoosePrivate = this.canChoosePrivate(selectedOrganization);
+
return (
- <>
- <form onSubmit={this.handleFormSubmit}>
- <OrganizationInput
- onChange={this.handleOrganizationSelect}
- organization={this.state.selectedOrganization}
- organizations={this.props.userOrganizations}
- />
- <ProjectKeyInput
- className="form-field"
- initialValue={this.state.projectKey}
- onChange={this.handleProjectKeyChange}
- />
- <ProjectNameInput
- className="form-field"
- initialValue={this.state.projectName}
- onChange={this.handleProjectNameChange}
- />
- <SubmitButton disabled={!this.canSubmit(this.state) || submitting}>
- {translate('setup')}
- </SubmitButton>
- <DeferredSpinner className="spacer-left" loading={submitting} />
- </form>
- </>
+ <div className="create-project">
+ <div className="flex-1 huge-spacer-right">
+ <form className="manual-project-create" onSubmit={this.handleFormSubmit}>
+ <OrganizationInput
+ onChange={this.handleOrganizationSelect}
+ organization={selectedOrganization ? selectedOrganization.key : ''}
+ organizations={this.props.userOrganizations}
+ />
+ <ProjectKeyInput
+ className="form-field"
+ initialValue={this.state.projectKey}
+ onChange={this.handleProjectKeyChange}
+ />
+ <ProjectNameInput
+ className="form-field"
+ initialValue={this.state.projectName}
+ onChange={this.handleProjectNameChange}
+ />
+ {isSonarCloud() &&
+ selectedOrganization && (
+ <div
+ className={classNames('visibility-select-wrapper', {
+ open: Boolean(this.state.selectedOrganization)
+ })}>
+ <VisibilitySelector
+ canTurnToPrivate={canChoosePrivate}
+ onChange={this.handleVisibilityChange}
+ showDetails={true}
+ visibility={canChoosePrivate ? this.state.selectedVisibility : 'public'}
+ />
+ </div>
+ )}
+ <SubmitButton disabled={!this.canSubmit(this.state) || submitting}>
+ {translate('setup')}
+ </SubmitButton>
+ <DeferredSpinner className="spacer-left" loading={submitting} />
+ </form>
+ </div>
+
+ {isSonarCloud() &&
+ selectedOrganization && (
+ <div className="create-project-side-sticky">
+ <UpgradeOrganizationBox
+ className={classNames('animated', { open: !canChoosePrivate })}
+ onOrganizationUpgrade={this.handleOrganizationUpgrade}
+ organization={selectedOrganization}
+ />
+ </div>
+ )}
+ </div>
);
}
}
expect(getWrapper()).toMatchSnapshot();
});
-it('should correctly create a project', async () => {
+it('should correctly create a public project', async () => {
const onProjectCreate = jest.fn();
const wrapper = getWrapper({ onProjectCreate });
wrapper.find('withRouter(OrganizationInput)').prop<Function>('onChange')({ key: 'foo' });
expect(wrapper.find('SubmitButton').prop('disabled')).toBe(false);
submit(wrapper.find('form'));
- expect(createProject).toBeCalledWith({ project: 'bar', name: 'Bar', organization: 'foo' });
+ expect(createProject).toBeCalledWith({
+ project: 'bar',
+ name: 'Bar',
+ organization: 'foo',
+ visibility: 'public'
+ });
+
+ await waitAndUpdate(wrapper);
+ expect(onProjectCreate).toBeCalledWith(['bar']);
+});
+
+it('should correctly create a private project', async () => {
+ const onProjectCreate = jest.fn();
+ const wrapper = getWrapper({ onProjectCreate });
+ wrapper.find('withRouter(OrganizationInput)').prop<Function>('onChange')({ key: 'bar' });
+
+ change(wrapper.find('ProjectKeyInput'), 'bar');
+ change(wrapper.find('ProjectNameInput'), 'Bar');
+
+ submit(wrapper.find('form'));
+ expect(createProject).toBeCalledWith({
+ project: 'bar',
+ name: 'Bar',
+ organization: 'bar',
+ visibility: 'private'
+ });
await waitAndUpdate(wrapper);
expect(onProjectCreate).toBeCalledWith(['bar']);
return shallow(
<ManualProjectCreate
currentUser={{ groups: [], isLoggedIn: true, login: 'foo', name: 'Foo', scmAccounts: [] }}
+ fetchMyOrganizations={jest.fn()}
onProjectCreate={jest.fn()}
- userOrganizations={[{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]}
+ userOrganizations={[
+ { key: 'foo', name: 'Foo' },
+ { key: 'bar', name: 'Bar', subscription: 'PAID' }
+ ]}
{...props}
/>
);
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render correctly 1`] = `
-<Fragment>
- <form
- onSubmit={[Function]}
+<div
+ className="create-project"
+>
+ <div
+ className="flex-1 huge-spacer-right"
>
- <withRouter(OrganizationInput)
- onChange={[Function]}
- organization=""
- organizations={
- Array [
- Object {
- "key": "foo",
- "name": "Foo",
- },
- Object {
- "key": "bar",
- "name": "Bar",
- },
- ]
- }
- />
- <ProjectKeyInput
- className="form-field"
- onChange={[Function]}
- />
- <ProjectNameInput
- className="form-field"
- onChange={[Function]}
- />
- <SubmitButton
- disabled={true}
+ <form
+ className="manual-project-create"
+ onSubmit={[Function]}
>
- setup
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- timeout={100}
- />
- </form>
-</Fragment>
+ <withRouter(OrganizationInput)
+ onChange={[Function]}
+ organization=""
+ organizations={
+ Array [
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ },
+ Object {
+ "key": "bar",
+ "name": "Bar",
+ "subscription": "PAID",
+ },
+ ]
+ }
+ />
+ <ProjectKeyInput
+ className="form-field"
+ onChange={[Function]}
+ />
+ <ProjectNameInput
+ className="form-field"
+ onChange={[Function]}
+ />
+ <SubmitButton
+ disabled={true}
+ >
+ setup
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ timeout={100}
+ />
+ </form>
+ </div>
+</div>
`;
canTurnToPrivate?: boolean;
className?: string;
onChange: (visibility: T.Visibility) => void;
+ showDetails?: boolean;
visibility?: T.Visibility;
}
render() {
return (
- <div className={this.props.className}>
+ <div className={classNames('visibility-select', this.props.className)}>
<a
- className="link-base-color link-no-underline"
+ className="link-base-color link-no-underline visibility-select-option"
href="#"
id="visibility-public"
onClick={this.handlePublicClick}>
/>
<span className="spacer-left">{translate('visibility.public')}</span>
</a>
+ {this.props.showDetails && (
+ <span className="visibility-details note">
+ {translate('visibility.public.description.long')}
+ </span>
+ )}
{this.props.canTurnToPrivate ? (
- <a
- className="link-base-color link-no-underline huge-spacer-left"
- href="#"
- id="visibility-private"
- onClick={this.handlePrivateClick}>
- <i
- className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'private'
- })}
- />
- <span className="spacer-left">{translate('visibility.private')}</span>
- </a>
+ <>
+ <a
+ className="link-base-color link-no-underline huge-spacer-left visibility-select-option"
+ href="#"
+ id="visibility-private"
+ onClick={this.handlePrivateClick}>
+ <i
+ className={classNames('icon-radio', {
+ 'is-checked': this.props.visibility === 'private'
+ })}
+ />
+ <span className="spacer-left">{translate('visibility.private')}</span>
+ </a>
+ {this.props.showDetails && (
+ <span className="visibility-details note">
+ {translate('visibility.private.description.long')}
+ </span>
+ )}
+ </>
) : (
- <span className="huge-spacer-left text-muted cursor-not-allowed" id="visibility-private">
- <i
- className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'private'
- })}
- />
- <span className="spacer-left">{translate('visibility.private')}</span>
- </span>
+ <>
+ <span
+ className="huge-spacer-left text-muted cursor-not-allowed visibility-select-option"
+ id="visibility-private">
+ <i
+ className={classNames('icon-radio', {
+ 'is-checked': this.props.visibility === 'private'
+ })}
+ />
+ <span className="spacer-left">{translate('visibility.private')}</span>
+ </span>
+ {this.props.showDetails && (
+ <span className="visibility-details note">
+ {translate('visibility.private.description.long')}
+ </span>
+ )}
+ </>
)}
</div>
);
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`changes visibility 1`] = `
-<div>
+<div
+ className="visibility-select"
+>
<a
- className="link-base-color link-no-underline"
+ className="link-base-color link-no-underline visibility-select-option"
href="#"
id="visibility-public"
onClick={[Function]}
</span>
</a>
<a
- className="link-base-color link-no-underline huge-spacer-left"
+ className="link-base-color link-no-underline huge-spacer-left visibility-select-option"
href="#"
id="visibility-private"
onClick={[Function]}
`;
exports[`changes visibility 2`] = `
-<div>
+<div
+ className="visibility-select"
+>
<a
- className="link-base-color link-no-underline"
+ className="link-base-color link-no-underline visibility-select-option"
href="#"
id="visibility-public"
onClick={[Function]}
</span>
</a>
<a
- className="link-base-color link-no-underline huge-spacer-left"
+ className="link-base-color link-no-underline huge-spacer-left visibility-select-option"
href="#"
id="visibility-private"
onClick={[Function]}
`;
exports[`renders disabled 1`] = `
-<div>
+<div
+ className="visibility-select"
+>
<a
- className="link-base-color link-no-underline"
+ className="link-base-color link-no-underline visibility-select-option"
href="#"
id="visibility-public"
onClick={[Function]}
</span>
</a>
<span
- className="huge-spacer-left text-muted cursor-not-allowed"
+ className="huge-spacer-left text-muted cursor-not-allowed visibility-select-option"
id="visibility-private"
>
<i
visibility.public.description.VW=This portfolio is public. Anyone can browse it.
visibility.public.description.APP=This application is public. Anyone can browse it.
visibility.public.description.short=Anyone can browse and see the source code.
+visibility.public.description.long=Anyone will be able to browse your source code and see the result of your analysis.
visibility.private=Private
visibility.private.description.TRK=This project is private. Only authorized users can browse and see the source code.
visibility.private.description.VW=This portfolio is private. Only authorized users can browse it.
visibility.private.description.APP=This application is private. Only authorized users can browse it.
visibility.private.description.short=Only authorized users can browse and see the source code.
+visibility.private.description.long=Only members of the organization will be able to browse your source code and see the result of your analysis.
#------------------------------------------------------------------------------
onboarding.create_project.see_project=See the project
onboarding.create_project.select_repositories=Select repositories
onboarding.create_project.subscribe_to_import_private_repositories=You need to subscribe your organization to a paid plan to import private projects
+onboarding.create_project.encourage_to_subscribe=Subscribe your organization to our paid plan to get unlimited private projects.
onboarding.create_project.subscribtion_success_x={0} has been successfully upgraded to paid plan. You can now import and analyze private projects.
onboarding.create_organization.page.header=Create Organization