diff --git a/web/src/RouteDestinations.tsx b/web/src/RouteDestinations.tsx index 2d7f54f41..57eac4cf6 100644 --- a/web/src/RouteDestinations.tsx +++ b/web/src/RouteDestinations.tsx @@ -52,8 +52,8 @@ import AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline' import { useAppContext } from 'AppContext' import PipelineSettings from 'components/PipelineSettings/PipelineSettings' import GitspaceDetail from 'cde/pages/GitspaceDetail/GitspaceDetail' -import Gitspaces from 'cde/pages/Gitspaces/Gitspaces' -import GitspacesListing from 'cde/pages/GitspacesListing/GitspacesListing' +import { GitspaceListing } from 'cde-gitness/pages/GitspaceListing/GitspaceListing' +import { GitspaceCreate } from 'cde-gitness/pages/GitspaceCreate/GitspaceCreate' export const RouteDestinations: React.FC = React.memo(function RouteDestinations() { const { getString } = useStrings() @@ -267,7 +267,15 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations {standalone && ( - + + + + )} + + {standalone && ( + + + )} @@ -283,15 +291,7 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations {standalone && ( - - - - )} - - {standalone && ( - - - + )} diff --git a/web/src/cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm.module.scss b/web/src/cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm.module.scss new file mode 100644 index 000000000..86a082562 --- /dev/null +++ b/web/src/cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm.module.scss @@ -0,0 +1,3 @@ +.repoAndBranch { + margin-bottom: 0 !important; +} diff --git a/web/src/cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm.module.scss.d.ts b/web/src/cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm.module.scss.d.ts new file mode 100644 index 000000000..ebc423548 --- /dev/null +++ b/web/src/cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm.module.scss.d.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 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. + */ + +/* eslint-disable */ +// This is an auto-generated file +export declare const repoAndBranch: string diff --git a/web/src/cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm.tsx b/web/src/cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm.tsx new file mode 100644 index 000000000..705fa5238 --- /dev/null +++ b/web/src/cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm.tsx @@ -0,0 +1,226 @@ +import React, { useEffect, useState } from 'react' +import { useGet } from 'restful-react' +import { Container, ExpandingSearchInput, Layout, Text } from '@harnessio/uicore' +import { Menu, MenuItem } from '@blueprintjs/core' +import { Color } from '@harnessio/design-system' +import { Icon } from '@harnessio/icons' +import { useFormikContext } from 'formik' +import type { TypesRepository } from 'services/code' +import { useGetSpaceParam } from 'hooks/useGetSpaceParam' +import { String, useStrings } from 'framework/strings' +import { LIST_FETCHING_LIMIT } from 'utils/Utils' +import NewRepoModalButton from 'components/NewRepoModalButton/NewRepoModalButton' +import { GitspaceSelect } from '../../../cde/components/GitspaceSelect/GitspaceSelect' +import gitnessRepoLogo from './gitness.svg?url' +import css from './GitnessRepoImportForm.module.scss' + +const RepositoryText = ({ repoList, value }: { repoList: TypesRepository[] | null; value?: string }) => { + const { getString } = useStrings() + const repoMetadata = repoList?.find(repo => repo.git_url === value) + const repoName = repoMetadata?.path + + return ( + + + {repoName ? ( + + + {getString('cde.repository.repo')} + + {repoName || getString('cde.repository.repositoryURL')} + + + + ) : ( + {getString('cde.repository.selectRepository')} + )} + + ) +} + +const BranchText = ({ value }: { value?: string }) => { + const { getString } = useStrings() + return ( + + + {value ? ( + + + {getString('branch')} + + {value} + + + + ) : ( + {getString('cde.create.selectBranchPlaceholder')} + )} + + ) +} + +export const GitnessRepoImportForm = () => { + const { getString } = useStrings() + const space = useGetSpaceParam() + const [branchSearch, setBranchSearch] = useState('') + const [repoSearch, setRepoSearch] = useState('') + const [repoRef, setReporef] = useState('') + + const { + data: repositories, + loading, + refetch: refetchRepos + } = useGet({ + path: `/api/v1/spaces/${space}/+/repos`, + queryParams: { query: repoSearch }, + debounce: 500 + }) + + const { + data: branches, + refetch, + loading: loadingBranches + } = useGet<{ name: string }[]>({ + path: `/api/v1/repos/${repoRef}/+/branches`, + queryParams: { + limit: LIST_FETCHING_LIMIT, + page: 1, + sort: 'date', + order: 'desc', + include_commit: false, + query: branchSearch + }, + lazy: true + }) + + useEffect(() => { + if (repoRef || branchSearch) { + refetch() + } + }, [repoRef, branchSearch]) + + const repoListOptions = repositories || [] + + const formik = useFormikContext() + + const { values } = formik + const repoMetadata = repoListOptions.find(repo => repo.git_url === values.code_repo_url) + if (repoRef !== repoMetadata?.path) { + setReporef(repoMetadata?.path as string) + } + + return ( + + + } + tooltipProps={{ + onClose: () => { + setRepoSearch('') + } + }} + renderMenu={ + + + + + {loading ? ( + + ) : repoListOptions?.length ? ( + repoListOptions.map(repo => ( + + + {repo.path} + + } + active={repo.git_url === values.code_repo_url} + onClick={() => { + formik.setValues((prvValues: any) => { + return { + ...prvValues, + code_repo_url: repo.git_url, + id: repo.path, + name: repo.path + } + }) + formik.setFieldValue('code_repo_url', repo.git_url) + }} + /> + )) + ) : ( + + { + refetchRepos() + }} + /> + + )} + + } + /> + + + } + tooltipProps={{ + onClose: () => { + setBranchSearch('') + } + }} + renderMenu={ + + + + + {loadingBranches ? ( + + ) : branches?.length ? ( + branches?.map(branch => ( + formik.setFieldValue('branch', branch.name)} + /> + )) + ) : ( + } + /> + )} + + } + /> + + + ) +} diff --git a/web/src/cde-gitness/components/GitnessRepoImportForm/gitness.svg b/web/src/cde-gitness/components/GitnessRepoImportForm/gitness.svg new file mode 100644 index 000000000..56b3bc6c2 --- /dev/null +++ b/web/src/cde-gitness/components/GitnessRepoImportForm/gitness.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/cde-gitness/components/GitspaceListing/ListGitspaces.module.scss b/web/src/cde-gitness/components/GitspaceListing/ListGitspaces.module.scss new file mode 100644 index 000000000..5f4c5b721 --- /dev/null +++ b/web/src/cde-gitness/components/GitspaceListing/ListGitspaces.module.scss @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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. + */ + +.table { + margin-top: var(--spacing-large); + + div[class*='TableV2--cell'] { + width: auto !important; + } + + div[class*='TableV2--cells'], + div[class*='TableV2--header'] { + display: grid !important; + grid-template-columns: 1fr 1fr 1fr 0.7fr 50px; + } +} + +.popover { + > div[class*='popover-arrow'] { + display: none; + } + + .listContainer { + :global { + a.bp3-menu-item:hover, + .bp3-active { + background: var(--primary-1) !important; + color: var(--grey-1000) !important; + } + } + } +} + +.gitspaceURL { + white-space: nowrap !important; + overflow: hidden; + text-overflow: ellipsis; +} + +.repositoryCell { + width: fit-content !important; +} diff --git a/web/src/cde-gitness/components/GitspaceListing/ListGitspaces.module.scss.d.ts b/web/src/cde-gitness/components/GitspaceListing/ListGitspaces.module.scss.d.ts new file mode 100644 index 000000000..7b5539a28 --- /dev/null +++ b/web/src/cde-gitness/components/GitspaceListing/ListGitspaces.module.scss.d.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 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. + */ + +/* eslint-disable */ +// This is an auto-generated file +export declare const gitspaceUrl: string +export declare const listContainer: string +export declare const popover: string +export declare const repositoryCell: string +export declare const table: string diff --git a/web/src/cde-gitness/components/GitspaceListing/ListGitspaces.tsx b/web/src/cde-gitness/components/GitspaceListing/ListGitspaces.tsx new file mode 100644 index 000000000..9f14e7509 --- /dev/null +++ b/web/src/cde-gitness/components/GitspaceListing/ListGitspaces.tsx @@ -0,0 +1,505 @@ +/* + * Copyright 2023 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 { Container, Layout, TableV2, Text, useToaster } from '@harnessio/uicore' +import React from 'react' +import { Color } from '@harnessio/design-system' +import type { Renderer, CellProps } from 'react-table' +import ReactTimeago from 'react-timeago' +import { + Circle, + GitBranch, + Cpu, + Clock, + Play, + Square, + Db, + ModernTv, + OpenInBrowser, + DeleteCircle, + EditPencil, + ViewColumns2, + GithubCircle, + GitLabFull, + Code, + Bitbucket as BitbucketIcon +} from 'iconoir-react' +import { Menu, MenuItem, PopoverInteractionKind, Position } from '@blueprintjs/core' +import { useHistory } from 'react-router-dom' +import { isNil } from 'lodash-es' +import { UseStringsReturn, useStrings } from 'framework/strings' +import { useAppContext } from 'AppContext' +import { getErrorMessage } from 'utils/Utils' +import { useConfirmAct } from 'hooks/useConfirmAction' +import VSCode from 'cde/icons/VSCode.svg?url' +import { GitspaceStatus } from 'cde/constants' +import { + EnumGitspaceStateType, + EnumIDEType, + useDeleteGitspace, + type TypesGitspaceConfig, + type EnumCodeRepoType +} from 'cde-gitness/services' +import css from './ListGitspaces.module.scss' + +enum CodeRepoType { + Github = 'github', + Gitlab = 'gitlab', + HarnessCode = 'harness_code', + Bitbucket = 'bitbucket', + Unknown = 'unknown' +} + +const getIconByRepoType = ({ repoType }: { repoType?: EnumCodeRepoType }): React.ReactNode => { + switch (repoType) { + case CodeRepoType.Github: + return + case CodeRepoType.Gitlab: + return + case CodeRepoType.Bitbucket: + return + default: + case CodeRepoType.Unknown: + case CodeRepoType.HarnessCode: + return + } +} + +export const getStatusColor = (status?: EnumGitspaceStateType) => { + switch (status) { + case GitspaceStatus.RUNNING: + return '#42AB45' + case GitspaceStatus.STOPPED: + return '#F3F3FA' + case GitspaceStatus.ERROR: + return '#FF0000' + default: + return '#000000' + } +} + +export const getStatusText = (getString: UseStringsReturn['getString'], status?: EnumGitspaceStateType) => { + switch (status) { + case GitspaceStatus.RUNNING: + return getString('cde.listing.online') + case GitspaceStatus.STOPPED: + return getString('cde.listing.offline') + case GitspaceStatus.ERROR: + return getString('cde.listing.error') + default: + return getString('cde.listing.offline') + } +} + +enum IDEType { + VSCODE = 'vs_code', + VSCODEWEB = 'vs_code_web' +} + +const getUsageTemplate = ( + getString: UseStringsReturn['getString'], + icon: React.ReactNode, + resource_usage?: string, + total_time_used?: number +): React.ReactElement | null => { + return ( + + {icon} + + {getString('cde.used')} {resource_usage || 0} + + / + + {total_time_used || 0} {getString('cde.hours')} + + + ) +} + +export const RenderGitspaceName: Renderer> = ({ row }) => { + const details = row.original + const { name } = details + return ( + + + + {name} + + + ) +} + +export const RenderRepository: Renderer> = ({ row }) => { + const { getString } = useStrings() + const details = row.original + const { name, branch, code_repo_url, code_repo_type, instance } = details || {} + + return ( + + { + e.preventDefault() + e.stopPropagation() + window.open(code_repo_url, '_blank') + }}> + {getIconByRepoType({ repoType: code_repo_type })} + + {name} + + : + + + {branch} + + + {(isNil(instance?.tracked_changes) || instance?.tracked_changes === '') && ( + + {getString('cde.noChange')} + + )} + + ) +} + +export const RenderCPUUsage: Renderer> = ({ row }) => { + const { getString } = useStrings() + const instance = row.original.instance + const { resource_usage, total_time_used } = instance || {} + + return getUsageTemplate(getString, , resource_usage, total_time_used) +} + +export const RenderStorageUsage: Renderer> = ({ row }) => { + const { getString } = useStrings() + const instance = row.original.instance + const { resource_usage, total_time_used } = instance || {} + + return getUsageTemplate(getString, , resource_usage, total_time_used) +} + +export const RenderLastActivity: Renderer> = ({ row }) => { + const { getString } = useStrings() + const instance = row.original.instance + const { last_used } = instance || {} + return ( + + + {last_used ? ( + + ) : ( + + {getString('cde.na')} + + )} + + ) +} + +export const RenderGitspaceStatus: Renderer> = ({ row }) => { + const { getString } = useStrings() + const details = row.original + const { instance, name } = details + const { state } = instance || {} + const color = getStatusColor(state) + return ( + + + + {getStatusText(getString, state)} + + + ) +} + +export const StartStopButton = ({ state, loading }: { state?: EnumGitspaceStateType; loading?: boolean }) => { + const { getString } = useStrings() + return ( + + {loading ? <> : state === GitspaceStatus.RUNNING ? : } + + {state === GitspaceStatus.RUNNING + ? getString('cde.details.stopGitspace') + : getString('cde.details.startGitspace')} + + + ) +} + +export const OpenGitspaceButton = ({ ide }: { ide?: EnumIDEType }) => { + const { getString } = useStrings() + + return ( + + {ide === IDEType.VSCODE ? : } + {ide === IDEType.VSCODE ? getString('cde.ide.openVSCode') : getString('cde.ide.openBrowser')} + + ) +} + +interface ActionMenuProps { + data: TypesGitspaceConfig + refreshList: () => void + handleStartStop?: () => Promise + loading?: boolean + actionLoading?: boolean + deleteLoading?: boolean + deleteGitspace: (e: React.MouseEvent) => Promise +} + +const ActionMenu = ({ + data, + deleteGitspace, + refreshList, + handleStartStop, + actionLoading, + deleteLoading +}: ActionMenuProps) => { + const { getString } = useStrings() + const { showError } = useToaster() + const { instance, ide } = data + const { id, state, url = ' ' } = instance || {} + const history = useHistory() + const { routes } = useAppContext() + const pathparamsList = instance?.space_path?.split('/') || [] + const projectIdentifier = pathparamsList[pathparamsList.length - 1] || '' + + return ( + { + e.preventDefault() + e.stopPropagation() + }}> + + { + history.push( + routes.toCDEGitspaceDetail({ + space: instance?.space_path || '', + gitspaceId: instance?.id || '' + }) + ) + }} + text={ + + + {getString('cde.viewGitspace')} + + } + /> + { + history.push( + routes.toCDEGitspacesEdit({ + space: instance?.space_path || '', + gitspaceId: instance?.id || '' + }) + ) + }} + text={ + + + {getString('cde.editGitspace')} + + } + /> + { + try { + if (!actionLoading) { + e.preventDefault() + e.stopPropagation() + await handleStartStop?.() + await refreshList() + } + } catch (error) { + showError(getErrorMessage(error)) + } + }} + text={ + + + + } + /> + {ide && state == GitspaceStatus.RUNNING && ( + { + e.preventDefault() + e.stopPropagation() + if (ide === IDEType.VSCODE) { + window.open(`vscode://harness-inc.gitspaces/${projectIdentifier}/${id}`, '_blank') + } else { + window.open(url, '_blank') + } + }} + text={ + + + + } + /> + )} + void} + text={ + + {deleteLoading ? <> : } + {getString('cde.deleteGitspace')} + + } + /> + + + ) +} + +interface RenderActionsProps extends CellProps { + refreshList: () => void +} + +export const RenderActions = ({ row, refreshList }: RenderActionsProps) => { + const { getString } = useStrings() + const { showError, showSuccess } = useToaster() + const details = row.original + const { instance, name } = details + const { mutate: deleteGitspace, loading: deleteLoading } = useDeleteGitspace({}) + + // To be added in BE later. + // const { mutate: actionGitspace, loading: actionLoading } = useGitspaceAction({ + // accountIdentifier, + // projectIdentifier, + // orgIdentifier, + // gitspaceIdentifier: instance?.id || '' + // }) + + // const handleStartStop = async () => { + // return await actionGitspace({ + // action: instance?.state === GitspaceStatus.RUNNING ? GitspaceActionType.STOP : GitspaceActionType.START + // }) + // } + + const confirmDelete = useConfirmAct() + + const handleDelete = async (e: React.MouseEvent) => { + confirmDelete({ + title: getString('cde.deleteGitspaceTitle'), + message: getString('cde.deleteGitspaceText', { name: name }), + action: async () => { + try { + e.preventDefault() + e.stopPropagation() + await deleteGitspace(instance?.id || '') + showSuccess(getString('cde.deleteSuccess')) + await refreshList() + } catch (exception) { + showError(getErrorMessage(exception)) + } + } + }) + } + + return ( + { + e.preventDefault() + e.stopPropagation() + }} + style={{ cursor: 'pointer' }} + icon={deleteLoading || false ? 'steps-spinner' : 'Options'} + tooltip={ + + } + tooltipProps={{ + interactionKind: PopoverInteractionKind.HOVER, + position: Position.BOTTOM_RIGHT, + usePortal: true, + popoverClassName: css.popover + }} + /> + ) +} + +export const ListGitspaces = ({ data, refreshList }: { data: TypesGitspaceConfig[]; refreshList: () => void }) => { + const history = useHistory() + const { getString } = useStrings() + const { routes } = useAppContext() + + return ( + + {data && ( + + className={css.table} + onRowClick={row => { + const pathparamsList = row?.instance?.space_path?.split('/') || [] + const projectIdentifier = pathparamsList[pathparamsList.length - 1] || '' + + if (row?.instance?.state === GitspaceStatus.RUNNING) { + if (row?.ide === IDEType.VSCODE) { + window.open(`vscode://harness-inc.gitspaces/${projectIdentifier}/${row?.instance?.id}`, '_blank') + } else { + window.open(row?.instance.url, '_blank') + } + } else { + history.push( + routes.toCDEGitspaceDetail({ + space: row?.instance?.space_path as string, + gitspaceId: row?.instance?.id as string + }) + ) + } + }} + columns={[ + { + id: 'gitspaces', + Header: getString('cde.gitspaces'), + Cell: RenderGitspaceName + }, + { + id: 'repository', + Header: getString('cde.repositoryAndBranch'), + Cell: RenderRepository + }, + { + id: 'status', + Header: getString('cde.status'), + Cell: RenderGitspaceStatus + }, + { + id: 'lastactivity', + Header: getString('cde.sessionDuration'), + Cell: RenderLastActivity + }, + { + id: 'action', + Cell: (props: RenderActionsProps) => + } + ]} + data={data} + /> + )} + + ) +} diff --git a/web/src/cde-gitness/components/RepositoryTypeButton/RepositoryTypeButton.module.scss b/web/src/cde-gitness/components/RepositoryTypeButton/RepositoryTypeButton.module.scss new file mode 100644 index 000000000..ae946d57b --- /dev/null +++ b/web/src/cde-gitness/components/RepositoryTypeButton/RepositoryTypeButton.module.scss @@ -0,0 +1,30 @@ +.split-button { + display: flex; + border: 1px solid var(--grey-200); + border-radius: 4px; + overflow: hidden; +} + +.split-button p { + flex-wrap: wrap; + padding: 4px 8px !important; + border: none; + align-self: center; + color: #1f2937 !important; + cursor: pointer; + font-size: 12px !important; + transition: background-color 0.3s; + + &:first-child { + border-right: 1px solid var(--grey-200); + } +} + +.split-button p.active { + background-color: var(--primary-1) !important; + color: var(--primary-8) !important; +} + +.split-button p:hover { + background-color: var(--white); +} diff --git a/web/src/cde-gitness/components/RepositoryTypeButton/RepositoryTypeButton.module.scss.d.ts b/web/src/cde-gitness/components/RepositoryTypeButton/RepositoryTypeButton.module.scss.d.ts new file mode 100644 index 000000000..67942fcdb --- /dev/null +++ b/web/src/cde-gitness/components/RepositoryTypeButton/RepositoryTypeButton.module.scss.d.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 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. + */ + +/* eslint-disable */ +// This is an auto-generated file +export declare const active: string +export declare const splitButton: string diff --git a/web/src/cde-gitness/components/RepositoryTypeButton/RepositoryTypeButton.tsx b/web/src/cde-gitness/components/RepositoryTypeButton/RepositoryTypeButton.tsx new file mode 100644 index 000000000..c85e19873 --- /dev/null +++ b/web/src/cde-gitness/components/RepositoryTypeButton/RepositoryTypeButton.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react' +import { Container, Text } from '@harnessio/uicore' +import { useStrings } from 'framework/strings' +import { useConfirmAct } from 'hooks/useConfirmAction' +import css from './RepositoryTypeButton.module.scss' + +export enum RepositoryType { + GITNESS = 'gitness', + THIRDPARTY = 'thirdParty' +} + +const RepositoryTypeButton = ({ + hasChange, + onChange +}: { + hasChange?: boolean + onChange: (type: RepositoryType) => void +}) => { + const { getString } = useStrings() + const confirmSwitch = useConfirmAct() + const [activeButton, setActiveButton] = useState(RepositoryType.GITNESS) + + const handleSwitch = (type: RepositoryType) => { + const onConfirm = () => { + setActiveButton(type) + onChange(type) + } + if (hasChange) { + confirmSwitch({ + title: getString('cde.create.unsaved.title'), + message: getString('cde.create.unsaved.message'), + action: async () => { + onConfirm() + } + }) + } else { + onConfirm() + } + } + + return ( + + { + handleSwitch(RepositoryType.GITNESS) + }}> + {getString('cde.create.gitnessRepositories')} + + { + handleSwitch(RepositoryType.THIRDPARTY) + }}> + {getString('cde.create.thirdPartyGitRepositories')} + + + ) +} + +export default RepositoryTypeButton diff --git a/web/src/cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm.module.scss b/web/src/cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm.module.scss new file mode 100644 index 000000000..f1b5d8534 --- /dev/null +++ b/web/src/cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm.module.scss @@ -0,0 +1,12 @@ +.hideContainer { + :global { + --bp3-intent-color: unset !important; + .bp3-form-helper-text { + margin-top: unset !important; + } + } +} + +.repoAndBranch { + width: 47% !important; +} diff --git a/web/src/cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm.module.scss.d.ts b/web/src/cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm.module.scss.d.ts new file mode 100644 index 000000000..ea39a05c1 --- /dev/null +++ b/web/src/cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm.module.scss.d.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 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. + */ + +/* eslint-disable */ +// This is an auto-generated file +export declare const hideContainer: string +export declare const repoAndBranch: string diff --git a/web/src/cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm.tsx b/web/src/cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm.tsx new file mode 100644 index 000000000..b1be9648a --- /dev/null +++ b/web/src/cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm.tsx @@ -0,0 +1,125 @@ +import { FormInput, FormikForm, Layout } from '@harnessio/uicore' +import React, { useState } from 'react' +import { useFormikContext } from 'formik' +import { useStrings } from 'framework/strings' +import { GitProviders, ImportFormData, getOrgLabel, getOrgPlaceholder, getProviders } from 'utils/GitUtils' +import css from './ThirdPartyRepoImportForm.module.scss' + +export interface ThirdPartyRepoImportFormProps extends ImportFormData { + branch: string + ide: string + id: string +} + +export const ThirdPartyRepoImportForm = () => { + const [auth, setAuth] = useState(false) + const { getString } = useStrings() + const { values, setFieldValue, validateField } = useFormikContext() + return ( + + + {![GitProviders.GITHUB, GitProviders.GITLAB, GitProviders.BITBUCKET, GitProviders.AZURE].includes( + values.gitProvider + ) && ( + + )} + + {values.gitProvider === GitProviders.AZURE && ( + + )} + + + { + const target = event.target as HTMLInputElement + setFieldValue('repo', target.value) + if (target.value) { + setFieldValue('name', target.value) + validateField('repo') + } + }} + /> + + + { + const target = event.target as HTMLInputElement + setFieldValue('branch', target.value) + }} + /> + + + + { + setAuth(!auth) + }} + style={auth ? {} : { margin: 0 }} + /> + + + {auth ? ( + <> + {[GitProviders.BITBUCKET, GitProviders.AZURE].includes(values.gitProvider) && ( + + )} + + + ) : null} + + ) +} diff --git a/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.constants.ts b/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.constants.ts new file mode 100644 index 000000000..ea8f52a72 --- /dev/null +++ b/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.constants.ts @@ -0,0 +1,28 @@ +import type { ThirdPartyRepoImportFormProps } from 'cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm' +import type { EnumIDEType, OpenapiCreateGitspaceRequest } from 'cde-gitness/services' +import { GitProviders } from 'utils/GitUtils' + +export const gitnessFormInitialValues: OpenapiCreateGitspaceRequest = { + branch: '', + code_repo_url: '', + devcontainer_path: '', + id: '', + ide: 'vsCode' as EnumIDEType, + infra_provider_resource_id: 'default', + name: '' +} +export const thirdPartyformInitialValues: ThirdPartyRepoImportFormProps = { + gitProvider: GitProviders.GITHUB, + hostUrl: '', + org: '', + project: '', + repo: '', + username: '', + password: '', + name: '', + description: '', + branch: '', + ide: 'vsCode' as EnumIDEType, + id: '', + importPipelineLabel: false +} diff --git a/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.module.scss b/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.module.scss new file mode 100644 index 000000000..c2ef7f09f --- /dev/null +++ b/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.module.scss @@ -0,0 +1,49 @@ +.main { + display: flex; + justify-content: center; + align-items: center; + + .cardMain { + width: 50%; + } + + .cardTitle { + font-size: 16px !important; + } +} + +.subContainers { + margin: var(--spacing-xlarge) 0 !important; + margin-bottom: 0px !important; +} + +.formDivider { + height: 2px !important; + border-top: 1px solid var(--grey-200) !important; +} + +.formTitleContainer { + padding: var(--spacing-medium) var(--spacing-large) !important; + border: 1px solid var(--grey-200); + border-radius: 5px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.formContainer { + border: 1px solid var(--grey-200); + border-radius: 5px; + padding: var(--spacing-medium) var(--spacing-large) !important; + border-top-right-radius: 0px; + border-top-left-radius: 0px; + border-top: none; +} + +.formOuterContainer { + padding: var(--spacing-medium) 0 !important; + padding-bottom: 0px !important; + + button { + margin-bottom: none; + } +} diff --git a/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.module.scss.d.ts b/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.module.scss.d.ts new file mode 100644 index 000000000..841fb6516 --- /dev/null +++ b/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.module.scss.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2023 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. + */ + +/* eslint-disable */ +// This is an auto-generated file +export declare const cardMain: string +export declare const cardTitle: string +export declare const formContainer: string +export declare const formDivider: string +export declare const formOuterContainer: string +export declare const formTitleContainer: string +export declare const main: string +export declare const subContainers: string diff --git a/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.tsx b/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.tsx new file mode 100644 index 000000000..a603bf23a --- /dev/null +++ b/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.tsx @@ -0,0 +1,123 @@ +import React, { useState } from 'react' +import { + Button, + ButtonVariation, + Card, + Container, + Formik, + FormikForm, + Layout, + Page, + Text, + useToaster +} from '@harnessio/uicore' +import { FontVariation } from '@harnessio/design-system' +import { useHistory } from 'react-router-dom' +import { useStrings } from 'framework/strings' +import { + ThirdPartyRepoImportForm, + ThirdPartyRepoImportFormProps +} from 'cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm' +import { GitnessRepoImportForm } from 'cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm' +import { SelectIDE } from 'cde/components/CreateGitspace/components/SelectIDE/SelectIDE' +import { useCreateGitspace, type OpenapiCreateGitspaceRequest } from 'cde-gitness/services' +import { useGetSpaceParam } from 'hooks/useGetSpaceParam' +import { getErrorMessage } from 'utils/Utils' +import { useAppContext } from 'AppContext' +import RepositoryTypeButton, { RepositoryType } from '../../components/RepositoryTypeButton/RepositoryTypeButton' +import { gitnessFormInitialValues, thirdPartyformInitialValues } from './GitspaceCreate.constants' +import { handleImportSubmit, validateGitnessForm, validationSchemaStepOne } from './GitspaceCreate.utils' +import css from './GitspaceCreate.module.scss' + +export const GitspaceCreate = () => { + const { getString } = useStrings() + const space = useGetSpaceParam() + const { routes } = useAppContext() + const history = useHistory() + const [activeButton, setActiveButton] = useState(RepositoryType.GITNESS) + const { showSuccess, showError } = useToaster() + const { mutate } = useCreateGitspace({}) + + return ( + <> + + + + + {getString('cde.createGitspace')} + + + { + try { + const payload = + activeButton === RepositoryType.GITNESS + ? data + : handleImportSubmit(data as ThirdPartyRepoImportFormProps) + await mutate({ ...payload, space_ref: space } as OpenapiCreateGitspaceRequest & { + space_ref?: string + }) + showSuccess(getString('cde.create.gitspaceCreateSuccess')) + history.push(routes.toCDEGitspaces({ space })) + } catch (error) { + showError(getString('cde.create.gitspaceCreateFailed')) + showError(getErrorMessage(error)) + } + }} + initialValues={ + activeButton === RepositoryType.GITNESS ? gitnessFormInitialValues : thirdPartyformInitialValues + } + validationSchema={ + activeButton === RepositoryType.GITNESS + ? validateGitnessForm(getString) + : validationSchemaStepOne(getString) + } + formName="importRepoForm" + enableReinitialize> + {formik => { + return ( + <> + + + {getString('cde.create.repositoryDetails')} + + { + setActiveButton(type) + formik?.resetForm() + }} + /> + + + + {activeButton === RepositoryType.GITNESS && ( + + + + )} + {activeButton === RepositoryType.THIRDPARTY && ( + + + + )} + + + + + + + + ) + }} + + + + + + ) +} diff --git a/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.utils.ts b/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.utils.ts new file mode 100644 index 000000000..87b9c12fc --- /dev/null +++ b/web/src/cde-gitness/pages/GitspaceCreate/GitspaceCreate.utils.ts @@ -0,0 +1,90 @@ +import * as yup from 'yup' +import { compact } from 'lodash-es' +import type { UseStringsReturn } from 'framework/strings' +import { GitProviders, getProviderTypeMapping } from 'utils/GitUtils' +import type { ThirdPartyRepoImportFormProps } from 'cde-gitness/components/ThirdPartyRepoImportForm/ThirdPartyRepoImportForm' + +export const validateGitnessForm = (getString: UseStringsReturn['getString']) => + yup.object().shape({ + branch: yup.string().trim().required(getString('cde.branchValidationMessage')), + code_repo_url: yup.string().trim().required(getString('cde.repoValidationMessage')), + id: yup.string().trim().required(), + ide: yup.string().trim().required(), + infra_provider_resource_id: yup.string().trim().required(getString('cde.machineValidationMessage')), + name: yup.string().trim().required() + }) + +export const validationSchemaStepOne = (getString: UseStringsReturn['getString']) => + yup.object().shape({ + gitProvider: yup.string().required(), + repo: yup + .string() + .trim() + .when('gitProvider', { + is: gitProvider => [GitProviders.GITHUB, GitProviders.GITLAB, GitProviders.BITBUCKET].includes(gitProvider), + then: yup.string().required(getString('importSpace.orgRequired')) + }), + branch: yup.string().trim().required(getString('cde.branchValidationMessage')), + hostUrl: yup + .string() + // .matches(MATCH_REPOURL_REGEX, getString('importSpace.invalidUrl')) + .when('gitProvider', { + is: gitProvider => + ![GitProviders.GITHUB, GitProviders.GITLAB, GitProviders.BITBUCKET, GitProviders.AZURE].includes(gitProvider), + then: yup.string().required(getString('importRepo.required')), + otherwise: yup.string().notRequired() // Optional based on your needs + }), + org: yup + .string() + .trim() + .when('gitProvider', { + is: GitProviders.AZURE, + then: yup.string().required(getString('importSpace.orgRequired')) + }), + project: yup + .string() + .trim() + .when('gitProvider', { + is: GitProviders.AZURE, + then: yup.string().required(getString('importSpace.spaceNameRequired')) + }), + name: yup.string().trim().required(getString('validation.nameIsRequired')) + }) + +export const handleImportSubmit = (formData: ThirdPartyRepoImportFormProps) => { + const type = getProviderTypeMapping(formData.gitProvider) + + const provider = { + type, + username: formData.username, + password: formData.password, + host: '' + } + + if ( + ![GitProviders.GITHUB, GitProviders.GITLAB, GitProviders.BITBUCKET, GitProviders.AZURE].includes( + formData.gitProvider + ) + ) { + provider.host = formData.hostUrl + } + + const importPayload = { + name: formData.repo || formData.name, + description: formData.description || '', + id: formData.repo, + provider, + ide: formData.ide, + branch: formData.branch, + infra_provider_resource_id: 'default', + provider_repo: compact([ + formData.org, + formData.gitProvider === GitProviders.AZURE ? formData.project : '', + formData.repo + ]) + .join('/') + .replace(/\.git$/, '') + } + + return importPayload +} diff --git a/web/src/cde-gitness/pages/GitspaceListing/GitspaceListing.tsx b/web/src/cde-gitness/pages/GitspaceListing/GitspaceListing.tsx new file mode 100644 index 000000000..961a819ab --- /dev/null +++ b/web/src/cde-gitness/pages/GitspaceListing/GitspaceListing.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import { + Button, + Page, + ButtonVariation, + Breadcrumbs, + HarnessDocTooltip, + Container, + Layout, + Text +} from '@harnessio/uicore' +import { FontVariation } from '@harnessio/design-system' +import { useHistory } from 'react-router-dom' +import { useGet } from 'restful-react' +import { useAppContext } from 'AppContext' +import { useStrings } from 'framework/strings' +import { useGetSpaceParam } from 'hooks/useGetSpaceParam' +import { LIST_FETCHING_LIMIT, PageBrowserProps, getErrorMessage } from 'utils/Utils' +import noSpace from 'cde/images/no-gitspace.svg?url' +import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination' +import { useQueryParams } from 'hooks/useQueryParams' +import { usePageIndex } from 'hooks/usePageIndex' +import { ListGitspaces } from 'cde-gitness/components/GitspaceListing/ListGitspaces' +import type { TypesGitspaceConfig } from 'cde-gitness/services' +import css from './GitspacesListing.module.scss' + +export const GitspaceListing = () => { + const space = useGetSpaceParam() + const history = useHistory() + const { getString } = useStrings() + const { routes } = useAppContext() + const pageBrowser = useQueryParams() + const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1 + const [page, setPage] = usePageIndex(pageInit) + + const { + data = '', + loading = false, + error = undefined, + refetch, + response + } = useGet({ + path: `/api/v1/spaces/${space}/+/gitspaces`, + queryParams: { page, limit: LIST_FETCHING_LIMIT }, + debounce: 500 + }) + + // useEffect(() => { + // if (!data && !loading) { + // history.push(routes.toCDEGitspacesCreate({ space })) + // } + // }, [data, loading]) + + return ( + <> + +

{getString('cde.gitspaces')}

+ + + } + breadcrumbs={ + + } + toolbar={ + + } + /> + + + + {getErrorMessage(error)} +