diff --git a/web/src/components/NewRepoModalButton/ImportForm/ImportForm.tsx b/web/src/components/NewRepoModalButton/ImportForm/ImportForm.tsx
new file mode 100644
index 000000000..a625ec0a9
--- /dev/null
+++ b/web/src/components/NewRepoModalButton/ImportForm/ImportForm.tsx
@@ -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 => {
+ return (
+
+ {
+ 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')
+ }
+ }
+ }}
+ />
+ {
+ setAuth(!auth)
+ }}
+ />
+
+ {auth ? (
+ <>
+
+
+ >
+ ) : null}
+
+ {
+ formik.validateField('repoUrl')
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ {getString('public')}
+
+ {getString('createRepoModal.publicLabel')}
+
+
+
+
+
+ ),
+ value: RepoVisibility.PUBLIC
+ },
+ {
+ label: (
+
+
+
+
+
+ {getString('private')}
+
+ {getString('createRepoModal.privateLabel')}
+
+
+
+
+
+ ),
+ value: RepoVisibility.PRIVATE
+ }
+ ]}
+ />
+
+
+
+
+
+
+
+ {loading && }
+
+
+ )
+ }}
+
+ )
+}
+
+export default ImportForm
diff --git a/web/src/components/NewRepoModalButton/NewRepoModalButton.module.scss b/web/src/components/NewRepoModalButton/NewRepoModalButton.module.scss
index 9f2b00866..c5bdbf0ae 100644
--- a/web/src/components/NewRepoModalButton/NewRepoModalButton.module.scss
+++ b/web/src/components/NewRepoModalButton/NewRepoModalButton.module.scss
@@ -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;
+ }
+ }
+}
diff --git a/web/src/components/NewRepoModalButton/NewRepoModalButton.module.scss.d.ts b/web/src/components/NewRepoModalButton/NewRepoModalButton.module.scss.d.ts
index b4758aa9c..82b44fddd 100644
--- a/web/src/components/NewRepoModalButton/NewRepoModalButton.module.scss.d.ts
+++ b/web/src/components/NewRepoModalButton/NewRepoModalButton.module.scss.d.ts
@@ -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
diff --git a/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx b/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx
index e8fa1797c..9a63fe4bf 100644
--- a/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx
+++ b/web/src/components/NewRepoModalButton/NewRepoModalButton.tsx
@@ -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 = ({
space_path: space
}
})
+ const { mutate: importRepo, loading: importRepoLoading } = useMutate({
+ 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 = ({
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 = ({
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 (
)
}
+ 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(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 = ({
},
[space]
)
+ return (
+ {
+ openModal()
+ }}>
+ {repoCreateOptions.map(option => {
+ return (
+
+ {option.desc}
+ >
+ }
+ onClick={() => {
+ setRepoOption(option)
+ }}
+ />
+ )
+ })}
+
+ )
+}
- return
+interface RepoCreationOption {
+ type: RepoCreationType
+ title: string
+ desc: string
}
interface BranchNameProps {
diff --git a/web/src/components/NewSpaceModalButton/ImportSpaceForm/ImportSpaceForm.tsx b/web/src/components/NewSpaceModalButton/ImportSpaceForm/ImportSpaceForm.tsx
new file mode 100644
index 000000000..540a19c8e
--- /dev/null
+++ b/web/src/components/NewSpaceModalButton/ImportSpaceForm/ImportSpaceForm.tsx
@@ -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 => {
+ 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 (
+
+
+ {step === 0 ? (
+ <>
+
+
+
+
+ {getString('importSpace.content')}
+
+
+
+
+
+
+ {formik.errors.gitProvider ? (
+
+ {formik.errors.gitProvider}
+
+ ) : null}
+
+ {getString('importSpace.authorization')}
+
+
+
+
+
+ {formik.errors.username ? (
+
+ {formik.errors.username}
+
+ ) : null}
+
+ {formik.errors.password ? (
+
+ {formik.errors.password}
+
+ ) : null}
+
+ >
+ ) : null}
+ {step === 1 ? (
+ <>
+
+ {/* */}
+
+ {getString('importSpace.details')}
+
+ {/* */}
+
+
+
+
+
+
+ {formik.errors.organization ? (
+
+ {formik.errors.organization}
+
+ ) : null}
+
+
+
+
+
+
+
+ {
+ setAuth(!auth)
+ }}
+ disabled
+ padding={{ right: 'small' }}
+ className={css.checkbox}
+ />
+
+ {
+ setAuth(!auth)
+ }}
+ />
+
+
+
+
+
+
+ {formik.errors.name ? (
+
+ {formik.errors.name}
+
+ ) : null}
+
+
+
+ >
+ ) : null}
+
+
+
+
+ {step === 1 ? (
+
+
+
+ )
+ }}
+
+ )
+}
+
+export default ImportSpaceForm
diff --git a/web/src/components/NewSpaceModalButton/NewSpaceModalButton.module.scss b/web/src/components/NewSpaceModalButton/NewSpaceModalButton.module.scss
index 9f2b00866..7d8f6ba89 100644
--- a/web/src/components/NewSpaceModalButton/NewSpaceModalButton.module.scss
+++ b/web/src/components/NewSpaceModalButton/NewSpaceModalButton.module.scss
@@ -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;
+ }
+ }
+}
diff --git a/web/src/components/NewSpaceModalButton/NewSpaceModalButton.module.scss.d.ts b/web/src/components/NewSpaceModalButton/NewSpaceModalButton.module.scss.d.ts
index b4758aa9c..632aabdb4 100644
--- a/web/src/components/NewSpaceModalButton/NewSpaceModalButton.module.scss.d.ts
+++ b/web/src/components/NewSpaceModalButton/NewSpaceModalButton.module.scss.d.ts
@@ -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
diff --git a/web/src/components/NewSpaceModalButton/NewSpaceModalButton.tsx b/web/src/components/NewSpaceModalButton/NewSpaceModalButton.tsx
index 03ff239ee..dae02adff 100644
--- a/web/src/components/NewSpaceModalButton/NewSpaceModalButton.tsx
+++ b/web/src/components/NewSpaceModalButton/NewSpaceModalButton.tsx
@@ -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 {
+export interface NewSpaceModalButtonProps extends Omit {
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 = ({
cancelButtonTitle,
onRefetch,
handleNavigation,
+ onSubmit,
+ fromSpace = false,
...props
}) => {
const ModalComponent: React.FC = () => {
@@ -78,10 +87,14 @@ export const NewSpaceModalButton: React.FC = ({
verb: 'POST',
path: `/api/v1/spaces`
})
+ const { mutate: importSpace, loading: submitImportLoading } = useMutate({
+ 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 = ({
}
}
+ 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 (
)
}
- const [openModal, hideModal] = useModalHook(ModalComponent)
+ const { getString } = useStrings()
- return
+ 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(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 (
+ {
+ openModal()
+ }}>
+ {spaceCreateOptions.map(option => {
+ return (
+
+
+ {option.desc}
+ >
+ }
+ onClick={event => {
+ event.stopPropagation()
+ event.preventDefault()
+ setSpaceOption(option)
+ }}
+ />
+
+ )
+ })}
+
+ )
+}
+
+interface SpaceCreationOption {
+ type: SpaceCreationType
+ title: string
+ desc: string
}
diff --git a/web/src/components/SpaceSelector/SpaceSelector.tsx b/web/src/components/SpaceSelector/SpaceSelector.tsx
index 8dc605e8b..99b5e08a1 100644
--- a/web/src/components/SpaceSelector/SpaceSelector.tsx
+++ b/web/src/components/SpaceSelector/SpaceSelector.tsx
@@ -92,6 +92,11 @@ export const SpaceSelector: React.FC = ({ 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 = ({ onSelect }) => {
variation={ButtonVariation.PRIMARY}
icon="plus"
onRefetch={voidFn(refetch)}
+ onSubmit={spaceData => {
+ history.push(routes.toCODERepositories({ space: spaceData.path as string }))
+ }}
+ fromSpace={true}
/>
}
message={ {getString('emptySpaceText')}}
diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts
index fde04567e..128b0cd2b 100644
--- a/web/src/framework/strings/stringTypes.ts
+++ b/web/src/framework/strings/stringTypes.ts
@@ -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
diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml
index 4857d1112..69424e0e2 100644
--- a/web/src/i18n/strings.en.yaml
+++ b/web/src/i18n/strings.en.yaml
@@ -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...'
diff --git a/web/src/pages/Home/Home.module.scss b/web/src/pages/Home/Home.module.scss
index 4594e5b56..6d322fc66 100644
--- a/web/src/pages/Home/Home.module.scss
+++ b/web/src/pages/Home/Home.module.scss
@@ -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;
+ }
+ }
+}
diff --git a/web/src/pages/Home/Home.module.scss.d.ts b/web/src/pages/Home/Home.module.scss.d.ts
index e948d155b..a0d0175ee 100644
--- a/web/src/pages/Home/Home.module.scss.d.ts
+++ b/web/src/pages/Home/Home.module.scss.d.ts
@@ -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
diff --git a/web/src/pages/Home/Home.tsx b/web/src/pages/Home/Home.tsx
index c9f577cc2..1f0b25326 100644
--- a/web/src/pages/Home/Home.tsx
+++ b/web/src/pages/Home/Home.tsx
@@ -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 = (
{
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() {
})}
{getString('homepage.firstStep')}
-
+
{NewSpaceButton}
diff --git a/web/src/pages/RepositoriesListing/RepositoriesListing.module.scss b/web/src/pages/RepositoriesListing/RepositoriesListing.module.scss
index e84c78c0a..6a9f98e37 100644
--- a/web/src/pages/RepositoriesListing/RepositoriesListing.module.scss
+++ b/web/src/pages/RepositoriesListing/RepositoriesListing.module.scss
@@ -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;
+}
diff --git a/web/src/pages/RepositoriesListing/RepositoriesListing.module.scss.d.ts b/web/src/pages/RepositoriesListing/RepositoriesListing.module.scss.d.ts
index 14776345b..0d14bc1eb 100644
--- a/web/src/pages/RepositoriesListing/RepositoriesListing.module.scss.d.ts
+++ b/web/src/pages/RepositoriesListing/RepositoriesListing.module.scss.d.ts
@@ -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
diff --git a/web/src/pages/RepositoriesListing/RepositoriesListing.tsx b/web/src/pages/RepositoriesListing/RepositoriesListing.tsx
index 31269b3d4..fa92ef234 100644
--- a/web/src/pages/RepositoriesListing/RepositoriesListing.tsx
+++ b/web/src/pages/RepositoriesListing/RepositoriesListing.tsx
@@ -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[] = useMemo(
+ const columns: Column[] = useMemo(
() => [
{
Header: getString('repos.name'),
width: 'calc(100% - 180px)',
- Cell: ({ row }: CellProps) => {
+
+ Cell: ({ row }: CellProps) => {
const record = row.original
return (
@@ -78,10 +96,16 @@ export default function RepositoriesListing() {
{record.uid}
- {record.description && (
+ {record.importing ? (
- {record.description}
+ {getString('importProgress')}
+ ) : (
+ record.description && (
+
+ {record.description}
+
+ )
)}
@@ -92,8 +116,12 @@ export default function RepositoriesListing() {
{
Header: getString('repos.updated'),
width: '180px',
- Cell: ({ row }: CellProps) => {
- return (
+ Cell: ({ row }: CellProps) => {
+ return row.original.importing ? (
+
+
+
+ ) : (
{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() {
{!!repositories?.length && (
-
+
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)
+ }
/>
)}
diff --git a/web/src/utils/GitUtils.ts b/web/src/utils/GitUtils.ts
index 1e9c418f4..d54ec3ecf 100644
--- a/web/src/utils/GitUtils.ts
+++ b/web/src/utils/GitUtils.ts
@@ -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
+ }
+}