color: ${themeColor('pageContentLight')};
`;
+export const DarkLabel = styled.label`
+ color: ${themeColor('pageContentDark')};
+
+ ${tw`sw-body-sm-highlight`}
+`;
+
export const LightPrimary = styled.span`
color: ${themeContrast('primaryLight')};
`;
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;
repositories: GithubRepository[];
searchQuery: string;
selectedOrganization?: GithubOrganization;
- selectedRepository?: GithubRepository;
selectedAlmInstance?: AlmSettingsInstance;
}
triggerSearch = (query: string) => {
const { selectedOrganization } = this.state;
if (selectedOrganization) {
- this.setState({ selectedRepository: undefined });
this.fetchRepositories({ organizationKey: selectedOrganization.key, query });
}
};
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);
}
};
- 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,
})
);
}
repositories,
searchQuery,
selectedOrganization,
- selectedRepository,
selectedAlmInstance,
} = this.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}
*/
/* 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;
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;
}
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>}
+ />
))
)}
total={repositoryPaging.total}
loadMore={props.onLoadMore}
loading={loadingRepositories}
+ useMIUIButtons
/>
</div>
</div>
loadingOrganizations,
organizations,
selectedOrganization,
- selectedRepository,
almInstances,
selectedAlmInstance,
} = props;
}
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}
/>
{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>
+ </>
);
}
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(
'/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(
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');
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">
</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"
* 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';
: `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>
import { getGlobalSettingsUrl } from '../../../../helpers/urls';
import {
AlmSettingsInstance,
- ProjectAlmBindingConfigurationErrors,
ProjectAlmBindingConfigurationErrorScope,
+ ProjectAlmBindingConfigurationErrors,
ProjectAlmBindingResponse,
} from '../../../../types/alm-settings';
import { ALM_INTEGRATION_CATEGORY } from '../../constants';
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>
* 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) {
);
}
+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"
/>
);
}
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}.