feat: [CDE-251]: SSH Dropdown (#2576)

* feat: [CDE-251]: Updated harness code icon
* feat: [CDE-251]: SSH Token creation modal
* feat: [CDE-251]: SSH Dropdown
pull/3545/head
Deepesh Kumar 2024-08-23 13:21:40 +00:00 committed by Harness
parent 9ed0df9e92
commit b83779350d
10 changed files with 354 additions and 7 deletions

View File

@ -73,6 +73,9 @@ export interface AppProps {
useFeatureFlags: Unknown
useGetSettingValue: Unknown
useGetUserSourceCodeManagers?: Unknown
useListAggregatedTokens?: Unknown
useDeleteToken?: Unknown
useCreateToken?: Unknown
}>
currentUser: Required<TypesUser>

View File

@ -1 +1 @@
<svg width="32" height="33" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.6 29.7c7.07 0 12.8-5.73 12.8-12.8S22.67 4.1 15.6 4.1c-2.029 0-3.947.472-5.652 1.312l3.473 3.28c.39-.269.859-.426 1.364-.426 1.35 0 2.444 1.12 2.444 2.5 0 .446-.114.864-.314 1.227l3.357 3.171a2.389 2.389 0 0 1 1.03-.232c1.35 0 2.443 1.12 2.443 2.5s-1.094 2.5-2.443 2.5c-1.35 0-2.444-1.12-2.444-2.5 0-.395.09-.768.249-1.1l-3.428-3.239c-.193.078-.396.13-.609.156l-1.816 8.744a2.512 2.512 0 0 1 1.124 2.104c0 1.38-1.094 2.5-2.444 2.5-1.35 0-2.443-1.12-2.443-2.5 0-1.288.953-2.349 2.177-2.485l1.813-8.732a2.512 2.512 0 0 1-1.14-2.114c0-.253.037-.496.105-.726L8.46 6.275A12.787 12.787 0 0 0 2.8 16.9c0 7.07 5.73 12.8 12.8 12.8Z" fill="url(#a)"/><defs><linearGradient id="a" x1="8.628" y1="6.165" x2="22.571" y2="27.635" gradientUnits="userSpaceOnUse"><stop stop-color="#FF7F77"/><stop offset="0" stop-color="#FD7A64"/><stop offset="1" stop-color="#E62D14"/></linearGradient></defs></svg>
<svg width="563" height="563" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M25.74 219.359c-34.32 34.319-34.32 89.963 0 124.281l193.619 193.621c34.319 34.319 89.963 34.319 124.281 0L537.261 343.64c34.319-34.318 34.319-89.962 0-124.281L343.64 25.739c-34.318-34.319-89.962-34.319-124.281 0L25.739 219.36Zm166.753-8.686c6.864-6.864 6.864-17.994 0-24.857-6.864-6.864-17.994-6.864-24.857 0l-83.255 83.256c-6.864 6.864-6.864 17.992 0 24.856l83.255 83.256c6.863 6.864 17.993 6.864 24.857 0 6.864-6.865 6.864-17.993 0-24.857L121.665 281.5l70.828-70.827Zm174.954-24.857c-6.863 6.863-6.863 17.993 0 24.857l70.827 70.827-70.827 70.827c-6.863 6.864-6.863 17.992 0 24.857 6.864 6.864 17.992 6.864 24.856 0l83.256-83.256c6.864-6.864 6.864-17.992 0-24.856l-83.256-83.256c-6.864-6.864-17.992-6.864-24.856 0ZM330.17 169.77c2.667-9.334-2.737-19.061-12.07-21.728-9.335-2.667-19.062 2.737-21.729 12.072l-66.603 233.114c-2.667 9.334 2.738 19.061 12.072 21.728 9.333 2.667 19.06-2.737 21.727-12.07L330.17 169.77Z" fill="url(#a)"/><defs><linearGradient id="a" x1="128.184" y1="45.414" x2="434.816" y2="517.585" gradientUnits="userSpaceOnUse"><stop stop-color="#73DFE7"/><stop offset="1" stop-color="#0095F7"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1009 B

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M11.995 4.25a3.754 3.754 0 0 0-3.4 5.35.25.25 0 0 1-.05.284L4.25 14.123v1.627h1.481v-1.101a.25.25 0 0 1 .25-.25h1.076v-1.101a.25.25 0 0 1 .25-.25H8.43v-1.031a.25.25 0 0 1 .25-.25h1.079v-.285a.25.25 0 0 1 .374-.217 3.754 3.754 0 1 0 1.863-7.015Zm-1.736 7.64A4.254 4.254 0 1 0 8.07 9.65l-4.246 4.19a.25.25 0 0 0-.075.178V16c0 .138.112.25.25.25h1.981a.25.25 0 0 0 .25-.25v-1.101h1.076a.25.25 0 0 0 .25-.25v-1.101H8.68a.25.25 0 0 0 .25-.25v-1.031h1.079a.25.25 0 0 0 .25-.25v-.128Zm2.871-5.628a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm-1.25.75a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0Z" fill="#6B6D85"/></svg>

After

Width:  |  Height:  |  Size: 728 B

View File

@ -0,0 +1,26 @@
.note {
display: flex !important;
align-items: center;
background: var(--blue-100) !important;
padding: var(--spacing-small) 0 !important;
justify-content: center;
}
.sshKeyValue {
:global {
textarea {
height: 108px !important;
resize: vertical;
}
}
}
.main {
width: 35% !important;
}
.addsshButton {
width: 100%;
justify-content: flex-start;
font-size: var(--font-size-small) !important;
}

View File

@ -0,0 +1,22 @@
/*
* 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 addsshButton: string
export declare const main: string
export declare const note: string
export declare const sshKeyValue: string

View File

@ -0,0 +1,246 @@
import React from 'react'
import * as Yup from 'yup'
import {
Button,
ButtonVariation,
Container,
Dialog,
Formik,
FormikForm,
FormInput,
Layout,
Text,
useToaster
} from '@harnessio/uicore'
import { useFormikContext } from 'formik'
import { Menu, MenuItem } from '@blueprintjs/core'
import { Color } from '@harnessio/design-system'
import { useAppContext } from 'AppContext'
import Secret from 'cde-gitness/assests/secret.svg?url'
import { useGetCDEAPIParams } from 'cde-gitness/hooks/useGetCDEAPIParams'
import type { OpenapiCreateGitspaceRequest } from 'services/cde'
import { useModalHook } from 'hooks/useModalHook'
import { useStrings } from 'framework/strings'
import { useConfirmAct } from 'hooks/useConfirmAction'
import { getErrorMessage } from 'utils/Utils'
import { CDECustomDropdown } from '../CDECustomDropdown/CDECustomDropdown'
import { getSelectedExpirationDate } from './CDESSHSelect.utils'
import css from './CDESSHSelect.module.scss'
export const CDESSHSelect = () => {
const { getString } = useStrings()
const { showError } = useToaster()
const { values, setFieldValue } = useFormikContext<OpenapiCreateGitspaceRequest>()
const { accountIdentifier } = useGetCDEAPIParams()
const { hooks, currentUser } = useAppContext()
const { useListAggregatedTokens, useDeleteToken, useCreateToken } = hooks
const { data, loading, refetch } = useListAggregatedTokens({
queryParams: {
accountIdentifier,
apiKeyType: 'SSH_KEY'
}
})
const { mutate: createToken } = useCreateToken({
queryParams: { accountIdentifier }
})
const { mutate: deleteToken } = useDeleteToken({
queryParams: {
accountIdentifier,
apiKeyType: 'SSH_KEY',
parentIdentifier: currentUser.uid || 'cWQVVVCET4iwomT4hRoj4w',
apiKeyIdentifier: `cdesshkey`
}
})
const tokenList = data?.data?.content.map((item: { token: any }) => item.token) || []
const [openModal, hideModal] = useModalHook(() => {
const expiryOptions = [
{ label: getString('cde.sshSelect.30days'), value: '30' },
{ label: getString('cde.sshSelect.90days'), value: '90' },
{ label: getString('cde.sshSelect.180days'), value: '180' },
{ label: getString('cde.sshSelect.noexpiration'), value: '-1' }
]
return (
<Dialog isOpen onClose={hideModal} title="Add SSH Key" className={css.main}>
<Layout.Vertical spacing="large">
<Text margin="larg">Add an SSH key for secure access to Gitspaces via SSH.</Text>
<Layout.Horizontal className={css.note} spacing="small">
<Text icon="info-messaging" font="small" iconProps={{ size: 24 }}>
SSH key are used to connect securely to workspaces
</Text>
<Text font="small" color={Color.PRIMARY_5}>
Learn how to create an SSH Key
</Text>
</Layout.Horizontal>
<Formik<{
sshKeyName: string
sshKeyValue: string
expiryDate: string
expiry: string
}>
formName="sshCreate"
onSubmit={async value => {
try {
await createToken({
identifier: value?.sshKeyName.trim(),
name: value?.sshKeyName.trim(),
description: '',
tags: {},
accountIdentifier,
apiKeyIdentifier: `cdesshkey`,
parentIdentifier: currentUser.uid || 'cWQVVVCET4iwomT4hRoj4w',
apiKeyType: 'SSH_KEY',
sshKeyContent: value.sshKeyValue,
sshKeyUsage: ['AUTH'],
validTo: Date.parse(value?.expiryDate),
validFrom: new Date().getTime()
})
setFieldValue('ssh_token_identifier', value?.sshKeyName.trim())
hideModal()
refetch()
} catch (error) {
showError(getErrorMessage(error))
hideModal()
}
}}
initialValues={{
sshKeyName: '',
sshKeyValue: '',
expiryDate: getSelectedExpirationDate('30'),
expiry: '30'
}}
validationSchema={Yup.object().shape({
sshKeyName: Yup.string().required(),
sshKeyValue: Yup.string().required()
})}>
{formikProps => {
return (
<FormikForm>
<FormInput.Text name="sshKeyName" label="Key Name" />
<FormInput.DropDown
items={expiryOptions}
name="expiry"
dropDownProps={{
filterable: false,
minWidth: 100
}}
label={getString('expirationDate')}
onChange={item => {
formikProps.setFieldValue('expiryDate', getSelectedExpirationDate(item.value.toString()))
}}
/>
<FormInput.TextArea
placeholder={`Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'`}
name="sshKeyValue"
label="SSH Key"
isOptional
className={css.sshKeyValue}
/>
<Layout.Horizontal spacing="large">
<Button variation={ButtonVariation.PRIMARY} type="submit">
Add Key
</Button>
<Button variation={ButtonVariation.TERTIARY}>Cancel</Button>
</Layout.Horizontal>
</FormikForm>
)
}}
</Formik>
</Layout.Vertical>
</Dialog>
)
}, [accountIdentifier])
const confirmDelete = useConfirmAct()
const handleDelete = async (e: React.MouseEvent<HTMLElement, MouseEvent>, tokenId: string): Promise<void> => {
e.stopPropagation()
confirmDelete({
intent: 'danger',
title: getString('cde.sshSelect.deleteToken', { name: tokenId }),
message: getString('cde.deleteGitspaceText'),
confirmText: getString('delete'),
action: async () => {
try {
await deleteToken(tokenId || '', {
headers: { 'content-type': 'application/json' }
})
refetch()
} catch (err) {
showError(getErrorMessage(err))
}
}
})
}
return (
<CDECustomDropdown
leftElement={
<Layout.Horizontal>
<img src={Secret} height={20} width={20} style={{ marginRight: '8px', alignItems: 'center' }} />
<Layout.Vertical spacing="small">
<Text>SSH Key </Text>
<Text font="small" width="56%">
By default we will create the SSH key used to login to the Gitspace. You can add keys under Preferences in
User Settings
</Text>
</Layout.Vertical>
</Layout.Horizontal>
}
label={
<Text icon={loading ? 'loading' : undefined}>{values?.ssh_token_identifier || '-- Select SSH Key --'}</Text>
}
menu={
<Menu>
<Container border={{ bottom: true }}>
{tokenList.length ? (
<>
{tokenList.map((item: { name: string; identifier: string; apiKeyIdentifier: string }) => {
return (
<MenuItem
key={item.identifier}
text={
<Text
width="100%"
flex={{ justifyContent: 'space-between' }}
rightIcon="cross"
rightIconProps={{
size: 12,
onClick: e => {
handleDelete(e, item.identifier)
setFieldValue('ssh_token_identifier', undefined)
}
}}>
{item.name}
</Text>
}
onClick={() => {
setFieldValue('ssh_token_identifier', item.identifier)
}}
/>
)
})}
</>
) : (
<Text padding="small">
There are no keys configured. By default we will create a SSH key to login into Gitspace.
</Text>
)}
</Container>
<Button
variation={ButtonVariation.LINK}
onClick={() => {
openModal()
}}
className={css.addsshButton}>
Add SSH Key
</Button>
</Menu>
}
/>
)
}

View File

@ -0,0 +1,23 @@
import moment from 'moment'
export const DATE_FORMAT = 'MM/DD/YYYY'
export const getReadableDateTime = (timestamp?: number, formatString = 'MMM DD, YYYY'): string => {
if (!timestamp) {
return ''
}
return moment(timestamp).format(formatString)
}
export const getSelectedExpirationDate = (value: string): string => {
switch (value) {
case '30':
case '90':
case '180':
return getReadableDateTime(new Date().setDate(new Date().getDate() + parseInt(value)), DATE_FORMAT)
case '-1':
return '12/31/2099'
default:
return ''
}
}

View File

@ -13,8 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import React, { useEffect, useState } from 'react'
import { Button, ButtonVariation, Container, Formik, FormikForm, Layout, Text, useToaster } from '@harnessio/uicore'
import { useHistory } from 'react-router-dom'
import { Color, FontVariation } from '@harnessio/design-system'
@ -37,6 +36,8 @@ import { OpenapiCreateGitspaceRequest, useCreateGitspace } from 'services/cde'
import { useGetCDEAPIParams } from 'cde-gitness/hooks/useGetCDEAPIParams'
import { GitnessRepoImportForm } from 'cde-gitness/components/GitnessRepoImportForm/GitnessRepoImportForm'
import { EnumGitspaceCodeRepoType } from 'cde-gitness/constants'
import { CDESSHSelect } from 'cde-gitness/components/CDESSHSelect/CDESSHSelect'
import { useQueryParams } from 'hooks/useQueryParams'
import { gitnessFormInitialValues } from './GitspaceCreate.constants'
import { validateGitnessForm } from './GitspaceCreate.utils'
import css from './GitspaceCreate.module.scss'
@ -56,11 +57,14 @@ export const CDECreateGitspace = () => {
const { accountIdentifier = '', orgIdentifier = '', projectIdentifier = '' } = useGetCDEAPIParams()
const { showSuccess, showError } = useToaster()
const { mutate } = useCreateGitspace({ accountIdentifier, orgIdentifier, projectIdentifier })
const { idpRepoURL = '' } = useQueryParams<{ idpRepoURL?: string }>()
const [repoURLviaQueryParam, setrepoURLviaQueryParam] = useState('')
const scmOptions: SCMType[] = [
{ name: 'Harness Code', value: EnumGitspaceCodeRepoType.HARNESS_CODE, icon: harnessCode },
{ name: 'Github', value: EnumGitspaceCodeRepoType.GITHUB, icon: github },
{ name: 'Gitlab', value: EnumGitspaceCodeRepoType.GITLAB, icon: gitlab },
{ name: 'GitHub Cloud', value: EnumGitspaceCodeRepoType.GITHUB, icon: github },
{ name: 'GitLab Cloud', value: EnumGitspaceCodeRepoType.GITLAB, icon: gitlab },
{ name: 'Bitbucket', value: EnumGitspaceCodeRepoType.BITBUCKET, icon: bitbucket },
{ name: 'Any public Git repository', value: EnumGitspaceCodeRepoType.UNKNOWN, icon: genericGit }
]
@ -69,6 +73,12 @@ export const CDECreateGitspace = () => {
queryParams: { accountIdentifier, userIdentifier: currentUser?.uid }
})
useEffect(() => {
if (idpRepoURL !== repoURLviaQueryParam) {
setrepoURLviaQueryParam(idpRepoURL)
}
}, [idpRepoURL])
const oauthSCMsListTypes =
OauthSCMs?.data?.userSourceCodeManagerResponseDTOList?.map((item: { type: string }) => item.type.toLowerCase()) ||
[]
@ -96,7 +106,11 @@ export const CDECreateGitspace = () => {
showError(getErrorMessage(error))
}
}}
initialValues={{ ...gitnessFormInitialValues, code_repo_type: EnumGitspaceCodeRepoType.HARNESS_CODE }}
initialValues={{
...gitnessFormInitialValues,
code_repo_url: idpRepoURL,
code_repo_type: EnumGitspaceCodeRepoType.HARNESS_CODE
}}
validationSchema={validateGitnessForm(getString)}
formName="importRepoForm"
enableReinitialize>
@ -204,6 +218,7 @@ export const CDECreateGitspace = () => {
</Container>
<Container className={css.formOuterContainer}>
<CDEIDESelect onChange={formik.setFieldValue} selectedIde={formik.values.ide} />
<CDESSHSelect />
<SelectInfraProvider />
<Button width={'100%'} variation={ButtonVariation.PRIMARY} height={50} type="submit">
{getString('cde.createGitspace')}

View File

@ -262,6 +262,11 @@ export interface StringsMap {
'cde.repositoryAndBranch': string
'cde.retry': string
'cde.sessionDuration': string
'cde.sshSelect.180days': string
'cde.sshSelect.30days': string
'cde.sshSelect.90days': string
'cde.sshSelect.deleteToken': string
'cde.sshSelect.noexpiration': string
'cde.startingGitspace': string
'cde.status': string
'cde.stopingGitspace': string

View File

@ -1152,7 +1152,7 @@ cde:
na: NA
retry: Retry
eventTimeline: Event Timeline
cpu: Cpu
cpu: CPU
memory: Memory
disk: Disk
regionSelectWarning: Select a region first
@ -1173,6 +1173,12 @@ cde:
status: Status
getStarted: Get started with Gitspaces by
createImport: creating or importing repositories
sshSelect:
30days: 30 Days
90days: 90 Days
180days: 180 Days
noexpiration: No Expiration
deleteToken: Delete SSH Token '{{name}}'
homePage:
start: Start
noSetupRequired: no setup required