Browse Source

SONARCLOUD-303 Allow to choose project visibility in manual project creation tutorial

tags/7.6
Wouter Admiraal 5 years ago
parent
commit
53cedfbab4

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

@@ -75,6 +75,7 @@ export function createProject(data: {
name: string;
project: string;
organization?: string;
visibility?: T.Visibility;
}): Promise<{ project: ProjectBase }> {
return postJSON('/api/projects/create', data).catch(throwGlobalError);
}

+ 11
- 0
server/sonar-web/src/main/js/apps/create/components/CardPlan.css View File

@@ -30,6 +30,17 @@
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);
}

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx View File

@@ -148,6 +148,7 @@ export class CreateProjectPage extends React.PureComponent<Props & WithRouterPro
{showManualTab || !almApplication ? (
<ManualProjectCreate
currentUser={currentUser}
fetchMyOrganizations={this.props.fetchMyOrganizations}
onProjectCreate={this.handleProjectCreate}
organization={state.organization}
userOrganizations={userOrganizations.filter(

+ 43
- 0
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.css View File

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

+ 110
- 33
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx View File

@@ -18,6 +18,7 @@
* 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';
@@ -25,9 +26,14 @@ import { createProject } from '../../../api/components';
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[];
@@ -36,11 +42,13 @@ interface Props {
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;
@@ -61,19 +69,27 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
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();
@@ -83,7 +99,8 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
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]),
() => {
@@ -96,7 +113,34 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
};

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) => {
@@ -107,32 +151,65 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
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>
);
}
}

+ 32
- 3
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx View File

@@ -35,7 +35,7 @@ it('should render correctly', () => {
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' });
@@ -47,7 +47,32 @@ it('should correctly create a project', async () => {
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']);
@@ -57,8 +82,12 @@ function getWrapper(props = {}) {
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}
/>
);

+ 46
- 38
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap View File

@@ -1,44 +1,52 @@
// 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>
`;

+ 44
- 22
server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx View File

@@ -25,6 +25,7 @@ interface Props {
canTurnToPrivate?: boolean;
className?: string;
onChange: (visibility: T.Visibility) => void;
showDetails?: boolean;
visibility?: T.Visibility;
}

@@ -43,9 +44,9 @@ export default class VisibilitySelector extends React.PureComponent<Props> {

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}>
@@ -56,29 +57,50 @@ export default class VisibilitySelector extends React.PureComponent<Props> {
/>
<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>
);

+ 15
- 9
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/VisibilitySelector-test.tsx.snap View File

@@ -1,9 +1,11 @@
// 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]}
@@ -18,7 +20,7 @@ exports[`changes visibility 1`] = `
</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]}
@@ -36,9 +38,11 @@ exports[`changes visibility 1`] = `
`;

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]}
@@ -53,7 +57,7 @@ exports[`changes visibility 2`] = `
</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]}
@@ -71,9 +75,11 @@ exports[`changes visibility 2`] = `
`;

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]}
@@ -88,7 +94,7 @@ exports[`renders disabled 1`] = `
</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

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

@@ -486,11 +486,13 @@ visibility.public.description.TRK=This project is public. Anyone can browse and
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.


#------------------------------------------------------------------------------
@@ -2779,6 +2781,7 @@ onboarding.create_project.repository_imported=Already imported: {link}
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

Loading…
Cancel
Save