Browse Source

SONAR-20086 Migrating GitHub onboarding page to new design

tags/10.2.0.77647
Revanshu Paliwal 10 months ago
parent
commit
08ef818d25

+ 6
- 0
server/sonar-web/design-system/src/components/Text.tsx View File

@@ -108,6 +108,12 @@ export const LightLabel = styled.span`
color: ${themeColor('pageContentLight')};
`;

export const DarkLabel = styled.label`
color: ${themeColor('pageContentDark')};

${tw`sw-body-sm-highlight`}
`;

export const LightPrimary = styled.span`
color: ${themeContrast('primaryLight')};
`;

+ 5
- 17
server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx View File

@@ -31,8 +31,8 @@ import { getHostUrl } from '../../../../helpers/urls';
import { GithubOrganization, GithubRepository } from '../../../../types/alm-integration';
import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
import { Paging } from '../../../../types/types';
import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';
import { CreateProjectApiCallback } from '../types';
import GitHubProjectCreateRenderer from './GitHubProjectCreateRenderer';

interface Props {
canAdmin: boolean;
@@ -52,7 +52,6 @@ interface State {
repositories: GithubRepository[];
searchQuery: string;
selectedOrganization?: GithubOrganization;
selectedRepository?: GithubRepository;
selectedAlmInstance?: AlmSettingsInstance;
}

@@ -231,7 +230,6 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
triggerSearch = (query: string) => {
const { selectedOrganization } = this.state;
if (selectedOrganization) {
this.setState({ selectedRepository: undefined });
this.fetchRepositories({ organizationKey: selectedOrganization.key, query });
}
};
@@ -239,18 +237,11 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
handleSelectOrganization = (key: string) => {
this.setState(({ organizations }) => ({
searchQuery: '',
selectedRepository: undefined,
selectedOrganization: organizations.find((o) => o.key === key),
}));
this.fetchRepositories({ organizationKey: key });
};

handleSelectRepository = (key: string) => {
this.setState(({ repositories }) => ({
selectedRepository: repositories?.find((r) => r.key === key),
}));
};

handleSearch = (searchQuery: string) => {
this.setState({ searchQuery });
this.triggerSearch(searchQuery);
@@ -268,15 +259,15 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
}
};

