mirror of https://github.com/harness/drone.git
387 lines
14 KiB
TypeScript
387 lines
14 KiB
TypeScript
/*
|
|
* 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 React, { useEffect, useState } from 'react'
|
|
import { Dialog, Intent } from '@blueprintjs/core'
|
|
import * as yup from 'yup'
|
|
import {
|
|
Button,
|
|
ButtonProps,
|
|
Container,
|
|
Layout,
|
|
Text,
|
|
FlexExpander,
|
|
Formik,
|
|
FormikForm,
|
|
Heading,
|
|
useToaster,
|
|
FormInput,
|
|
ButtonVariation
|
|
} from '@harnessio/uicore'
|
|
import { Icon } from '@harnessio/icons'
|
|
import cx from 'classnames'
|
|
import { FontVariation, Color } from '@harnessio/design-system'
|
|
import { useMutate } from 'restful-react'
|
|
import { get } from 'lodash-es'
|
|
import { Render } from 'react-jsx-match'
|
|
import { useModalHook } from 'hooks/useModalHook'
|
|
import { useRuleViolationCheck } from 'hooks/useRuleViolationCheck'
|
|
import { String, useStrings } from 'framework/strings'
|
|
import { getErrorMessage } from 'utils/Utils'
|
|
import type { OpenapiCommitFilesRequest, TypesListCommitResponse } from 'services/code'
|
|
import { GitCommitAction, GitInfoProps, isGitBranchNameValid } from 'utils/GitUtils'
|
|
import css from './CommitModalButton.module.scss'
|
|
|
|
enum CommitToGitRefOption {
|
|
DIRECTLY = 'directly',
|
|
NEW_BRANCH = 'new-branch'
|
|
}
|
|
|
|
interface FormData {
|
|
title?: string
|
|
message?: string
|
|
branch?: string
|
|
newBranch?: string
|
|
}
|
|
|
|
interface CommitModalProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
|
commitAction: GitCommitAction
|
|
gitRef: string
|
|
resourcePath: string
|
|
commitTitlePlaceHolder: string
|
|
disableBranchCreation?: boolean
|
|
oldResourcePath?: string
|
|
payload?: string
|
|
sha?: string
|
|
onSuccess: (data: TypesListCommitResponse, newBranch?: string) => void
|
|
}
|
|
|
|
export function useCommitModal({
|
|
repoMetadata,
|
|
commitAction,
|
|
gitRef,
|
|
resourcePath,
|
|
commitTitlePlaceHolder,
|
|
oldResourcePath,
|
|
disableBranchCreation = false,
|
|
payload = '',
|
|
sha,
|
|
onSuccess
|
|
}: CommitModalProps) {
|
|
const ModalComponent: React.FC = () => {
|
|
const { getString } = useStrings()
|
|
const [targetBranchOption, setTargetBranchOption] = useState(CommitToGitRefOption.DIRECTLY)
|
|
const { showError, showSuccess } = useToaster()
|
|
const { violation, bypassable, bypassed, setAllStates, resetViolation } = useRuleViolationCheck()
|
|
const [disableCTA, setDisableCTA] = useState(false)
|
|
const { mutate, loading } = useMutate<TypesListCommitResponse>({
|
|
verb: 'POST',
|
|
path: `/api/v1/repos/${repoMetadata.path}/+/commits`
|
|
})
|
|
const { mutate: dryRunCall } = useMutate({
|
|
verb: 'POST',
|
|
path: `/api/v1/repos/${repoMetadata.path}/+/commits`
|
|
})
|
|
|
|
useEffect(() => {
|
|
dryRun(CommitToGitRefOption.DIRECTLY)
|
|
}, [])
|
|
|
|
const handleSubmit = (formData: FormData) => {
|
|
try {
|
|
const data: OpenapiCommitFilesRequest = {
|
|
actions: [
|
|
{
|
|
action: commitAction,
|
|
path: oldResourcePath || resourcePath,
|
|
payload: `${oldResourcePath ? `${resourcePath}\0` : ''}${payload}`,
|
|
sha
|
|
// encoding: 'base64',
|
|
// payload: window.btoa(payload || '')
|
|
}
|
|
],
|
|
branch: gitRef,
|
|
new_branch: targetBranchOption === CommitToGitRefOption.NEW_BRANCH ? formData.newBranch : '',
|
|
title: formData.title || commitTitlePlaceHolder,
|
|
message: formData.message,
|
|
bypass_rules: bypassed
|
|
}
|
|
|
|
mutate(data)
|
|
.then(response => {
|
|
hideModal()
|
|
onSuccess(response, targetBranchOption === CommitToGitRefOption.NEW_BRANCH ? formData.newBranch : '')
|
|
|
|
if (commitAction === GitCommitAction.DELETE) {
|
|
showSuccess(getString('fileDeleted').replace('__path__', resourcePath))
|
|
}
|
|
})
|
|
.catch(_error => {
|
|
if (_error.status === 422) {
|
|
setAllStates({
|
|
violation: true,
|
|
bypassed: true,
|
|
bypassable: _error?.data?.violations[0]?.bypassable
|
|
})
|
|
} else showError(getErrorMessage(_error), 0, getString('failedToCreateRepo'))
|
|
})
|
|
} catch (exception) {
|
|
showError(getErrorMessage(exception), 0, getString('failedToCreateRepo'))
|
|
}
|
|
}
|
|
|
|
const dryRun = async (targetBranch: CommitToGitRefOption) => {
|
|
resetViolation()
|
|
setDisableCTA(false)
|
|
if (targetBranch === CommitToGitRefOption.DIRECTLY) {
|
|
try {
|
|
const data: OpenapiCommitFilesRequest = {
|
|
actions: [
|
|
{
|
|
action: commitAction,
|
|
path: oldResourcePath || resourcePath,
|
|
payload: `${oldResourcePath ? `${resourcePath}\0` : ''}${payload}`,
|
|
sha
|
|
}
|
|
],
|
|
branch: gitRef,
|
|
new_branch: '',
|
|
title: '',
|
|
message: '',
|
|
bypass_rules: false,
|
|
dry_run_rules: true
|
|
}
|
|
|
|
const response = await dryRunCall(data)
|
|
|
|
if (response?.rule_violations?.length) {
|
|
setAllStates({
|
|
violation: true,
|
|
bypassed: true,
|
|
bypassable: response?.rule_violations[0]?.bypassable
|
|
})
|
|
setDisableCTA(!response?.rule_violations[0]?.bypassable)
|
|
}
|
|
} catch (exception) {
|
|
showError(getErrorMessage(exception), 0, getString('failedToCreateRepo'))
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
isOpen
|
|
enforceFocus={false}
|
|
onClose={hideModal}
|
|
title={''}
|
|
style={{ width: 700, maxHeight: '95vh', overflow: 'auto' }}>
|
|
<Layout.Vertical className={cx(css.main)}>
|
|
<Heading level={3} font={{ variation: FontVariation.H3 }} margin={{ bottom: 'xlarge' }}>
|
|
{getString('commitChanges')}
|
|
</Heading>
|
|
|
|
<Container margin={{ right: 'xxlarge' }}>
|
|
<Formik<FormData>
|
|
initialValues={{
|
|
title: '',
|
|
message: '',
|
|
branch: CommitToGitRefOption.DIRECTLY,
|
|
newBranch: ''
|
|
}}
|
|
formName="commitChanges"
|
|
enableReinitialize={true}
|
|
validationSchema={yup.object().shape({
|
|
newBranch: yup
|
|
.string()
|
|
.trim()
|
|
.test('valid-branch-name', getString('validation.gitBranchNameInvalid'), value => {
|
|
if (targetBranchOption === CommitToGitRefOption.NEW_BRANCH) {
|
|
const val = value || ''
|
|
return !!val && isGitBranchNameValid(val)
|
|
}
|
|
return true
|
|
})
|
|
})}
|
|
validateOnChange
|
|
validateOnBlur
|
|
onSubmit={handleSubmit}>
|
|
<FormikForm>
|
|
<FormInput.Text
|
|
name="title"
|
|
label={getString('commitMessage')}
|
|
placeholder={commitTitlePlaceHolder}
|
|
tooltipProps={{
|
|
dataTooltipId: 'commitMessage'
|
|
}}
|
|
inputGroup={{ autoFocus: true }}
|
|
/>
|
|
<FormInput.TextArea
|
|
className={css.extendedDescription}
|
|
name="message"
|
|
placeholder={getString('optionalExtendedDescription')}
|
|
/>
|
|
<Container
|
|
className={cx(
|
|
css.radioGroup,
|
|
targetBranchOption === CommitToGitRefOption.DIRECTLY ? css.directly : css.newBranch
|
|
)}>
|
|
<FormInput.RadioGroup
|
|
name="branch"
|
|
disabled={disableBranchCreation}
|
|
label=""
|
|
onChange={e => {
|
|
setTargetBranchOption(get(e.target, 'defaultValue') as unknown as CommitToGitRefOption)
|
|
dryRun(get(e.target, 'defaultValue') as unknown as CommitToGitRefOption)
|
|
}}
|
|
items={[
|
|
{
|
|
label: (
|
|
<Layout.Horizontal className={css.warningMessageLayout}>
|
|
<String stringID="commitDirectlyTo" vars={{ gitRef }} useRichText />
|
|
<Render when={violation && targetBranchOption === CommitToGitRefOption.DIRECTLY}>
|
|
<Layout.Horizontal className={css.warningMessage}>
|
|
<Icon intent={Intent.WARNING} name="danger-icon" size={16} />
|
|
<Text font={{ variation: FontVariation.BODY2 }} color={Color.RED_800}>
|
|
{bypassable
|
|
? getString('branchProtection.commitDirectlyAlertText')
|
|
: getString('branchProtection.commitDirectlyBlockText')}
|
|
</Text>
|
|
</Layout.Horizontal>
|
|
</Render>
|
|
</Layout.Horizontal>
|
|
),
|
|
value: CommitToGitRefOption.DIRECTLY
|
|
},
|
|
{
|
|
label: (
|
|
<Layout.Horizontal className={css.warningMessageLayout}>
|
|
<String stringID="commitToNewBranch" useRichText />
|
|
<Render when={violation && targetBranchOption === CommitToGitRefOption.NEW_BRANCH}>
|
|
<Layout.Horizontal className={css.warningMessage}>
|
|
<Icon intent={Intent.WARNING} name="danger-icon" size={16} />
|
|
<Text font={{ variation: FontVariation.BODY2 }} color={Color.RED_800}>
|
|
{bypassable
|
|
? getString('branchProtection.commitNewBranchAlertText')
|
|
: getString('branchProtection.commitNewBranchBlockText')}
|
|
</Text>
|
|
</Layout.Horizontal>
|
|
</Render>
|
|
</Layout.Horizontal>
|
|
),
|
|
value: CommitToGitRefOption.NEW_BRANCH
|
|
}
|
|
]}
|
|
/>
|
|
{targetBranchOption === CommitToGitRefOption.NEW_BRANCH && (
|
|
<Container>
|
|
<Layout.Horizontal spacing="medium" className={css.newBranchContainer}>
|
|
<Icon name="git-branch" />
|
|
<FormInput.Text
|
|
name="newBranch"
|
|
placeholder={getString('enterNewBranchName')}
|
|
tooltipProps={{
|
|
dataTooltipId: 'enterNewBranchName'
|
|
}}
|
|
inputGroup={{ autoFocus: true }}
|
|
onChange={() => {
|
|
setAllStates({ violation: false, bypassable: false, bypassed: false })
|
|
}}
|
|
/>
|
|
</Layout.Horizontal>
|
|
</Container>
|
|
)}
|
|
</Container>
|
|
|
|
<Layout.Horizontal spacing="small" padding={{ right: 'xxlarge', top: 'xxlarge', bottom: 'large' }}>
|
|
{!bypassable ? (
|
|
<Button
|
|
type="submit"
|
|
variation={ButtonVariation.PRIMARY}
|
|
text={getString('commit')}
|
|
disabled={loading || disableCTA}
|
|
/>
|
|
) : (
|
|
<Button
|
|
intent={Intent.DANGER}
|
|
disabled={loading}
|
|
type="submit"
|
|
variation={ButtonVariation.SECONDARY}
|
|
text={
|
|
targetBranchOption === CommitToGitRefOption.NEW_BRANCH
|
|
? getString('branchProtection.commitNewBranchAlertBtn')
|
|
: getString('branchProtection.commitDirectlyAlertBtn')
|
|
}
|
|
/>
|
|
)}
|
|
<Button text={getString('cancel')} variation={ButtonVariation.LINK} onClick={hideModal} />
|
|
<FlexExpander />
|
|
|
|
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
|
|
</Layout.Horizontal>
|
|
</FormikForm>
|
|
</Formik>
|
|
</Container>
|
|
</Layout.Vertical>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
const [openModal, hideModal] = useModalHook(ModalComponent, [onSuccess, gitRef, resourcePath, commitTitlePlaceHolder])
|
|
|
|
return [openModal, hideModal]
|
|
}
|
|
|
|
interface CommitModalButtonProps extends Omit<ButtonProps, 'onClick' | 'onSubmit'>, Pick<GitInfoProps, 'repoMetadata'> {
|
|
commitAction: GitCommitAction
|
|
gitRef: string
|
|
resourcePath: string
|
|
commitTitlePlaceHolder: string
|
|
disableBranchCreation?: boolean
|
|
oldResourcePath?: string
|
|
payload?: string
|
|
sha?: string
|
|
onSuccess: (data: TypesListCommitResponse, newBranch?: string) => void
|
|
}
|
|
|
|
export const CommitModalButton: React.FC<CommitModalButtonProps> = ({
|
|
repoMetadata,
|
|
commitAction,
|
|
gitRef,
|
|
resourcePath,
|
|
commitTitlePlaceHolder,
|
|
oldResourcePath,
|
|
disableBranchCreation = false,
|
|
payload = '',
|
|
sha,
|
|
onSuccess,
|
|
...props
|
|
}) => {
|
|
const [openModal] = useCommitModal({
|
|
repoMetadata,
|
|
commitAction,
|
|
gitRef,
|
|
resourcePath,
|
|
commitTitlePlaceHolder,
|
|
oldResourcePath,
|
|
disableBranchCreation,
|
|
payload,
|
|
sha,
|
|
onSuccess
|
|
})
|
|
|
|
return <Button onClick={openModal} {...props} />
|
|
}
|