Merge branch 'main' into add-V1-schema

This commit is contained in:
Vardan Bansal 2023-09-13 14:50:43 -07:00
commit cd07a34559
18 changed files with 1337 additions and 223 deletions

View File

@ -0,0 +1,191 @@
import React, { useState } from 'react'
import { Intent } from '@blueprintjs/core'
import * as yup from 'yup'
import { Button, Container, Layout, FlexExpander, Formik, FormikForm, FormInput, Text } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { FontVariation } from '@harnessio/design-system'
import { useStrings } from 'framework/strings'
import { REGEX_VALID_REPO_NAME } from 'utils/Utils'
import { ImportFormData, RepoVisibility, parseUrl } from 'utils/GitUtils'
import css from '../NewRepoModalButton.module.scss'
interface ImportFormProps {
handleSubmit: (data: ImportFormData) => void
loading: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any
hideModal: any
}
const ImportForm = (props: ImportFormProps) => {
const { handleSubmit, loading, hideModal } = props
const { getString } = useStrings()
const [auth, setAuth] = useState(false)
// eslint-disable-next-line no-control-regex
const MATCH_REPOURL_REGEX = /^(https?:\/\/(?:www\.)?(github|gitlab)\.com\/([^/]+\/[^/]+))/
const formInitialValues: ImportFormData = {
repoUrl: '',
username: '',
password: '',
name: '',
description: '',
isPublic: RepoVisibility.PRIVATE
}
return (
<Formik
initialValues={formInitialValues}
formName="editVariations"
validationSchema={yup.object().shape({
repoUrl: yup
.string()
.matches(MATCH_REPOURL_REGEX, getString('importSpace.invalidUrl'))
.required(getString('importRepo.required')),
name: yup
.string()
.trim()
.required(getString('validation.nameIsRequired'))
.matches(REGEX_VALID_REPO_NAME, getString('validation.repoNamePatternIsNotValid')),
username: yup.string().trim().required(getString('importRepo.usernameReq')),
password: yup.string().trim().required(getString('importRepo.passwordReq'))
})}
onSubmit={handleSubmit}>
{formik => {
return (
<FormikForm>
<FormInput.Text
name="repoUrl"
label={getString('importRepo.url')}
placeholder={getString('importRepo.urlPlaceholder')}
tooltipProps={{
dataTooltipId: 'repositoryURLTextField'
}}
onChange={event => {
const target = event.target as HTMLInputElement
formik.setFieldValue('repoUrl', target.value)
if (target.value) {
const provider = parseUrl(target.value)
if (provider?.fullRepo) {
formik.setFieldValue('name', provider.repoName ? provider.repoName : provider?.fullRepo)
formik.validateField('repoUrl')
}
}
}}
/>
<FormInput.CheckBox
name="authorization"
label={getString('importRepo.reqAuth')}
tooltipProps={{
dataTooltipId: 'authorization'
}}
onClick={() => {
setAuth(!auth)
}}
/>
{auth ? (
<>
<FormInput.Text
name="username"
label={getString('userName')}
placeholder={getString('importRepo.userPlaceholder')}
tooltipProps={{
dataTooltipId: 'repositoryUserTextField'
}}
/>
<FormInput.Text
inputGroup={{ type: 'password' }}
name="password"
label={getString('importRepo.passToken')}
placeholder={getString('importRepo.passwordPlaceholder')}
tooltipProps={{
dataTooltipId: 'repositoryPasswordTextField'
}}
/>
</>
) : null}
<hr className={css.dividerContainer} />
<FormInput.Text
name="name"
label={getString('name')}
placeholder={getString('enterRepoName')}
tooltipProps={{
dataTooltipId: 'repositoryNameTextField'
}}
onChange={() => {
formik.validateField('repoUrl')
}}
/>
<FormInput.Text
name="description"
label={getString('description')}
placeholder={getString('enterDescription')}
tooltipProps={{
dataTooltipId: 'repositoryDescriptionTextField'
}}
/>
<hr className={css.dividerContainer} />
<Container>
<FormInput.RadioGroup
name="isPublic"
label=""
items={[
{
label: (
<Container>
<Layout.Horizontal>
<Icon name="git-clone-step" size={20} margin={{ right: 'medium' }} />
<Container>
<Layout.Vertical spacing="xsmall">
<Text>{getString('public')}</Text>
<Text font={{ variation: FontVariation.TINY }}>
{getString('createRepoModal.publicLabel')}
</Text>
</Layout.Vertical>
</Container>
</Layout.Horizontal>
</Container>
),
value: RepoVisibility.PUBLIC
},
{
label: (
<Container>
<Layout.Horizontal>
<Icon name="git-clone-step" size={20} margin={{ right: 'medium' }} />
<Container margin={{ left: 'small' }}>
<Layout.Vertical spacing="xsmall">
<Text>{getString('private')}</Text>
<Text font={{ variation: FontVariation.TINY }}>
{getString('createRepoModal.privateLabel')}
</Text>
</Layout.Vertical>
</Container>
</Layout.Horizontal>
</Container>
),
value: RepoVisibility.PRIVATE
}
]}
/>
</Container>
<Layout.Horizontal
spacing="small"
padding={{ right: 'xxlarge', top: 'xlarge', bottom: 'large' }}
style={{ alignItems: 'center' }}>
<Button type="submit" text={getString('importRepo.title')} intent={Intent.PRIMARY} disabled={loading} />
<Button text={getString('cancel')} minimal onClick={hideModal} />
<FlexExpander />
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
</Layout.Horizontal>
</FormikForm>
)
}}
</Formik>
)
}
export default ImportForm

View File

@ -1,3 +1,29 @@
.divider {
margin: var(--spacing-medium) 0 var(--spacing-large) 0 !important;
}
.dividerContainer {
opacity: 0.2;
height: 1px;
color: var(--grey-100);
margin: 20px 0;
}
.popover {
transform: translateY(5px) !important;
.menuItem {
strong {
display: inline-block;
margin-left: 10px;
}
p {
font-size: 12px;
padding: 0 var(--spacing-xlarge);
line-height: 16px;
margin: 5px 0;
max-width: 320px;
}
}
}

View File

@ -1,3 +1,6 @@
/* eslint-disable */
// This is an auto-generated file
export declare const divider: string
export declare const dividerContainer: string
export declare const menuItem: string
export declare const popover: string

View File