handleImportRepository = () => {
const { selectedOrganization, selectedRepository, selectedAlmInstance } = this.state;
handleImportRepository = (repoKey: string) => {
const { selectedOrganization, selectedAlmInstance } = this.state;

if (selectedAlmInstance && selectedOrganization && selectedRepository) {
if (selectedAlmInstance && selectedOrganization && repoKey !== '') {
this.props.onProjectSetupDone(
setupGithubProjectCreation({
almSetting: selectedAlmInstance.key,
organization: selectedOrganization.key,
repositoryKey: selectedRepository.key,
repositoryKey: repoKey,
})
);
}
@@ -304,7 +295,6 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
repositories,
searchQuery,
selectedOrganization,
selectedRepository,
selectedAlmInstance,
} = this.state;

@@ -319,13 +309,11 @@ export default class GitHubProjectCreate extends React.Component<Props, State> {
onLoadMore={this.handleLoadMore}
onSearch={this.handleSearch}
onSelectOrganization={this.handleSelectOrganization}
onSelectRepository={this.handleSelectRepository}
organizations={organizations}
repositoryPaging={repositoryPaging}
searchQuery={searchQuery}
repositories={repositories}
selectedOrganization={selectedOrganization}
selectedRepository={selectedRepository}
almInstances={almInstances}
selectedAlmInstance={selectedAlmInstance}
onSelectedAlmInstanceChange={this.onSelectedAlmInstanceChange}

+ 92
- 167
server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreateRenderer.tsx View File

@@ -19,28 +19,27 @@
*/
/* eslint-disable react/no-unused-prop-types */

import {
DarkLabel,
DeferredSpinner,
FlagMessage,
InputSearch,
InputSelect,
LightPrimary,
Link,
Title,
} from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { colors } from '../../../../app/theme';
import Link from '../../../../components/common/Link';
import ListFooter from '../../../../components/controls/ListFooter';
import Radio from '../../../../components/controls/Radio';
import SearchBox from '../../../../components/controls/SearchBox';
import Select, { LabelValueSelectOption } from '../../../../components/controls/Select';
import { Button } from '../../../../components/controls/buttons';
import CheckIcon from '../../../../components/icons/CheckIcon';
import QualifierIcon from '../../../../components/icons/QualifierIcon';
import { Alert } from '../../../../components/ui/Alert';
import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
import { LabelValueSelectOption } from '../../../../components/controls/Select';
import { translate } from '../../../../helpers/l10n';
import { getBaseUrl } from '../../../../helpers/system';
import { getProjectUrl } from '../../../../helpers/urls';
import { GithubOrganization, GithubRepository } from '../../../../types/alm-integration';
import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
import { ComponentQualifier } from '../../../../types/component';
import { Paging } from '../../../../types/types';
import AlmRepoItem from '../components/AlmRepoItem';
import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown';
import CreateProjectPageHeader from '../components/CreateProjectPageHeader';

export interface GitHubProjectCreateRendererProps {
canAdmin: boolean;
@@ -48,17 +47,15 @@ export interface GitHubProjectCreateRendererProps {
loadingBindings: boolean;
loadingOrganizations: boolean;
loadingRepositories: boolean;
onImportRepository: () => void;
onImportRepository: (key: string) => void;
onLoadMore: () => void;
onSearch: (q: string) => void;
onSelectOrganization: (key: string) => void;
onSelectRepository: (key: string) => void;
organizations: GithubOrganization[];
repositories?: GithubRepository[];
repositoryPaging: Paging;
searchQuery: string;
selectedOrganization?: GithubOrganization;
selectedRepository?: GithubRepository;
almInstances: AlmSettingsInstance[];
selectedAlmInstance?: AlmSettingsInstance;
onSelectedAlmInstanceChange: (instance: AlmSettingsInstance) => void;
@@ -69,89 +66,40 @@ function orgToOption({ key, name }: GithubOrganization) {
}

function renderRepositoryList(props: GitHubProjectCreateRendererProps) {
const {
loadingRepositories,
repositories,
repositoryPaging,
searchQuery,
selectedOrganization,
selectedRepository,
} = props;

const isChecked = (repository: GithubRepository) =>
!!repository.sqProjectKey ||
(!!selectedRepository && selectedRepository.key === repository.key);

const isDisabled = (repository: GithubRepository) =>
!!repository.sqProjectKey || loadingRepositories;
const { loadingRepositories, repositories, repositoryPaging, searchQuery, selectedOrganization } =
props;

return (
selectedOrganization &&
repositories && (
<div className="boxed-group padded display-flex-wrap">
<div className="width-100">
<SearchBox
className="big-spacer-bottom"
<div>
<div className="sw-flex sw-items-center sw-mb-6">
<InputSearch
size="large"
onChange={props.onSearch}
placeholder={translate('onboarding.create_project.search_repositories')}
value={searchQuery}
clearIconAriaLabel={translate('clear')}
/>
<DeferredSpinner loading={loadingRepositories} className="sw-ml-2" />
</div>

{repositories.length === 0 ? (
<div className="padded">
<DeferredSpinner loading={loadingRepositories}>
{translate('no_results')}
</DeferredSpinner>
<div className="sw-py-6 sw-px-2">
<LightPrimary className="sw-body-sm">{translate('no_results')}</LightPrimary>
</div>
) : (
repositories.map((r) => (
<Radio
className="spacer-top spacer-bottom padded create-project-github-repository"
<AlmRepoItem
key={r.key}
checked={isChecked(r)}
disabled={isDisabled(r)}
value={r.key}
onCheck={props.onSelectRepository}
>
<div className="big overflow-hidden max-width-100" title={r.name}>
<div className="text-ellipsis">
{r.sqProjectKey ? (
<div className="display-flex-center max-width-100">
<Link
className="display-flex-center max-width-60"
to={getProjectUrl(r.sqProjectKey)}
>
<QualifierIcon
className="spacer-right"
qualifier={ComponentQualifier.Project}
/>
<span className="text-ellipsis">{r.name}</span>
</Link>
<em className="display-flex-center small big-spacer-left flex-0">
<span className="text-muted-2">
{translate('onboarding.create_project.repository_imported')}
</span>
<CheckIcon className="little-spacer-left" size={12} fill={colors.green} />
</em>
</div>
) : (
r.name
)}
</div>
{r.url && (
<a
className="notice small display-flex-center little-spacer-top"
onClick={(e) => e.stopPropagation()}
target="_blank"
href={r.url}
rel="noopener noreferrer"
>
{translate('onboarding.create_project.see_on_github')}
</a>
)}
</div>
</Radio>
almKey={r.key}
almUrl={r.url}
almUrlText={translate('onboarding.create_project.see_on_github')}
almIconSrc={`${getBaseUrl()}/images/tutorials/github-actions.svg`}
sqProjectKey={r.sqProjectKey}
onImport={props.onImportRepository}
primaryTextNode={<span title={r.name}>{r.name}</span>}
/>
))
)}

@@ -161,6 +109,7 @@ function renderRepositoryList(props: GitHubProjectCreateRendererProps) {
total={repositoryPaging.total}
loadMore={props.onLoadMore}
loading={loadingRepositories}
useMIUIButtons
/>
</div>
</div>
@@ -176,7 +125,6 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
loadingOrganizations,
organizations,
selectedOrganization,
selectedRepository,
almInstances,
selectedAlmInstance,
} = props;
@@ -186,33 +134,13 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
}

return (
<div>
<CreateProjectPageHeader
additionalActions={
selectedOrganization && (
<div className="display-flex-center pull-right">
<Button
className="button-large button-primary"
disabled={!selectedRepository}
onClick={props.onImportRepository}
>
{translate('onboarding.create_project.import_selected_repo')}
</Button>
</div>
)
}
title={
<span className="text-middle display-flex-center">
<img
alt="" // Should be ignored by screen readers
className="spacer-right"
height={24}
src={`${getBaseUrl()}/images/alm/github.svg`}
/>
{translate('onboarding.create_project.github.title')}
</span>
}
/>
<>
<header className="sw-mb-10">
<Title className="sw-mb-4">{translate('onboarding.create_project.github.title')}</Title>
<LightPrimary className="sw-body-sm">
{translate('onboarding.create_project.github.subtitle')}
</LightPrimary>
</header>

<AlmSettingsInstanceDropdown
almKey={AlmKeys.GitHub}
@@ -222,76 +150,73 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
/>

{error && selectedAlmInstance && (
<div className="display-flex-justify-center">
<div className="boxed-group padded width-50 huge-spacer-top">
<h2 className="big-spacer-bottom">
{translate('onboarding.create_project.github.warning.title')}
</h2>
<Alert variant="warning">
{canAdmin ? (
<FormattedMessage
id="onboarding.create_project.github.warning.message_admin"
defaultMessage={translate(
'onboarding.create_project.github.warning.message_admin'
)}
values={{
link: (
<Link to="/admin/settings?category=almintegration">
{translate('onboarding.create_project.github.warning.message_admin.link')}
</Link>
),
}}
/>
) : (
translate('onboarding.create_project.github.warning.message')
)}
</Alert>
</div>
</div>
<FlagMessage variant="warning" className="sw-my-2">
<span>
{canAdmin ? (
<FormattedMessage
id="onboarding.create_project.github.warning.message_admin"
defaultMessage={translate('onboarding.create_project.github.warning.message_admin')}
values={{
link: (
<Link to="/admin/settings?category=almintegration">
{translate('onboarding.create_project.github.warning.message_admin.link')}
</Link>
),
}}
/>
) : (
translate('onboarding.create_project.github.warning.message')
)}
</span>
</FlagMessage>
)}

{!error && (
<DeferredSpinner loading={loadingOrganizations}>
<div className="form-field">
<label htmlFor="github-choose-organization">
<DeferredSpinner loading={loadingOrganizations && !error}>
{!error && (
<div className="sw-flex sw-flex-col">
<DarkLabel htmlFor="github-choose-organization" className="sw-mb-2">
{translate('onboarding.create_project.github.choose_organization')}
</label>
</DarkLabel>
{organizations.length > 0 ? (
<Select
<InputSelect
className="sw-w-abs-300 sw-mb-9"
size="full"
isSearchable
inputId="github-choose-organization"
className="input-super-large"
options={organizations.map(orgToOption)}
onChange={({ value }: LabelValueSelectOption) => props.onSelectOrganization(value)}
value={selectedOrganization ? orgToOption(selectedOrganization) : null}
/>
) : (
!loadingOrganizations && (
<Alert className="spacer-top" variant="error">
{canAdmin ? (
<FormattedMessage
id="onboarding.create_project.github.no_orgs_admin"
defaultMessage={translate('onboarding.create_project.github.no_orgs_admin')}
values={{
link: (
<Link to="/admin/settings?category=almintegration">
{translate(
'onboarding.create_project.github.warning.message_admin.link'
)}
</Link>
),
}}
/>
) : (
translate('onboarding.create_project.github.no_orgs')
)}
</Alert>
<FlagMessage variant="error" className="sw-mb-2">
<span>
{canAdmin ? (
<FormattedMessage
id="onboarding.create_project.github.no_orgs_admin"
defaultMessage={translate('onboarding.create_project.github.no_orgs_admin')}
values={{
link: (
<Link to="/admin/settings?category=almintegration">
{translate(
'onboarding.create_project.github.warning.message_admin.link'
)}
</Link>
),
}}
/>
) : (
translate('onboarding.create_project.github.no_orgs')
)}
</span>
</FlagMessage>
)
)}
</div>
</DeferredSpinner>
)}
)}
</DeferredSpinner>

{renderRepositoryList(props)}
</div>
</>
);
}

+ 6
- 13
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHub-it.tsx View File

@@ -106,8 +106,8 @@ it('should show import project feature when the authentication is successfull',
expect(screen.getByText('Github repo 1')).toBeInTheDocument();
expect(screen.getByText('Github repo 2')).toBeInTheDocument();

repoItem = screen.getByRole('radio', {
name: 'Github repo 1',
repoItem = screen.getByRole('row', {
name: 'Github repo 1 onboarding.create_project.see_on_github onboarding.create_project.repository_imported',
});

expect(
@@ -120,16 +120,11 @@ it('should show import project feature when the authentication is successfull',
'/dashboard?id=key123'
);

repoItem = screen.getByRole('radio', {
name: 'Github repo 2',
repoItem = screen.getByRole('row', {
name: 'Github repo 2 onboarding.create_project.see_on_github onboarding.create_project.import',
});

const importButton = screen.getByText('onboarding.create_project.import_selected_repo');

expect(repoItem).toBeInTheDocument();
expect(importButton).toBeDisabled();
await user.click(repoItem);
expect(importButton).toBeEnabled();
const importButton = screen.getByText('onboarding.create_project.import');
await user.click(importButton);

expect(
@@ -154,9 +149,7 @@ it('should show search filter when the authentication is successful', async () =

await selectEvent.select(ui.organizationSelector.get(), [/org-1/]);

const inputSearch = screen.getByRole('searchbox', {
name: 'onboarding.create_project.search_repositories',
});
const inputSearch = screen.getByRole('searchbox');
await user.click(inputSearch);
await user.keyboard('search');


+ 4
- 3
server/sonar-web/src/main/js/apps/create/project/components/AlmRepoItem.tsx View File

@@ -58,9 +58,10 @@ export default function AlmRepoItem({
return (
<StyledCard
key={almKey}
role="row"
className={classNames('sw-flex sw-mb-2 sw-px-4', {
'sw-py-4': sqProjectKey,
'sw-py-2': !sqProjectKey,
'sw-py-4': sqProjectKey !== undefined,
'sw-py-2': sqProjectKey === undefined,
})}
>
<div className="sw-w-[70%] sw-flex sw-mr-1">
@@ -87,7 +88,7 @@ export default function AlmRepoItem({
</div>
</div>
<div className="sw-flex sw-justify-between sw-items-center sw-flex-1">
{almUrl && (
{almUrl !== undefined && (
<div className="sw-flex sw-items-center">
<Link
className="sw-body-sm-highlight"

+ 6
- 4
server/sonar-web/src/main/js/apps/create/project/components/AlmSettingsInstanceDropdown.tsx View File

@@ -17,6 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { DarkLabel } from 'design-system';
import * as React from 'react';
import AlmSettingsInstanceSelector from '../../../../components/devops-platform/AlmSettingsInstanceSelector';
import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n';
@@ -42,15 +44,15 @@ export default function AlmSettingsInstanceDropdown(props: AlmSettingsInstanceDr
: `alm.${almKey}`;

return (
<div className="display-flex-column huge-spacer-bottom">
<label htmlFor="alm-config-selector" className="spacer-bottom">
<div className="sw-flex sw-flex-col">
<DarkLabel htmlFor="alm-config-selector" className="sw-mb-2">
{translateWithParameters('alm.configuration.selector.label', translate(almKeyTranslation))}
</label>
</DarkLabel>
<AlmSettingsInstanceSelector
instances={almInstances}
onChange={props.onChangeConfig}
initialValue={selectedAlmInstance ? selectedAlmInstance.key : undefined}
classNames="abs-width-400"
className="sw-w-abs-400 sw-mb-9"
inputId="alm-config-selector"
/>
</div>

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

@@ -31,8 +31,8 @@ import { translate } from '../../../../helpers/l10n';
import { getGlobalSettingsUrl } from '../../../../helpers/urls';
import {
AlmSettingsInstance,
ProjectAlmBindingConfigurationErrors,
ProjectAlmBindingConfigurationErrorScope,
ProjectAlmBindingConfigurationErrors,
ProjectAlmBindingResponse,
} from '../../../../types/alm-settings';
import { ALM_INTEGRATION_CATEGORY } from '../../constants';
@@ -135,7 +135,7 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe
instances={instances}
onChange={(instance: AlmSettingsInstance) => props.onFieldChange('key', instance.key)}
initialValue={formData.key}
classNames="abs-width-400 big-spacer-top it__configuration-name-select"
className="sw-w-abs-400 sw-mt-4 it__configuration-name-select"
inputId="name"
/>
</div>

+ 25
- 17
server/sonar-web/src/main/js/components/devops-platform/AlmSettingsInstanceSelector.tsx View File

@@ -17,18 +17,23 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { InputSelect, LabelValueSelectOption } from 'design-system';
import * as React from 'react';
import { components, OptionProps, SingleValueProps } from 'react-select';
import { OptionProps, SingleValueProps, components } from 'react-select';
import { translate } from '../../helpers/l10n';
import { AlmSettingsInstance } from '../../types/alm-settings';
import Select from '../controls/Select';

function optionRenderer(props: OptionProps<AlmSettingsInstance, false>) {
return <components.Option {...props}>{customOptions(props.data)}</components.Option>;
function optionRenderer(props: OptionProps<LabelValueSelectOption<AlmSettingsInstance>, false>) {
return <components.Option {...props}>{customOptions(props.data.value)}</components.Option>;
}

function singleValueRenderer(props: SingleValueProps<AlmSettingsInstance, false>) {
return <components.SingleValue {...props}>{customOptions(props.data)}</components.SingleValue>;
function singleValueRenderer(
props: SingleValueProps<LabelValueSelectOption<AlmSettingsInstance>, false>
) {
return (
<components.SingleValue {...props}>{customOptions(props.data.value)}</components.SingleValue>
);
}

function customOptions(instance: AlmSettingsInstance) {
@@ -42,36 +47,39 @@ function customOptions(instance: AlmSettingsInstance) {
);
}

function orgToOption(alm: AlmSettingsInstance) {
return { value: alm, label: alm.key };
}

interface Props {
instances: AlmSettingsInstance[];
initialValue?: string;
onChange: (instance: AlmSettingsInstance) => void;
classNames: string;
className: string;
inputId: string;
}

export default function AlmSettingsInstanceSelector(props: Props) {
const { instances, initialValue, classNames, inputId } = props;
const { instances, initialValue, className, inputId } = props;

return (
<Select
<InputSelect
inputId={inputId}
className={classNames}
className={className}
isClearable={false}
isSearchable={false}
options={instances}
onChange={(inst) => {
if (inst) {
props.onChange(inst);
}
options={instances.map(orgToOption)}
onChange={(data: LabelValueSelectOption<AlmSettingsInstance>) => {
props.onChange(data.value);
}}
components={{
Option: optionRenderer,
SingleValue: singleValueRenderer,
}}
placeholder={translate('alm.configuration.selector.placeholder')}
getOptionValue={(opt) => opt.key}
value={instances.find((inst) => inst.key === initialValue) ?? null}
getOptionValue={(opt: LabelValueSelectOption<AlmSettingsInstance>) => opt.value.key}
value={instances.map(orgToOption).find((opt) => opt.value.key === initialValue) ?? null}
size="full"
/>
);
}

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

@@ -3965,10 +3965,10 @@ onboarding.create_project.bitbucketcloud.title=Bitbucket Cloud project onboardin
onboarding.create_project.bitbucketcloud.no_projects=No projects could be fetched from Bitbucket. Contact your system administrator, or {link}.
onboarding.create_project.bitbucketcloud.link=See on Bitbucket
onboarding.create_project.github.title=GitHub project onboarding
onboarding.create_project.github.subtitle=Import repositories from one of your GitHub organizations
onboarding.create_project.github.choose_organization=Choose organization
onboarding.create_project.github.warning.title=Could not connect to GitHub
onboarding.create_project.github.warning.message=Please contact an administrator to configure GitHub integration.
onboarding.create_project.github.warning.message_admin=Please make sure the GitHub instance is correctly configured in the {link} to create a new project from a repository.
onboarding.create_project.github.warning.message=Could not connect to GitHub. Please contact an administrator to configure GitHub integration.
onboarding.create_project.github.warning.message_admin=Could not connect to GitHub. Please make sure the GitHub instance is correctly configured in the {link} to create a new project from a repository.
onboarding.create_project.github.warning.message_admin.link=DevOps Platform integration settings
onboarding.create_project.github.no_orgs=We couldn't load any organizations with your key. Contact an administrator.
onboarding.create_project.github.no_orgs_admin=We couldn't load any organizations. Make sure the GitHub App is installed in at least one organization and check the GitHub instance configuration in the {link}.

Loading…
Cancel
Save