diff --git a/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.module.scss b/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.module.scss index 60644bf7d..d7080fd7b 100644 --- a/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.module.scss +++ b/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.module.scss @@ -51,3 +51,29 @@ } } } + +.customTextInput { + :global { + [class*='TextInput--main'] { + margin-bottom: 0 !important; + height: inherit !important; + width: fit-content !important; + flex-grow: 1 !important; + border: none !important; + outline: none !important; + } + + .bp3-input { + border: none !important; + outline: none !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + background: transparent !important; + font-size: 14px !important; + } + + .bp3-input:hover { + box-shadow: none !important; + } + } +} diff --git a/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.module.scss.d.ts b/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.module.scss.d.ts index 64e263c9f..85ad9e408 100644 --- a/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.module.scss.d.ts +++ b/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.module.scss.d.ts @@ -16,6 +16,7 @@ /* eslint-disable */ // This is an auto-generated file +export declare const customTextInput: string export declare const formFields: string export declare const leftElementClassName: string export declare const repoInput: string diff --git a/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.tsx b/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.tsx index f1bb76657..5ce39248d 100644 --- a/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.tsx +++ b/web/src/cde-gitness/components/CDEAnyGitImport/CDEAnyGitImport.tsx @@ -14,22 +14,21 @@ * limitations under the License. */ -import React, { useCallback, useEffect, useState } from 'react' -import cx from 'classnames' -import { Container, FormikForm, FormInput, Layout } from '@harnessio/uicore' -import { Color } from '@harnessio/design-system' -import { Icon } from '@harnessio/icons' -import { debounce, get } from 'lodash-es' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { Container, FormikForm, Layout, Text, TextInput } from '@harnessio/uicore' +import { debounce, defaultTo, get } from 'lodash-es' import { useFormikContext } from 'formik' -import { Repository } from 'iconoir-react' +import { GitFork, Repository } from 'iconoir-react' +import { Menu, MenuItem } from '@blueprintjs/core' +import { Color } from '@harnessio/design-system' import { useStrings } from 'framework/strings' -import type { OpenapiCreateGitspaceRequest } from 'cde-gitness/services' -import { BranchInput } from 'cde-gitness/components/BranchInput/BranchInput' -import { useRepoLookupForGitspace } from 'services/cde' +import type { OpenapiCreateGitspaceRequest } from 'services/cde' +import { useListRepos, useListBranches, useRepoLookupForGitspace } from 'services/cde' import { useGetCDEAPIParams } from 'cde-gitness/hooks/useGetCDEAPIParams' -import type { RepoQueryParams } from 'cde-gitness/pages/GitspaceCreate/CDECreateGitspace' +import { scmOptions, SCMType, type RepoQueryParams } from 'cde-gitness/pages/GitspaceCreate/CDECreateGitspace' import { useQueryParams } from 'hooks/useQueryParams' import { getRepoIdFromURL, getRepoNameFromURL, isValidUrl } from './CDEAnyGitImport.utils' +import { GitspaceSelect } from '../GitspaceSelect/GitspaceSelect' import css from './CDEAnyGitImport.module.scss' enum RepoCheckStatus { @@ -40,15 +39,49 @@ enum RepoCheckStatus { export const CDEAnyGitImport = () => { const { getString } = useStrings() const repoQueryParams = useQueryParams() + const { setValues, setFieldError, values } = useFormikContext() const { accountIdentifier = '', orgIdentifier = '', projectIdentifier = '' } = useGetCDEAPIParams() + const [searchTerm, setSearchTerm] = useState(values?.code_repo_url as string) + const [searchBranch, setSearchBranch] = useState(values?.branch as string) + const { mutate, loading } = useRepoLookupForGitspace({ accountIdentifier, orgIdentifier, projectIdentifier }) + const { data: repoData, loading: repoLoading } = useListRepos({ + accountIdentifier, + orgIdentifier, + projectIdentifier, + gitspace_identifier: '', + queryParams: { + search_term: searchTerm, + repo_type: values?.code_repo_type as string + }, + debounce: 1000 + }) + + const { + data: branchData, + loading: branchLoading, + refetch: refetchBranch + } = useListBranches({ + accountIdentifier, + orgIdentifier, + projectIdentifier, + gitspace_identifier: '', + queryParams: { + search_term: searchBranch, + repo_type: values.code_repo_type || '', + repo_url: values.code_repo_url || '' + }, + debounce: 1000, + lazy: true + }) + const [repoCheckState, setRepoCheckState] = useState() useEffect(() => { @@ -63,6 +96,12 @@ export const CDEAnyGitImport = () => { } }, [values.code_repo_url, repoQueryParams.codeRepoURL]) + useEffect(() => { + if (searchBranch) { + refetchBranch() + } + }, [searchBranch]) + const onChange = useCallback( debounce(async (url: string, skipBranchUpdate?: boolean) => { let errorMessage = '' @@ -103,15 +142,6 @@ export const CDEAnyGitImport = () => { }) setRepoCheckState(RepoCheckStatus.Valid) } - } else { - if (url?.trim()?.length) { - errorMessage = 'Invalid URL Format' - setRepoCheckState(RepoCheckStatus.InValid) - } else { - if (repoCheckState) { - setRepoCheckState(undefined) - } - } } } catch (err) { errorMessage = get(err, 'message') || '' @@ -121,43 +151,142 @@ export const CDEAnyGitImport = () => { [repoCheckState, values?.code_repo_type] ) + const branchRef = useRef() + const repoRef = useRef() + + const scmOption = scmOptions.find(item => item.value === values.code_repo_type) as SCMType + return ( - - - - ), - className: css.leftElementClassName, - rightElement: ( - - {loading ? ( - - ) : repoCheckState ? ( - repoCheckState === RepoCheckStatus.Valid ? ( - - ) : ( - + + + (repoRef.current = ref)} + value={searchTerm} + placeholder="enter url or type reop name" + onChange={async event => { + const target = event.target as HTMLInputElement + setSearchTerm(target?.value?.trim() || '') + await onChange(target.value) + }} + /> + + } + tooltipProps={{ isOpen: repoRef.current?.onfocus }} + rightIcon={ + loading || repoLoading + ? 'loading' + : repoCheckState + ? repoCheckState === RepoCheckStatus.Valid + ? 'tick-circle' + : 'warning-sign' + : 'chevron-down' + } + withoutCurrentColor + formikName="code_repo_url" + renderMenu={ + + {repoData?.repositories?.length ? ( + repoData?.repositories?.map(item => { + return ( + + + + {item.name} + {item.clone_url} + + + } + onClick={() => { + setSearchTerm(item.name as string) + setValues((prvValues: any) => { + return { + ...prvValues, + code_repo_url: item.clone_url, + branch: item.default_branch, + identifier: getRepoIdFromURL(item.clone_url), + name: getRepoNameFromURL(item.clone_url), + code_repo_type: values?.code_repo_type + } + }) + setSearchBranch(item.default_branch as string) + refetchBranch() + }} + /> ) - ) : undefined} - - ) - }} - placeholder={getString('cde.repository.repositoryURL')} - className={cx(css.repoInput)} - onChange={async event => { - const target = event.target as HTMLInputElement - await onChange(target.value) - }} + }) + ) : loading || repoLoading ? ( + Fetching Repositories} /> + ) : ( + No Repositories Found} /> + )} + + } /> - + + + (branchRef.current = ref)} + value={searchBranch} + placeholder="enter branch name" + onChange={async event => { + const target = event.target as HTMLInputElement + setSearchBranch(target?.value?.trim() || '') + }} + /> + + } + tooltipProps={{ isOpen: branchRef.current?.onfocus }} + rightIcon={loading || branchLoading ? 'loading' : 'chevron-down'} + withoutCurrentColor + formikName="branch" + renderMenu={ + + {(branchData as unknown as { branches: { name: string }[] })?.branches?.length ? ( + (branchData as unknown as { branches: { name: string }[] })?.branches?.map(item => { + return ( + {item.name}} + onClick={() => { + setSearchBranch(item.name as string) + setValues((prvValues: any) => { + return { + ...prvValues, + branch: item.name + } + }) + }} + /> + ) + }) + ) : loading || repoLoading ? ( + Fetching Branches} /> + ) : ( + No Branches Found} /> + )} + + } + /> diff --git a/web/src/cde-gitness/components/CDEAnyGitImport/CDEUnknownSCM.tsx b/web/src/cde-gitness/components/CDEAnyGitImport/CDEUnknownSCM.tsx new file mode 100644 index 000000000..2486ed5bc --- /dev/null +++ b/web/src/cde-gitness/components/CDEAnyGitImport/CDEUnknownSCM.tsx @@ -0,0 +1,165 @@ +/* + * Copyright 2024 Harness, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useEffect, useState } from 'react' +import cx from 'classnames' +import { Container, FormikForm, FormInput, Layout } from '@harnessio/uicore' +import { Color } from '@harnessio/design-system' +import { Icon } from '@harnessio/icons' +import { debounce, get } from 'lodash-es' +import { useFormikContext } from 'formik' +import { Repository } from 'iconoir-react' +import { useStrings } from 'framework/strings' +import type { OpenapiCreateGitspaceRequest } from 'cde-gitness/services' +import { BranchInput } from 'cde-gitness/components/BranchInput/BranchInput' +import { useRepoLookupForGitspace } from 'services/cde' +import { useGetCDEAPIParams } from 'cde-gitness/hooks/useGetCDEAPIParams' +import type { RepoQueryParams } from 'cde-gitness/pages/GitspaceCreate/CDECreateGitspace' +import { useQueryParams } from 'hooks/useQueryParams' +import { getRepoIdFromURL, getRepoNameFromURL, isValidUrl } from './CDEAnyGitImport.utils' +import css from './CDEAnyGitImport.module.scss' + +enum RepoCheckStatus { + Valid = 'valid', + InValid = 'InValid' +} + +export const CDEUnknownSCM = () => { + const { getString } = useStrings() + const repoQueryParams = useQueryParams() + const { setValues, setFieldError, values } = useFormikContext() + const { accountIdentifier = '', orgIdentifier = '', projectIdentifier = '' } = useGetCDEAPIParams() + + const { mutate, loading } = useRepoLookupForGitspace({ + accountIdentifier, + orgIdentifier, + projectIdentifier + }) + + const [repoCheckState, setRepoCheckState] = useState() + + useEffect(() => { + if (values?.code_repo_type) { + setRepoCheckState(undefined) + } + }, [values?.code_repo_type]) + + useEffect(() => { + if (values.code_repo_url === repoQueryParams.codeRepoURL && repoQueryParams.codeRepoURL) { + onChange(repoQueryParams.codeRepoURL as string, Boolean(repoQueryParams.branch)) + } + }, [values.code_repo_url, repoQueryParams.codeRepoURL]) + + const onChange = useCallback( + debounce(async (url: string, skipBranchUpdate?: boolean) => { + let errorMessage = '' + try { + if (isValidUrl(url)) { + const response = (await mutate({ url, repo_type: values?.code_repo_type })) as { + is_private?: boolean + branch: string + url: string + } + if (!response?.branch) { + errorMessage = getString('cde.repository.privateRepoWarning') + setRepoCheckState(RepoCheckStatus.InValid) + setValues((prvValues: any) => { + return { + ...prvValues, + code_repo_url: response.url, + branch: undefined, + identifier: undefined, + name: undefined, + code_repo_type: values?.code_repo_type + } + }) + setTimeout(() => { + setFieldError('code_repo_url', errorMessage) + }, 500) + } else { + const branchValue = skipBranchUpdate ? {} : { branch: response.branch } + setValues((prvValues: any) => { + return { + ...prvValues, + code_repo_url: response.url, + ...branchValue, + identifier: getRepoIdFromURL(response.url), + name: getRepoNameFromURL(response.url), + code_repo_type: values?.code_repo_type + } + }) + setRepoCheckState(RepoCheckStatus.Valid) + } + } else { + if (url?.trim()?.length) { + errorMessage = 'Invalid URL Format' + setRepoCheckState(RepoCheckStatus.InValid) + } else { + if (repoCheckState) { + setRepoCheckState(undefined) + } + } + } + } catch (err) { + errorMessage = get(err, 'message') || '' + } + setFieldError('code_repo_url', errorMessage) + }, 1000), + [repoCheckState, values?.code_repo_type] + ) + + return ( + + + + + + + ), + className: css.leftElementClassName, + rightElement: ( + + {loading ? ( + + ) : repoCheckState ? ( + repoCheckState === RepoCheckStatus.Valid ? ( + + ) : ( + + ) + ) : undefined} + + ) + }} + placeholder={getString('cde.repository.repositoryURL')} + className={cx(css.repoInput)} + onChange={async event => { + const target = event.target as HTMLInputElement + await onChange(target.value) + }} + /> + + + + + + + ) +} diff --git a/web/src/cde-gitness/components/CDECustomDropdown/CDECustomDropdown.tsx b/web/src/cde-gitness/components/CDECustomDropdown/CDECustomDropdown.tsx index 0fb728068..63c48a015 100644 --- a/web/src/cde-gitness/components/CDECustomDropdown/CDECustomDropdown.tsx +++ b/web/src/cde-gitness/components/CDECustomDropdown/CDECustomDropdown.tsx @@ -66,6 +66,4 @@ export const CDECustomDropdown = ({ label, menu, leftElement, overridePopOverWid ) - - return
CDECustomDropdown
} diff --git a/web/src/cde-gitness/components/CDESSHSelect/CDESSHSelect.tsx b/web/src/cde-gitness/components/CDESSHSelect/CDESSHSelect.tsx index 1be251122..529c433d1 100644 --- a/web/src/cde-gitness/components/CDESSHSelect/CDESSHSelect.tsx +++ b/web/src/cde-gitness/components/CDESSHSelect/CDESSHSelect.tsx @@ -53,7 +53,8 @@ export const CDESSHSelect = () => { const { data, loading, refetch } = useListAggregatedTokens({ queryParams: { accountIdentifier, - apiKeyType: 'SSH_KEY' + apiKeyType: 'SSH_KEY', + parentIdentifier: currentUser.uid } }) @@ -65,7 +66,7 @@ export const CDESSHSelect = () => { queryParams: { accountIdentifier, apiKeyType: 'SSH_KEY', - parentIdentifier: currentUser.uid || 'cWQVVVCET4iwomT4hRoj4w', + parentIdentifier: currentUser.uid, apiKeyIdentifier: `cdesshkey` } }) @@ -108,7 +109,7 @@ export const CDESSHSelect = () => { tags: {}, accountIdentifier, apiKeyIdentifier: `cdesshkey`, - parentIdentifier: currentUser.uid || 'cWQVVVCET4iwomT4hRoj4w', + parentIdentifier: currentUser.uid, apiKeyType: 'SSH_KEY', sshKeyContent: value.sshKeyValue, sshKeyUsage: ['AUTH'], diff --git a/web/src/cde-gitness/components/GitspaceSelect/GitspaceSelect.tsx b/web/src/cde-gitness/components/GitspaceSelect/GitspaceSelect.tsx index c63c5cd02..b2cbb0532 100644 --- a/web/src/cde-gitness/components/GitspaceSelect/GitspaceSelect.tsx +++ b/web/src/cde-gitness/components/GitspaceSelect/GitspaceSelect.tsx @@ -35,14 +35,18 @@ interface GitspaceSelectProps { loading?: boolean buttonClassName?: string withoutCurrentColor?: boolean + hideMenu?: boolean + rightIcon?: IconName } export const GitspaceSelect = ({ text, icon, loading, + hideMenu, renderMenu, disabled, + rightIcon, overridePopOverWidth, formikName, tooltipProps, @@ -54,26 +58,28 @@ export const GitspaceSelect = ({ const buttonRef = useRef(null) const [popoverWidth, setPopoverWidth] = useState(0) - const defaultTooltipProps = { - tooltip: ( - - {renderMenu ? ( - renderMenu - ) : ( - - {getString('cde.noData')} - - )} - - ), - tooltipProps: { - fill: true, - interactionKind: PopoverInteractionKind.CLICK, - position: PopoverPosition.BOTTOM_LEFT, - popoverClassName: cx(css.popover), - ...tooltipProps - } - } + const defaultTooltipProps = hideMenu + ? {} + : { + tooltip: ( + + {renderMenu ? ( + renderMenu + ) : ( + + {getString('cde.noData')} + + )} + + ), + tooltipProps: { + fill: true, + interactionKind: PopoverInteractionKind.CLICK, + position: PopoverPosition.BOTTOM_LEFT, + popoverClassName: cx(css.popover), + ...tooltipProps + } + } useEffect(() => { if ( @@ -98,7 +104,7 @@ export const GitspaceSelect = ({