@ -1,5 +1,14 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Icon as BPIcon, Classes, Dialog, Intent, Menu, MenuDivider, MenuItem } from '@blueprintjs/core'
import {
Icon as BPIcon,
Classes,
Dialog,
Intent,
Menu,
MenuDivider,
MenuItem,
PopoverPosition
} from '@blueprintjs/core'
import * as yup from 'yup'
import {
Button,
@ -15,7 +24,8 @@ import {
Text,
ButtonVariation,
ButtonSize,
TextInput
TextInput,
SplitButton
} from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { FontVariation } from '@harnessio/design-system'
@ -30,26 +40,19 @@ import {
REGEX_VALID_REPO_NAME,
SUGGESTED_BRANCH_NAMES
} from 'utils/Utils'
import { isGitBranchNameValid } from 'utils/GitUtils'
import {
ImportFormData,
RepoCreationType,
RepoFormData,
RepoVisibility,
isGitBranchNameValid,
parseUrl
} from 'utils/GitUtils'
import type { TypesRepository, OpenapiCreateRepositoryRequest } from 'services/code'
import { useAppContext } from 'AppContext'
import ImportForm from './ImportForm/ImportForm'
import css from './NewRepoModalButton.module.scss'
enum RepoVisibility {
PUBLIC = 'public',
PRIVATE = 'private'
}
interface RepoFormData {
name: string
description: string
license: string
defaultBranch: string
gitignore: string
addReadme: boolean
isPublic: RepoVisibility
}
const formInitialValues: RepoFormData = {
name: '',
description: '',
@ -90,6 +93,15 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
space_path: space
}
})
const { mutate: importRepo, loading: importRepoLoading } = useMutate<TypesRepository>({
verb: 'POST',
path: `/api/v1/repos/import`,
queryParams: standalone
? undefined
: {
space_path: space
}
})
const {
data: gitignores,
loading: gitIgnoreLoading,
@ -100,14 +112,13 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
loading: licenseLoading,
error: licenseError
} = useGet({ path: '/api/v1/resources/license' })
const loading = submitLoading || gitIgnoreLoading || licenseLoading
const loading = submitLoading || gitIgnoreLoading || licenseLoading || importRepoLoading
useEffect(() => {
if (gitIgnoreError || licenseError) {
showError(getErrorMessage(gitIgnoreError || licenseError), 0)
}
}, [gitIgnoreError, licenseError, showError])
const handleSubmit = (formData: RepoFormData) => {
try {
const payload: OpenapiCreateRepositoryRequest = {
@ -132,7 +143,24 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
showError(getErrorMessage(exception), 0, getString('failedToCreateRepo'))
}
}
const handleImportSubmit = (formData: ImportFormData) => {
const provider = parseUrl(formData.repoUrl)
const importPayload = {
description: formData.description || '',
parent_ref: space,
uid: formData.name,
provider: { type: provider?.provider.toLowerCase(), username: formData.username, password: formData.password },
provider_repo: provider?.fullRepo
}
importRepo(importPayload)
.then(response => {
hideModal()
onSubmit(response)
})
.catch(_error => {
showError(getErrorMessage(_error), 0, getString('importRepo.failedToImportRepo'))
})
}
return (
<Dialog
isOpen
@ -145,147 +173,166 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
style={{ height: '100%' }}
data-testid="add-target-to-flag-modal">
<Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'xlarge' }}>
{modalTitle}
{repoOption.type === RepoCreationType.IMPORT ? getString('importRepo.title') : modalTitle}
</Heading>
<Container margin={{ right: 'xxlarge' }}>
<Formik
initialValues={formInitialValues}
formName="editVariations"
enableReinitialize={true}
validationSchema={yup.object().shape({
name: yup
.string()
.trim()
.required()
.matches(REGEX_VALID_REPO_NAME, getString('validation.repoNamePatternIsNotValid'))
})}
validateOnChange
validateOnBlur
onSubmit={handleSubmit}>
<FormikForm>
<FormInput.Text
name="name"
label={getString('name')}
placeholder={getString('enterRepoName')}
tooltipProps={{
dataTooltipId: 'repositoryNameTextField'
}}
inputGroup={{ autoFocus: true }}
/>
<FormInput.Text
name="description"
label={getString('description')}
placeholder={getString('enterDescription')}
tooltipProps={{
dataTooltipId: 'repositoryDescriptionTextField'
}}
/>
<Container margin={{ top: 'medium', bottom: 'medium' }}>
<Text>
{getString('createRepoModal.branchLabel')}
<strong>
<Button
text={branchName}
icon="git-new-branch"
rightIcon="chevron-down"
variation={ButtonVariation.TERTIARY}
size={ButtonSize.SMALL}
iconProps={{ size: 14 }}
tooltip={<BranchName currentBranchName={branchName} onSelect={name => setBranchName(name)} />}
tooltipProps={{ interactionKind: 'click' }}
/>
</strong>
{getString('createRepoModal.branch')}
</Text>
</Container>
<Container>
<FormInput.RadioGroup
name="isPublic"
label=""
items={[
{
label: (
<Container>
<Layout.Horizontal>
<Icon name="git-clone-step" size={20} margin={{ right: 'medium' }} />
<Container>
<Layout.Vertical spacing="xsmall">
<Text>{getString('public')}</Text>
<Text font={{ variation: FontVariation.TINY }}>
{getString('createRepoModal.publicLabel')}
</Text>
</Layout.Vertical>
</Container>
</Layout.Horizontal>
</Container>
),
value: RepoVisibility.PUBLIC
},
{
label: (
<Container>
<Layout.Horizontal>
<Icon name="git-clone-step" size={20} margin={{ right: 'medium' }} />
<Container margin={{ left: 'small' }}>
<Layout.Vertical spacing="xsmall">
<Text>{getString('private')}</Text>
<Text font={{ variation: FontVariation.TINY }}>
{getString('createRepoModal.privateLabel')}
</Text>
</Layout.Vertical>
</Container>
</Layout.Horizontal>
</Container>
),
value: RepoVisibility.PRIVATE
}
]}
{repoOption.type === RepoCreationType.IMPORT ? (
<ImportForm hideModal={hideModal} handleSubmit={handleImportSubmit} loading={false} />
) : (
<Formik
initialValues={formInitialValues}
formName="editVariations"
enableReinitialize={true}
validationSchema={yup.object().shape({
name: yup
.string()
.trim()
.required()
.matches(REGEX_VALID_REPO_NAME, getString('validation.repoNamePatternIsNotValid'))
})}
validateOnChange
validateOnBlur
onSubmit={handleSubmit}>
<FormikForm>
<FormInput.Text
name="name"
label={getString('name')}
placeholder={getString('enterRepoName')}
tooltipProps={{
dataTooltipId: 'repositoryNameTextField'
}}
inputGroup={{ autoFocus: true }}
/>
</Container>
<FormInput.Text
name="description"
label={getString('description')}
placeholder={getString('enterDescription')}
tooltipProps={{
dataTooltipId: 'repositoryDescriptionTextField'
}}
/>
<Container margin={{ top: 'medium', bottom: 'medium' }}>
<Text>
{getString('createRepoModal.branchLabel')}
<strong>
<Button
text={branchName}
icon="git-new-branch"
rightIcon="chevron-down"
variation={ButtonVariation.TERTIARY}
size={ButtonSize.SMALL}
iconProps={{ size: 14 }}
tooltip={<BranchName currentBranchName={branchName} onSelect={name => setBranchName(name)} />}
tooltipProps={{ interactionKind: 'click' }}
/>
</strong>
{getString('createRepoModal.branch')}
</Text>
</Container>
<Container>
<FormInput.RadioGroup
name="isPublic"
label=""
items={[
{
label: (
<Container>
<Layout.Horizontal>
<Icon name="git-clone-step" size={20} margin={{ right: 'medium' }} />
<Container>
<Layout.Vertical spacing="xsmall">
<Text>{getString('public')}</Text>
<Text font={{ variation: FontVariation.TINY }}>
{getString('createRepoModal.publicLabel')}
</Text>
</Layout.Vertical>
</Container>
</Layout.Horizontal>
</Container>
),
value: RepoVisibility.PUBLIC
},
{
label: (
<Container>
<Layout.Horizontal>
<Icon name="git-clone-step" size={20} margin={{ right: 'medium' }} />
<Container margin={{ left: 'small' }}>
<Layout.Vertical spacing="xsmall">
<Text>{getString('private')}</Text>
<Text font={{ variation: FontVariation.TINY }}>
{getString('createRepoModal.privateLabel')}
</Text>
</Layout.Vertical>
</Container>
</Layout.Horizontal>
</Container>
),
value: RepoVisibility.PRIVATE
}
]}
/>
</Container>
<FormInput.Select
name="license"
label={getString('addLicense')}
placeholder={getString('none')}
items={licences || []}
usePortal
/>
<FormInput.Select
name="license"
label={getString('addLicense')}
placeholder={getString('none')}
items={licences || []}
usePortal
/>
<FormInput.Select
name="gitignore"
label={getString('addGitIgnore')}
placeholder={getString('none')}
items={(gitignores || []).map((entry: string) => ({ label: entry, value: entry }))}
usePortal
/>
<FormInput.Select
name="gitignore"
label={getString('addGitIgnore')}
placeholder={getString('none')}
items={(gitignores || []).map((entry: string) => ({ label: entry, value: entry }))}
usePortal
/>
<FormInput.CheckBox
name="addReadme"
label={getString('addReadMe')}
tooltipProps={{
dataTooltipId: 'addReadMe'
}}
/>
<Layout.Horizontal
spacing="small"
padding={{ right: 'xxlarge', top: 'xxxlarge', bottom: 'large' }}
style={{ alignItems: 'center' }}>
<Button type="submit" text={getString('createRepo')} intent={Intent.PRIMARY} disabled={loading} />
<Button text={cancelButtonTitle || getString('cancel')} minimal onClick={hideModal} />
<FlexExpander />
<FormInput.CheckBox
name="addReadme"
label={getString('addReadMe')}
tooltipProps={{
dataTooltipId: 'addReadMe'
}}
/>
<Layout.Horizontal
spacing="small"
padding={{ right: 'xxlarge', top: 'xxxlarge', bottom: 'large' }}
style={{ alignItems: 'center' }}>
<Button type="submit" text={getString('createRepo')} intent={Intent.PRIMARY} disabled={loading} />
<Button text={cancelButtonTitle || getString('cancel')} minimal onClick={hideModal} />
<FlexExpander />
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
</Layout.Horizontal>
</FormikForm>
</Formik>
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
</Layout.Horizontal>
</FormikForm>
</Formik>
)}
</Container>
</Layout.Vertical>
</Dialog>
)
}
const { getString } = useStrings()
const [openModal, hideModal] = useModalHook(ModalComponent, [onSubmit])
const repoCreateOptions: RepoCreationOption[] = [
{
type: RepoCreationType.CREATE,
title: getString('newRepo'),
desc: getString('createNewRepo')
},
{
type: RepoCreationType.IMPORT,
title: getString('importGitRepo'),
desc: getString('importGitRepo')
}
]
const [repoOption, setRepoOption] = useState<RepoCreationOption>(repoCreateOptions[0])
const [openModal, hideModal] = useModalHook(ModalComponent, [onSubmit, repoOption])
const { standalone } = useAppContext()
const { hooks } = useAppContext()
const permResult = hooks?.usePermissionTranslate?.(
@ -297,8 +344,48 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
},
[space]
)
return (
<SplitButton
{...props}
loading={false}
text={repoOption.title}
variation={ButtonVariation.PRIMARY}
popoverProps={{
interactionKind: 'click',
usePortal: true,
popoverClassName: css.popover,
position: PopoverPosition.BOTTOM_RIGHT,
transitionDuration: 1000
}}
icon={repoOption.type === RepoCreationType.IMPORT ? undefined : 'plus'}
{...permissionProps(permResult, standalone)}
onClick={() => {
openModal()
}}>
{repoCreateOptions.map(option => {
return (
<Menu.Item
key={option.type}
className={css.menuItem}
text={
<>
<p>{option.desc}</p>
</>
}
onClick={() => {
setRepoOption(option)
}}
/>
)
})}
</SplitButton>
)
}
return <Button onClick={openModal} {...props} {...permissionProps(permResult, standalone)} />
interface RepoCreationOption {
type: RepoCreationType
title: string
desc: string
}
interface BranchNameProps {

View File

@ -0,0 +1,320 @@
import React, { useState } from 'react'
import { Intent } from '@blueprintjs/core'
import * as yup from 'yup'
import { Color } from '@harnessio/design-system'
import { Button, Container, Label, Layout, FlexExpander, Formik, FormikForm, FormInput, Text } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { useStrings } from 'framework/strings'
import { Organization, type ImportSpaceFormData } from 'utils/GitUtils'
import css from '../NewSpaceModalButton.module.scss'
interface ImportFormProps {
handleSubmit: (data: ImportSpaceFormData) => void
loading: boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
hideModal: any
}
const ImportSpaceForm = (props: ImportFormProps) => {
const { handleSubmit, loading, hideModal } = props
const { getString } = useStrings()
const [auth, setAuth] = useState(false)
const [step, setStep] = useState(0)
const [buttonLoading, setButtonLoading] = useState(false)
const formInitialValues: ImportSpaceFormData = {
gitProvider: '',
username: '',
password: '',
name: '',
description: '',
organization: ''
}
const providers = [
{ value: 'Github', label: 'Github' },
{ value: 'Gitlab', label: 'Gitlab' }
]
const validationSchemaStepOne = yup.object().shape({
gitProvider: yup.string().trim().required(getString('importSpace.providerRequired')),
username: yup.string().trim().required(getString('importRepo.usernameReq')),
password: yup.string().trim().required(getString('importRepo.passwordReq'))
})
const validationSchemaStepTwo = yup.object().shape({
organization: yup.string().trim().required(getString('importSpace.orgRequired')),
name: yup.string().trim().required(getString('importSpace.spaceNameRequired'))
})
return (
<Formik
initialValues={formInitialValues}
formName="editVariations"
enableReinitialize={true}
validateOnBlur
onSubmit={handleSubmit}>
{formik => {
const { values } = formik
const handleValidationClick = async () => {
try {
if (step === 0) {
await validationSchemaStepOne.validate(formik.values, { abortEarly: false })
setStep(1)
} else if (step === 1) {
await validationSchemaStepTwo.validate(formik.values, { abortEarly: false })
setButtonLoading(true)
} // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
formik.setErrors(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
err.inner.reduce((acc: { [x: string]: any }, current: { path: string | number; message: string }) => {
acc[current.path] = current.message
return acc
}, {})
)
}
}
const handleImport = async () => {
console.log('ds')
await handleSubmit(formik.values)
console.log('pos')
setButtonLoading(false)
}
return (
<Container width={'97%'}>
<FormikForm>
{step === 0 ? (
<>
<Container width={'70%'}>
<Layout.Horizontal>
<Icon className={css.icon} name="code-info" size={16} />
<Text padding={{ left: 'small' }} font={{ size: 'small' }}>
{getString('importSpace.content')}
</Text>
</Layout.Horizontal>
</Container>
<hr className={css.dividerContainer} />
<Container className={css.textContainer} width={'70%'}>
<FormInput.Select
value={{ value: values.gitProvider, label: values.gitProvider } || providers[0]}
name={'gitProvider'}
label={getString('importSpace.gitProvider')}
items={providers}
className={css.selectBox}
/>
{formik.errors.gitProvider ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.gitProvider}
</Text>
) : null}
<Layout.Horizontal flex>
{getString('importSpace.authorization')}
<Container padding={{ left: 'small' }} width={'100%'}>
<hr className={css.dividerContainer} />
</Container>
</Layout.Horizontal>
<FormInput.Text
name="username"
label={getString('userName')}
placeholder={getString('importRepo.userPlaceholder')}
tooltipProps={{
dataTooltipId: 'spaceUserTextField'
}}
/>
{formik.errors.username ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.username}
</Text>
) : null}
<FormInput.Text
name="password"
label={getString('importRepo.passToken')}
placeholder={getString('importRepo.passwordPlaceholder')}
tooltipProps={{
dataTooltipId: 'spacePasswordTextField'
}}
inputGroup={{ type: 'password' }}
/>
{formik.errors.password ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.password}
</Text>
) : null}
</Container>
</>
) : null}
{step === 1 ? (
<>
<Layout.Horizontal flex>
{/* <Container className={css.detailsLabel}> */}
<Text className={css.detailsLabel} font={{ size: 'small' }} flex>
{getString('importSpace.details')}
</Text>
{/* </Container> */}
<Container padding={{ left: 'small' }} width={'100%'}>
<hr className={css.dividerContainer} />
</Container>
</Layout.Horizontal>
<Container className={css.textContainer} width={'70%'}>
<FormInput.Text
name="organization"
label={
formik.values.gitProvider === Organization.GITHUB
? getString('importSpace.githubOrg')
: getString('importSpace.gitlabGroup')
}
placeholder={getString('importSpace.orgNamePlaceholder')}
tooltipProps={{
dataTooltipId: 'importSpaceOrgName'
}}
/>
{formik.errors.organization ? (
<Text
margin={{ bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.organization}
</Text>
) : null}
<Layout.Horizontal>
<Label>{getString('importSpace.importLabel')}</Label>
<Icon padding={{ left: 'small' }} className={css.icon} name="code-info" size={16} />
</Layout.Horizontal>
<Container className={css.importContainer} padding={'medium'}>
<Layout.Horizontal>
<FormInput.CheckBox
name="repositories"
label={getString('pageTitle.repositories')}
tooltipProps={{
dataTooltipId: 'authorization'
}}
defaultChecked
onClick={() => {
setAuth(!auth)
}}
disabled
padding={{ right: 'small' }}
className={css.checkbox}
/>
<Container padding={{ left: 'xxxlarge' }}>
<FormInput.CheckBox
name="pipelines"
label={getString('pageTitle.pipelines')}
tooltipProps={{
dataTooltipId: 'pipelines'
}}
onClick={() => {
setAuth(!auth)
}}
/>
</Container>
</Layout.Horizontal>
</Container>
<Container>
<hr className={css.dividerContainer} />
<FormInput.Text
name="name"
label={getString('importSpace.spaceName')}
placeholder={getString('enterName')}
tooltipProps={{
dataTooltipId: 'importSpaceName'
}}
/>
{formik.errors.name ? (
<Text
margin={{ bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.name}
</Text>
) : null}
<FormInput.Text
name="description"
label={getString('importSpace.description')}
placeholder={getString('importSpace.descPlaceholder')}
tooltipProps={{
dataTooltipId: 'importSpaceDesc'
}}
/>
</Container>
</Container>
</>
) : null}
<hr className={css.dividerContainer} />
<Layout.Horizontal
spacing="small"
padding={{ right: 'xxlarge', bottom: 'large' }}
style={{ alignItems: 'center' }}>
{step === 1 ? (
<Button
disabled={buttonLoading}
text={
buttonLoading ? (
<>
<Container className={css.loadingIcon} width={93.5} flex={{ alignItems: 'center' }}>
<Icon className={css.loadingIcon} name="steps-spinner" size={16} />
</Container>
</>
) : (
getString('importSpace.title')
)
}
intent={Intent.PRIMARY}
onClick={() => {
handleValidationClick()
if (formik.values.name !== '' || formik.values.organization !== '') {
handleImport()
setButtonLoading(false)
}
formik.setErrors({})
}}
/>
) : (
<Button
text={getString('importSpace.next')}
intent={Intent.PRIMARY}
onClick={() => {
handleValidationClick()
if (
(!formik.errors.gitProvider && formik.touched.gitProvider) ||
(!formik.errors.username && formik.touched.username) ||
(!formik.errors.password && formik.touched.password)
) {
formik.setErrors({})
setStep(1)
}
}}
/>
)}
<Button text={getString('cancel')} minimal onClick={hideModal} />
<FlexExpander />
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
</Layout.Horizontal>
</FormikForm>
</Container>
)
}}
</Formik>
)
}
export default ImportSpaceForm

View File

@ -1,3 +1,127 @@
.divider {
margin: var(--spacing-medium) 0 var(--spacing-large) 0 !important;
}
.popoverSplit {
:global {
.bp3-menu {
min-width: 248px;
max-width: 248px;
}
.bp3-menu-item {
min-width: 240px;
max-width: 240px;
}
.menuItem {
max-width: fit-content;
}
}
}
.test {
padding: 20px;
}
.dividerContainer {
opacity: 0.2;
height: 1px;
color: var(--grey-100);
margin: 20px 0;
}
.dividerContainer {
opacity: 0.2;
height: 1px;
color: var(--grey-100);
margin: 20px 0;
}
.halfDividerContainer {
opacity: 0.2;
height: 1px;
color: var(--grey-100);
margin-bottom: 20px;
}
.textContainer {
font-size: var(--font-size-small) !important;
--form-input-font-size: var(--font-size-small) !important;
.selectBox {
margin-bottom: unset !important;
}
}
.menuItem {
max-width: fit-content;
}
.importContainer {
background: var(--grey-50) !important;
border: 1px solid var(--grey-200) !important;
border-radius: 4px;
:global {
.bp3-form-group {
margin: unset !important;
}
}
}
.loadingIcon {
fill: var(--grey-0) !important;
color: var(--grey-0) !important;
:global {
.bp3-icon {
padding-left: 45px !important;
fill: var(--grey-0) !important;
color: var(--grey-0) !important;
}
}
}
.detailsLabel {
white-space: nowrap !important;
max-width: 155px !important;
color: var(--grey-600) !important;
}
.icon {
> svg {
fill: var(--primary-7) !important;
> path {
fill: var(--primary-7) !important;
}
}
}
.checkbox {
:global {
.Tooltip--acenter {
opacity: 0.7 !important;
}
.bp3-control-indicator {
background: var(--primary-7) !important;
opacity: 0.7;
}
}
}
.popoverSpace {
position: relative;
left: 33px;
:global {
.bp3-menu {
min-width: 162px;
max-width: 162px;
}
.bp3-menu-item {
min-width: 153px;
max-width: 153px;
}
.menuItem {
max-width: fit-content;
}
}
}

View File

@ -1,3 +1,16 @@
/* eslint-disable */
// This is an auto-generated file
export declare const checkbox: string
export declare const detailsLabel: string
export declare const divider: string
export declare const dividerContainer: string
export declare const halfDividerContainer: string
export declare const icon: string
export declare const importContainer: string
export declare const loadingIcon: string
export declare const menuItem: string
export declare const popoverSpace: string
export declare const popoverSplit: string
export declare const selectBox: string
export declare const test: string
export declare const textContainer: string

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Dialog, Intent } from '@blueprintjs/core'
import React, { useState } from 'react'
import { Dialog, Intent, PopoverPosition, Menu } from '@blueprintjs/core'
import * as yup from 'yup'
import {
Button,
@ -11,7 +11,9 @@ import {
FormikForm,
Heading,
useToaster,
FormInput
FormInput,
ButtonVariation,
SplitButton
} from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { FontVariation } from '@harnessio/design-system'
@ -19,16 +21,19 @@ import { useMutate } from 'restful-react'
import { get } from 'lodash-es'
import { useModalHook } from 'hooks/useModalHook'
import { useStrings } from 'framework/strings'
import { getErrorMessage, REGEX_VALID_REPO_NAME } from 'utils/Utils'
import { getErrorMessage, permissionProps, REGEX_VALID_REPO_NAME } from 'utils/Utils'
import type { TypesSpace, OpenapiCreateSpaceRequest } from 'services/code'
import { useAppContext } from 'AppContext'
import { ImportSpaceFormData, SpaceCreationType } from 'utils/GitUtils'
import ImportSpaceForm from './ImportSpaceForm/ImportSpaceForm'
import css from './NewSpaceModalButton.module.scss'
enum RepoVisibility {
PUBLIC = 'public',
PRIVATE = 'private'
}
interface RepoFormData {
interface SpaceFormData {
name: string
description: string
license: string
@ -38,7 +43,7 @@ interface RepoFormData {
isPublic: RepoVisibility
}
const formInitialValues: RepoFormData = {
const formInitialValues: SpaceFormData = {
name: '',
description: '',
license: '',
@ -48,13 +53,15 @@ const formInitialValues: RepoFormData = {
isPublic: RepoVisibility.PRIVATE
}
export interface NewSpaceModalButtonProps extends Omit<ButtonProps, 'onClick'> {
export interface NewSpaceModalButtonProps extends Omit<ButtonProps, 'onClick' | 'onSubmit'> {
space: string
modalTitle: string
submitButtonTitle?: string
cancelButtonTitle?: string
onRefetch: () => void
handleNavigation?: (value: string) => void
onSubmit: (data: TypesSpace) => void
fromSpace?: boolean
}
export interface OpenapiCreateSpaceRequestExtended extends OpenapiCreateSpaceRequest {
parent_id?: number
@ -67,6 +74,8 @@ export const NewSpaceModalButton: React.FC<NewSpaceModalButtonProps> = ({
cancelButtonTitle,
onRefetch,
handleNavigation,
onSubmit,
fromSpace = false,
...props
}) => {
const ModalComponent: React.FC = () => {
@ -78,10 +87,14 @@ export const NewSpaceModalButton: React.FC<NewSpaceModalButtonProps> = ({
verb: 'POST',
path: `/api/v1/spaces`
})
const { mutate: importSpace, loading: submitImportLoading } = useMutate<TypesSpace>({
verb: 'POST',
path: `/api/v1/spaces/import`
})
const loading = submitLoading
const loading = submitLoading || submitImportLoading
const handleSubmit = (formData: RepoFormData) => {
const handleSubmit = (formData: SpaceFormData) => {
try {
const payload: OpenapiCreateSpaceRequestExtended = {
description: get(formData, 'description', '').trim(),
@ -103,74 +116,179 @@ export const NewSpaceModalButton: React.FC<NewSpaceModalButtonProps> = ({
}
}
const handleImportSubmit = async (formData: ImportSpaceFormData) => {
try {
const importPayload = {
description: formData.description || '',
uid: formData.name,
provider: {
type: formData.gitProvider.toLowerCase(),
username: formData.username,
password: formData.password
},
provider_space: formData.organization
}
await importSpace(importPayload)
.then(response => {
hideModal()
onSubmit(response)
onRefetch()
})
.catch(_error => {
showError(getErrorMessage(_error), 0, getString('failedToImportSpace'))
})
} catch (exception) {
showError(getErrorMessage(exception), 0, getString('failedToImportSpace'))
}
}
return (
<Dialog
isOpen
enforceFocus={false}
onClose={hideModal}
enforceFocus={false}
title={''}
style={{ width: 700, maxHeight: '95vh', overflow: 'auto' }}>
style={{
width: spaceOption.type === SpaceCreationType.IMPORT ? 610 : 700,
maxHeight: '95vh',
overflow: 'auto'
}}>
<Layout.Vertical
padding={{ left: 'xxlarge' }}
style={{ height: '100%' }}
data-testid="add-target-to-flag-modal">
<Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'xlarge' }}>
{modalTitle}
<Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'large' }}>
{spaceOption.type === SpaceCreationType.IMPORT ? getString('importSpace.title') : modalTitle}
</Heading>
<Container margin={{ right: 'xxlarge' }}>
<Formik
initialValues={formInitialValues}
formName="editVariations"
enableReinitialize={true}
validationSchema={yup.object().shape({
name: yup
.string()
.trim()
.required()
.matches(REGEX_VALID_REPO_NAME, getString('validation.spaceNamePatternIsNotValid'))
})}
validateOnChange
validateOnBlur
onSubmit={handleSubmit}>
<FormikForm>
<FormInput.Text
name="name"
label={getString('name')}
placeholder={getString('enterName')}
tooltipProps={{
dataTooltipId: 'spaceNameTextField'
}}
inputGroup={{ autoFocus: true }}
/>
<FormInput.Text
name="description"
label={getString('description')}
placeholder={getString('enterDescription')}
tooltipProps={{
dataTooltipId: 'spaceDescriptionTextField'
}}
/>
{spaceOption.type === SpaceCreationType.IMPORT ? (
<ImportSpaceForm hideModal={hideModal} handleSubmit={handleImportSubmit} loading={false} />
) : (
<Formik
initialValues={formInitialValues}
formName="editVariations"
enableReinitialize={true}
validationSchema={yup.object().shape({
name: yup
.string()
.trim()
.required()
.matches(REGEX_VALID_REPO_NAME, getString('validation.spaceNamePatternIsNotValid'))
})}
validateOnChange
validateOnBlur
onSubmit={handleSubmit}>
<FormikForm>
<FormInput.Text
name="name"
label={getString('name')}
placeholder={getString('enterName')}
tooltipProps={{
dataTooltipId: 'spaceNameTextField'
}}
inputGroup={{ autoFocus: true }}
/>
<FormInput.Text
name="description"
label={getString('description')}
placeholder={getString('enterDescription')}
tooltipProps={{
dataTooltipId: 'spaceDescriptionTextField'
}}
/>
<Layout.Horizontal
spacing="small"
padding={{ right: 'xxlarge', top: 'xxxlarge', bottom: 'large' }}
style={{ alignItems: 'center' }}>
<Button type="submit" text={getString('createSpace')} intent={Intent.PRIMARY} disabled={loading} />
<Button text={cancelButtonTitle || getString('cancel')} minimal onClick={hideModal} />
<FlexExpander />
<Layout.Horizontal
spacing="small"
padding={{ right: 'xxlarge', top: 'xxxlarge', bottom: 'large' }}
style={{ alignItems: 'center' }}>
<Button type="submit" text={getString('createSpace')} intent={Intent.PRIMARY} disabled={loading} />
<Button text={cancelButtonTitle || getString('cancel')} minimal onClick={hideModal} />
<FlexExpander />
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
</Layout.Horizontal>
</FormikForm>
</Formik>
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
</Layout.Horizontal>
</FormikForm>
</Formik>
)}
</Container>
</Layout.Vertical>
</Dialog>
)
}
const [openModal, hideModal] = useModalHook(ModalComponent)
const { getString } = useStrings()
return <Button onClick={openModal} {...props} />
const spaceCreateOptions: SpaceCreationOption[] = [
{
type: SpaceCreationType.CREATE,
title: fromSpace ? getString('newSpace') : getString('createSpace'),
desc: getString('importSpace.createNewSpace')
},
{
type: SpaceCreationType.IMPORT,
title: getString('importSpace.title'),
desc: getString('importSpace.title')
}
]
const [spaceOption, setSpaceOption] = useState<SpaceCreationOption>(spaceCreateOptions[0])
const [openModal, hideModal] = useModalHook(ModalComponent, [onSubmit, spaceOption])
const { standalone } = useAppContext()
const { hooks } = useAppContext()
const permResult = hooks?.usePermissionTranslate?.(
{
resource: {
resourceType: 'CODE_REPOSITORY'
},
permissions: ['code_repo_push']
},
[space]
)
return (
<SplitButton
{...props}
loading={false}
text={spaceOption.title}
variation={ButtonVariation.PRIMARY}
popoverProps={{
interactionKind: 'click',
usePortal: true,
popoverClassName: fromSpace ? css.popoverSpace : css.popoverSplit,
position: PopoverPosition.BOTTOM_RIGHT,
transitionDuration: 1000
}}
icon={'plus'}
{...permissionProps(permResult, standalone)}
onClick={() => {
openModal()
}}>
{spaceCreateOptions.map(option => {
return (
<Container key={`import_space_container_${option.type}`}>
<Menu.Item
key={`import_space_${option.type}`}
className={css.menuItem}
text={
<>
<p>{option.desc}</p>
</>
}
onClick={event => {
event.stopPropagation()
event.preventDefault()
setSpaceOption(option)
}}
/>
</Container>
)
})}
</SplitButton>
)
}
interface SpaceCreationOption {
type: SpaceCreationType
title: string
desc: string
}

View File

@ -92,6 +92,11 @@ export const SpaceSelector: React.FC<SpaceSelectorProps> = ({ onSelect }) => {
variation={ButtonVariation.PRIMARY}
icon="plus"
onRefetch={voidFn(refetch)}
onSubmit={spaceData => {
history.push(routes.toCODERepositories({ space: spaceData.path as string }))
setOpened(false)
}}
fromSpace={true}
/>
)
@ -213,6 +218,10 @@ export const SpaceSelector: React.FC<SpaceSelectorProps> = ({ onSelect }) => {
variation={ButtonVariation.PRIMARY}
icon="plus"
onRefetch={voidFn(refetch)}
onSubmit={spaceData => {
history.push(routes.toCODERepositories({ space: spaceData.path as string }))
}}
fromSpace={true}
/>
}
message={<Text font={{ variation: FontVariation.H4 }}> {getString('emptySpaceText')}</Text>}

View File

@ -105,6 +105,7 @@ export interface StringsMap {
createBranchFromBranch: string
createBranchFromTag: string
createFile: string
createNewRepo: string
createNewToken: string
createNewUser: string
createPullRequest: string
@ -194,6 +195,7 @@ export interface StringsMap {
failedToDeleteBranch: string
failedToDeleteWebhook: string
failedToFetchFileContent: string
failedToImportSpace: string
failedToSavePipeline: string
fileDeleted: string
fileTooLarge: string
@ -215,6 +217,40 @@ export interface StringsMap {
history: string
'homepage.firstStep': string
'homepage.welcomeText': string
importGitRepo: string
importProgress: string
'importRepo.failedToImportRepo': string
'importRepo.passToken': string
'importRepo.passwordPlaceholder': string
'importRepo.passwordReq': string
'importRepo.reqAuth': string
'importRepo.required': string
'importRepo.spaceNameReq': string
'importRepo.title': string
'importRepo.url': string
'importRepo.urlPlaceholder': string
'importRepo.userPlaceholder': string
'importRepo.usernameReq': string
'importRepo.validation': string
'importSpace.authorization': string
'importSpace.content': string
'importSpace.createNewSpace': string
'importSpace.descPlaceholder': string
'importSpace.description': string
'importSpace.details': string
'importSpace.gitProvider': string
'importSpace.githubOrg': string
'importSpace.gitlabGroup': string
'importSpace.importLabel': string
'importSpace.invalidUrl': string
'importSpace.next': string
'importSpace.orgNamePlaceholder': string
'importSpace.orgRequired': string
'importSpace.organizationName': string
'importSpace.providerRequired': string
'importSpace.spaceName': string
'importSpace.spaceNameRequired': string
'importSpace.title': string
in: string
inactiveBranches: string
isRequired: string

View File

@ -589,6 +589,7 @@ spaceMemberships:
memberUpdated: Member updated successfully.
memberAdded: Member added successfully.
failedToCreateSpace: Failed to create Space. Please try again.
failedToImportSpace: Failed to import Space. Please try again.
failedToCreatePipeline: Failed to create Pipeline. Please try again.
failedToSavePipeline: Failed to save Pipeline. Please try again.
enterName: Enter the name
@ -692,3 +693,40 @@ plugins:
title: Plugins
addAPlugin: Add a {{category}} plugin
stepLabel: step
createNewRepo: Create New repository
importGitRepo: Import Git Repository
importRepo:
title: Import Repository
url: Repository URL
urlPlaceholder: Enter the Repository URL
reqAuth: Requires Authorization
userPlaceholder: Enter Username
passwordPlaceholder: Enter Password
passToken: Password/Token
failedToImportRepo: Failed to import repository. Please try again.
validation: Invalid GitHub or GitLab URL
required: Repository URL is required
spaceNameReq: Enter a name for the new space
usernameReq: Username is required
passwordReq: Password is required
importSpace:
title: Import Space
createNewSpace: Create New Space
authorization: Authorization
content: Import a GitLab Group or a GitHub Org to a new Space in Gitness. Entities at the top level will be imported to the space.
next: Next step
details: Details of target to import
organizationName: Organization Name
orgNamePlaceholder: Enter the org name here
spaceName: Space Name
description: Description (optional)
descPlaceholder: Enter the description
importLabel: What to import
providerRequired: Git Provider is required
orgRequired: Organization name is required
spaceNameRequired: Space name is required
gitProvider: Git Provider
invalidUrl: Invalid GitHub or GitLab URL
githubOrg: GitHub Organization Name
gitlabGroup: GitLab Group Name
importProgress: 'Import in progress...'

View File

@ -10,3 +10,28 @@
height: 100vh;
padding-top: 26% !important;
}
.buttonContainer {
:global {
.bp3-button-text {
font-size: 16px !important;
}
}
}
.bigButton {
:global {
.SplitButton--splitButton {
> .bp3-button {
.bp3-button-text {
font-size: 16px !important;
--font-size: 16px !important;
}
}
}
.bp3-icon-chevron-down {
left: -7px;
position: relative;
}
}
}

View File

@ -1,5 +1,7 @@
/* eslint-disable */
// This is an auto-generated file
export declare const bigButton: string
export declare const buttonContainer: string
export declare const container: string
export declare const main: string
export declare const spaceContainer: string

View File

@ -1,5 +1,5 @@
import React from 'react'
import { ButtonVariation, Container, Layout, PageBody, Text } from '@harnessio/uicore'
import { ButtonVariation, ButtonSize, Container, Layout, PageBody, Text } from '@harnessio/uicore'
import { FontVariation } from '@harnessio/design-system'
import { noop } from 'lodash-es'
import { useHistory } from 'react-router-dom'
@ -21,17 +21,20 @@ export default function Home() {
const NewSpaceButton = (
<NewSpaceModalButton
size={ButtonSize.LARGE}
className={css.bigButton}
space={space}
modalTitle={getString('createASpace')}
text={getString('newSpace')}
variation={ButtonVariation.PRIMARY}
icon="plus"
width={173}
height={48}
onRefetch={noop}
handleNavigation={spaceName => {
history.push(routes.toCODERepositories({ space: spaceName }))
}}
onSubmit={data => {
history.push(routes.toCODERepositories({ space: data.path as string }))
}}
/>
)
return (
@ -51,7 +54,7 @@ export default function Home() {
})}
</Text>
<Text font={{ variation: FontVariation.BODY1 }}>{getString('homepage.firstStep')} </Text>
<Container padding={{ top: 'large' }} flex={{ justifyContent: 'center' }}>
<Container className={css.buttonContainer} padding={{ top: 'large' }} flex={{ justifyContent: 'center' }}>
{NewSpaceButton}
</Container>
</Layout.Vertical>

View File

@ -39,13 +39,26 @@
.row {
height: 80px;
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
overflow: hidden;
&.noDesc > div {
height: 44px;
}
}
.rowDisable {
background-color: var(--grey-50) !important;
cursor: auto !important;
.repoName {
color: var(--grey-400) !important;
}
.desc {
color: var(--grey-300) !important;
}
}
.rowDisable:hover {
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16) !important;
border: unset !important;
}
}
.nameContainer {
@ -102,3 +115,7 @@
padding-top: var(--spacing-xsmall) !important;
}
}
.progressBar {
opacity: 0.7;
}

View File

@ -8,8 +8,10 @@ export declare const name: string
export declare const nameContainer: string
export declare const noDesc: string
export declare const pinned: string
export declare const progressBar: string
export declare const repoName: string
export declare const repoScope: string
export declare const row: string
export declare const rowDisable: string
export declare const table: string
export declare const withError: string

View File

@ -9,6 +9,7 @@ import {
TableV2 as Table,
Text
} from '@harnessio/uicore'
import { ProgressBar, Intent } from '@blueprintjs/core'
import { Icon } from '@harnessio/icons'
import { Color } from '@harnessio/design-system'
import type { CellProps, Column } from 'react-table'
@ -31,6 +32,10 @@ import { NoResultCard } from 'components/NoResultCard/NoResultCard'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import noRepoImage from './no-repo.svg'
import css from './RepositoriesListing.module.scss'
import useSpaceSSE from 'hooks/useSpaceSSE'
interface TypesRepoExtended extends TypesRepository {
importing?: boolean
}
export default function RepositoriesListing() {
const { getString } = useStrings()
@ -56,6 +61,18 @@ export default function RepositoriesListing() {
queryParams: { page, limit: LIST_FETCHING_LIMIT, query: searchTerm },
debounce: 500
})
useSpaceSSE({
space,
events: ['repository_import_completed'],
onEvent: data => {
// should I include pipeline id here? what if a new pipeline is created? coould check for ids that are higher than the lowest id on the page?
if (repositories?.some(repository => repository.id === data?.id && repository.parent_id === data?.parent_id)) {
//TODO - revisit full refresh - can I use the message to update the execution?
console.log('here')
refetch()
}
}
})
useEffect(() => {
setSearchTerm(undefined)
@ -64,12 +81,13 @@ export default function RepositoriesListing() {
}
}, [space, setPage]) // eslint-disable-line react-hooks/exhaustive-deps
const columns: Column<TypesRepository>[] = useMemo(
const columns: Column<TypesRepoExtended>[] = useMemo(
() => [
{
Header: getString('repos.name'),
width: 'calc(100% - 180px)',
Cell: ({ row }: CellProps<TypesRepository>) => {
Cell: ({ row }: CellProps<TypesRepoExtended>) => {
const record = row.original
return (
<Container className={css.nameContainer}>
@ -78,10 +96,16 @@ export default function RepositoriesListing() {
<Text className={css.repoName} width={nameTextWidth} lineClamp={2}>
<Keywords value={searchTerm}>{record.uid}</Keywords>
</Text>
{record.description && (
{record.importing ? (
<Text className={css.desc} width={nameTextWidth} lineClamp={1}>
{record.description}
{getString('importProgress')}
</Text>
) : (
record.description && (
<Text className={css.desc} width={nameTextWidth} lineClamp={1}>
{record.description}
</Text>
)
)}
</Layout.Vertical>
</Layout.Horizontal>
@ -92,8 +116,12 @@ export default function RepositoriesListing() {
{
Header: getString('repos.updated'),
width: '180px',
Cell: ({ row }: CellProps<TypesRepository>) => {
return (
Cell: ({ row }: CellProps<TypesRepoExtended>) => {
return row.original.importing ? (
<Layout.Horizontal style={{ alignItems: 'center' }} padding={{ right: 'large' }}>
<ProgressBar intent={Intent.PRIMARY} className={css.progressBar} />
</Layout.Horizontal>
) : (
<Layout.Horizontal style={{ alignItems: 'center' }}>
<Text color={Color.BLACK} lineClamp={1} rightIconProps={{ size: 10 }} width={120}>
{formatDate(row.original.updated as number)}
@ -107,6 +135,7 @@ export default function RepositoriesListing() {
],
[nameTextWidth, getString, searchTerm]
)
const onResize = useCallback(() => {
if (rowContainerRef.current) {
setNameTextWidth((rowContainerRef.current.closest('div[role="cell"]') as HTMLDivElement)?.offsetWidth - 100)
@ -118,8 +147,13 @@ export default function RepositoriesListing() {
modalTitle={getString('createARepo')}
text={getString('newRepo')}
variation={ButtonVariation.PRIMARY}
icon="plus"
onSubmit={repoInfo => history.push(routes.toCODERepository({ repoPath: repoInfo.path as string }))}
onSubmit={repoInfo => {
if (repoInfo.importing) {
refetch()
} else {
history.push(routes.toCODERepository({ repoPath: repoInfo.path as string }))
}
}}
/>
)
@ -155,12 +189,18 @@ export default function RepositoriesListing() {
<Container margin={{ top: 'medium' }}>
{!!repositories?.length && (
<Table<TypesRepository>
<Table<TypesRepoExtended>
className={css.table}
columns={columns}
data={repositories || []}
onRowClick={repoInfo => history.push(routes.toCODERepository({ repoPath: repoInfo.path as string }))}
getRowClassName={row => cx(css.row, !row.original.description && css.noDesc)}
onRowClick={repoInfo => {
return repoInfo.importing
? undefined
: history.push(routes.toCODERepository({ repoPath: repoInfo.path as string }))
}}
getRowClassName={row =>
cx(css.row, !row.original.description && css.noDesc, row.original.importing && css.rowDisable)
}
/>
)}

View File

@ -21,6 +21,47 @@ export interface GitInfoProps {
commits: TypesCommit[]
pullRequestMetadata: TypesPullReq
}
export interface RepoFormData {
name: string
description: string
license: string
defaultBranch: string
gitignore: string
addReadme: boolean
isPublic: RepoVisibility
}
export interface ImportFormData {
repoUrl: string
username: string
password: string
name: string
description: string
isPublic: RepoVisibility
}
export interface ImportSpaceFormData {
gitProvider: string
username: string
password: string
name: string
description: string
organization: string
}
export enum RepoVisibility {
PUBLIC = 'public',
PRIVATE = 'private'
}
export enum RepoCreationType {
IMPORT = 'import',
CREATE = 'create'
}
export enum SpaceCreationType {
IMPORT = 'import',
CREATE = 'create'
}
export enum GitContentType {
FILE = 'file',
@ -101,6 +142,11 @@ export const CodeIcon = {
ChecksSuccess: 'success-tick' as IconName
}
export enum Organization {
GITHUB = 'Github',
GITLAB = 'Gitlab'
}
export const REFS_TAGS_PREFIX = 'refs/tags/'
// eslint-disable-next-line no-control-regex
@ -168,3 +214,17 @@ export const decodeGitContent = (content = '') => {
}
return ''
}
export const parseUrl = (url: string) => {
const pattern = /^(https?:\/\/(?:www\.)?(github|gitlab)\.com\/([^/]+\/[^/]+))/
const match = url.match(pattern)
if (match) {
const provider = match[2]
const fullRepo = match[3]
const repoName = match[3].split('/')[1]
return { provider, fullRepo, repoName }
} else {
return null
}
}