[CODE-1116] UI : Protection rules integration with Branch Creation/Deletion and Commits (#824)

This commit is contained in:
Ritik Kapoor 2023-11-28 07:06:54 +00:00 committed by Harness
parent 5b16c72d4a
commit 274a5a01ab
16 changed files with 375 additions and 56 deletions

View File

@ -56,6 +56,18 @@
font-size: var(--form-input-font-size); font-size: var(--form-input-font-size);
font-weight: 500; font-weight: 500;
} }
.warningMessageLayout {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.warningMessage {
display: flex;
gap: 0.5rem;
align-items: center;
}
} }
.newBranchContainer { .newBranchContainer {

View File

@ -22,3 +22,5 @@ export declare const main: string
export declare const newBranch: string export declare const newBranch: string
export declare const newBranchContainer: string export declare const newBranchContainer: string
export declare const radioGroup: string export declare const radioGroup: string
export declare const warningMessage: string
export declare const warningMessageLayout: string

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import { Dialog, Intent } from '@blueprintjs/core' import { Dialog, Intent } from '@blueprintjs/core'
import * as yup from 'yup' import * as yup from 'yup'
import { import {
@ -22,6 +22,7 @@ import {
ButtonProps, ButtonProps,
Container, Container,
Layout, Layout,
Text,
FlexExpander, FlexExpander,
Formik, Formik,
FormikForm, FormikForm,
@ -32,10 +33,12 @@ import {
} from '@harnessio/uicore' } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons' import { Icon } from '@harnessio/icons'
import cx from 'classnames' import cx from 'classnames'
import { FontVariation } from '@harnessio/design-system' import { FontVariation, Color } from '@harnessio/design-system'
import { useMutate } from 'restful-react' import { useMutate } from 'restful-react'
import { get } from 'lodash-es' import { get } from 'lodash-es'
import { Render } from 'react-jsx-match'
import { useModalHook } from 'hooks/useModalHook' import { useModalHook } from 'hooks/useModalHook'
import { useRuleViolationCheck } from 'hooks/useRuleViolationCheck'
import { String, useStrings } from 'framework/strings' import { String, useStrings } from 'framework/strings'
import { getErrorMessage } from 'utils/Utils' import { getErrorMessage } from 'utils/Utils'
import type { OpenapiCommitFilesRequest, TypesListCommitResponse } from 'services/code' import type { OpenapiCommitFilesRequest, TypesListCommitResponse } from 'services/code'
@ -82,10 +85,20 @@ export function useCommitModal({
const { getString } = useStrings() const { getString } = useStrings()
const [targetBranchOption, setTargetBranchOption] = useState(CommitToGitRefOption.DIRECTLY) const [targetBranchOption, setTargetBranchOption] = useState(CommitToGitRefOption.DIRECTLY)
const { showError, showSuccess } = useToaster() const { showError, showSuccess } = useToaster()
const { violation, bypassable, bypassed, setAllStates, resetViolation } = useRuleViolationCheck()
const [disableCTA, setDisableCTA] = useState(false)
const { mutate, loading } = useMutate<TypesListCommitResponse>({ const { mutate, loading } = useMutate<TypesListCommitResponse>({
verb: 'POST', verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/commits` 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) => { const handleSubmit = (formData: FormData) => {
try { try {
@ -101,28 +114,73 @@ export function useCommitModal({
} }
], ],
branch: gitRef, branch: gitRef,
new_branch: formData.newBranch, new_branch: targetBranchOption === CommitToGitRefOption.NEW_BRANCH ? formData.newBranch : '',
title: formData.title || commitTitlePlaceHolder, title: formData.title || commitTitlePlaceHolder,
message: formData.message message: formData.message,
bypass_rules: bypassed
} }
mutate(data) mutate(data)
.then(response => { .then(response => {
hideModal() hideModal()
onSuccess(response, formData.newBranch) onSuccess(response, targetBranchOption === CommitToGitRefOption.NEW_BRANCH ? formData.newBranch : '')
if (commitAction === GitCommitAction.DELETE) { if (commitAction === GitCommitAction.DELETE) {
showSuccess(getString('fileDeleted').replace('__path__', resourcePath)) showSuccess(getString('fileDeleted').replace('__path__', resourcePath))
} }
}) })
.catch(_error => { .catch(_error => {
showError(getErrorMessage(_error), 0, getString('failedToCreateRepo')) if (_error.status === 422) {
setAllStates({
violation: true,
bypassed: true,
bypassable: _error?.data?.violations[0]?.bypassable
})
} else showError(getErrorMessage(_error), 0, getString('failedToCreateRepo'))
}) })
} catch (exception) { } catch (exception) {
showError(getErrorMessage(exception), 0, getString('failedToCreateRepo')) 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 ? `file://${resourcePath}\n` : ''}${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: false,
bypassable: response?.rule_violations[0]?.bypassable
})
setDisableCTA(!response?.rule_violations[0]?.bypassable)
}
} catch (exception) {
showError(getErrorMessage(exception), 0, getString('failedToCreateRepo'))
}
}
}
return ( return (
<Dialog <Dialog
isOpen isOpen
@ -186,14 +244,43 @@ export function useCommitModal({
label="" label=""
onChange={e => { onChange={e => {
setTargetBranchOption(get(e.target, 'defaultValue') as unknown as CommitToGitRefOption) setTargetBranchOption(get(e.target, 'defaultValue') as unknown as CommitToGitRefOption)
dryRun(get(e.target, 'defaultValue') as unknown as CommitToGitRefOption)
}} }}
items={[ items={[
{ {
label: <String stringID="commitDirectlyTo" vars={{ gitRef }} useRichText />, 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.commitDirectlyBlockText')
: getString('branchProtection.commitNewBranchBlockText')}
</Text>
</Layout.Horizontal>
</Render>
</Layout.Horizontal>
),
value: CommitToGitRefOption.DIRECTLY value: CommitToGitRefOption.DIRECTLY
}, },
{ {
label: <String stringID="commitToNewBranch" useRichText />, 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 value: CommitToGitRefOption.NEW_BRANCH
} }
]} ]}
@ -209,6 +296,9 @@ export function useCommitModal({
dataTooltipId: 'enterNewBranchName' dataTooltipId: 'enterNewBranchName'
}} }}
inputGroup={{ autoFocus: true }} inputGroup={{ autoFocus: true }}
onChange={() => {
setAllStates({ violation: false, bypassable: false, bypassed: false })
}}
/> />
</Layout.Horizontal> </Layout.Horizontal>
</Container> </Container>
@ -216,12 +306,22 @@ export function useCommitModal({
</Container> </Container>
<Layout.Horizontal spacing="small" padding={{ right: 'xxlarge', top: 'xxlarge', bottom: 'large' }}> <Layout.Horizontal spacing="small" padding={{ right: 'xxlarge', top: 'xxlarge', bottom: 'large' }}>
<Button {!bypassable ? (
type="submit" <Button
variation={ButtonVariation.PRIMARY} type="submit"
text={getString('commit')} variation={ButtonVariation.PRIMARY}
disabled={loading} text={getString('commit')}
/> disabled={loading || disableCTA}
/>
) : (
<Button
intent={Intent.DANGER}
disabled={loading}
type="submit"
variation={ButtonVariation.SECONDARY}
text={getString('branchProtection.commitNewBranchAlertBtn')}
/>
)}
<Button text={getString('cancel')} variation={ButtonVariation.LINK} onClick={hideModal} /> <Button text={getString('cancel')} variation={ButtonVariation.LINK} onClick={hideModal} />
<FlexExpander /> <FlexExpander />

View File

@ -30,6 +30,12 @@
margin-right: var(--spacing-10) !important; margin-right: var(--spacing-10) !important;
} }
.warningMessage {
display: flex;
gap: 0.5rem;
align-items: center;
}
// .branchSourceDesc { // .branchSourceDesc {
// color: var(--grey-400) !important; // color: var(--grey-400) !important;
// font-size: var(--form-input-font-size) !important; // font-size: var(--form-input-font-size) !important;

View File

@ -24,3 +24,4 @@ export declare const maxContainer: string
export declare const popoverContainer: string export declare const popoverContainer: string
export declare const selectContainer: string export declare const selectContainer: string
export declare const title: string export declare const title: string
export declare const warningMessage: string

View File

@ -29,13 +29,15 @@ import {
useToaster, useToaster,
FormInput, FormInput,
Label, Label,
Text,
ButtonVariation, ButtonVariation,
StringSubstitute StringSubstitute
} from '@harnessio/uicore' } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons' import { Icon } from '@harnessio/icons'
import { FontVariation } from '@harnessio/design-system' import { FontVariation, Color } from '@harnessio/design-system'
import { useMutate } from 'restful-react' import { useMutate } from 'restful-react'
import { get } from 'lodash-es' import { get } from 'lodash-es'
import { Render } from 'react-jsx-match'
import { useModalHook } from 'hooks/useModalHook' import { useModalHook } from 'hooks/useModalHook'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
import { getErrorMessage, permissionProps } from 'utils/Utils' import { getErrorMessage, permissionProps } from 'utils/Utils'
@ -43,6 +45,7 @@ import { GitInfoProps, normalizeGitRef, isGitBranchNameValid } from 'utils/GitUt
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect' import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
import type { RepoBranch } from 'services/code' import type { RepoBranch } from 'services/code'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam' import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { useRuleViolationCheck } from 'hooks/useRuleViolationCheck'
import { useAppContext } from 'AppContext' import { useAppContext } from 'AppContext'
import css from './CreateBranchModal.module.scss' import css from './CreateBranchModal.module.scss'
@ -79,6 +82,7 @@ export function useCreateBranchModal({
const { getString } = useStrings() const { getString } = useStrings()
const [sourceBranch, setSourceBranch] = useState(suggestedSourceBranch || (repoMetadata.default_branch as string)) const [sourceBranch, setSourceBranch] = useState(suggestedSourceBranch || (repoMetadata.default_branch as string))
const { showError, showSuccess } = useToaster() const { showError, showSuccess } = useToaster()
const { violation, bypassable, bypassed, setAllStates } = useRuleViolationCheck()
const { mutate: createBranch, loading } = useMutate<RepoBranch>({ const { mutate: createBranch, loading } = useMutate<RepoBranch>({
verb: 'POST', verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/branches` path: `/api/v1/repos/${repoMetadata.path}/+/branches`
@ -88,7 +92,8 @@ export function useCreateBranchModal({
try { try {
createBranch({ createBranch({
name, name,
target: normalizeGitRef(refIsATag ? `refs/tags/${sourceBranch}` : sourceBranch) target: normalizeGitRef(refIsATag ? `refs/tags/${sourceBranch}` : sourceBranch),
bypass_rules: bypassed
}) })
.then(response => { .then(response => {
hideModal() hideModal()
@ -106,7 +111,13 @@ export function useCreateBranchModal({
} }
}) })
.catch(_error => { .catch(_error => {
showError(getErrorMessage(_error), 0, 'failedToCreateBranch') if (_error.status === 422) {
setAllStates({
violation: true,
bypassed: true,
bypassable: _error?.data?.violations[0]?.bypassable
})
} else showError(getErrorMessage(_error), 0, 'failedToCreateBranch')
}) })
} catch (exception) { } catch (exception) {
showError(getErrorMessage(exception), 0, 'failedToCreateBranch') showError(getErrorMessage(exception), 0, 'failedToCreateBranch')
@ -154,6 +165,9 @@ export function useCreateBranchModal({
dataTooltipId: 'repositoryBranchTextField' dataTooltipId: 'repositoryBranchTextField'
}} }}
inputGroup={{ autoFocus: true }} inputGroup={{ autoFocus: true }}
onChange={() => {
setAllStates({ violation: false, bypassable: false, bypassed: false })
}}
/> />
<Container margin={{ top: 'medium' }}> <Container margin={{ top: 'medium' }}>
<Label className={css.label}>{getString('basedOn')}</Label> <Label className={css.label}>{getString('basedOn')}</Label>
@ -176,17 +190,37 @@ export function useCreateBranchModal({
spacing="small" spacing="small"
padding={{ right: 'xxlarge', top: 'xxlarge', bottom: 'large' }} padding={{ right: 'xxlarge', top: 'xxlarge', bottom: 'large' }}
style={{ alignItems: 'center' }}> style={{ alignItems: 'center' }}>
<Button {!bypassable ? (
type="submit" <Button
text={getString('createBranch')} type="submit"
variation={ButtonVariation.PRIMARY} text={getString('createBranch')}
disabled={loading} variation={ButtonVariation.PRIMARY}
/> disabled={loading}
/>
) : (
<Button
intent={Intent.DANGER}
disabled={loading}
type="submit"
variation={ButtonVariation.SECONDARY}
text={getString('branchProtection.createBranchAlertBtn')}
/>
)}
<Button text={getString('cancel')} variation={ButtonVariation.LINK} onClick={hideModal} /> <Button text={getString('cancel')} variation={ButtonVariation.LINK} onClick={hideModal} />
<FlexExpander /> <FlexExpander />
{loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />} {loading && <Icon intent={Intent.PRIMARY} name="steps-spinner" size={16} />}
</Layout.Horizontal> </Layout.Horizontal>
<Render when={violation}>
<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.createBranchAlertText')
: getString('branchProtection.createBranchBlockText')}
</Text>
</Layout.Horizontal>
</Render>
</FormikForm> </FormikForm>
</Formik> </Formik>
</Container> </Container>

View File

@ -54,9 +54,19 @@ export interface StringsMap {
'branchProtection.blockBranchDeletion': string 'branchProtection.blockBranchDeletion': string
'branchProtection.blockBranchDeletionText': string 'branchProtection.blockBranchDeletionText': string
'branchProtection.bypassList': string 'branchProtection.bypassList': string
'branchProtection.commitDirectlyBlockText': string
'branchProtection.commitNewBranchAlertBtn': string
'branchProtection.commitNewBranchAlertText': string
'branchProtection.commitNewBranchBlockText': string
'branchProtection.create': string 'branchProtection.create': string
'branchProtection.createBranchAlertBtn': string
'branchProtection.createBranchAlertText': string
'branchProtection.createBranchBlockText': string
'branchProtection.createRule': string 'branchProtection.createRule': string
'branchProtection.defaultBranch': string 'branchProtection.defaultBranch': string
'branchProtection.deleteBranchAlertBtn': string
'branchProtection.deleteBranchAlertText': string
'branchProtection.deleteBranchBlockText': string
'branchProtection.deleteProtectionRule': string 'branchProtection.deleteProtectionRule': string
'branchProtection.deleteRule': string 'branchProtection.deleteRule': string
'branchProtection.deleteText': string 'branchProtection.deleteText': string

View File

@ -24,22 +24,25 @@ import { useConfirmationDialog } from './useConfirmationDialog'
export interface UseConfirmActionDialogProps { export interface UseConfirmActionDialogProps {
message: React.ReactElement message: React.ReactElement
childtag?: React.ReactElement
intent?: Intent intent?: Intent
title?: string title?: string
confirmText?: string confirmText?: string
cancelText?: string cancelText?: string
action: (params?: Unknown) => void action: (params?: Unknown) => void
persistDialog?: boolean
} }
/** /**
* @deprecated Use useConfirmAct() hook instead * @deprecated Use useConfirmAct() hook instead
*/ */
export const useConfirmAction = (props: UseConfirmActionDialogProps) => { export const useConfirmAction = (props: UseConfirmActionDialogProps) => {
const { title, message, confirmText, cancelText, intent, action } = props const { title, message, confirmText, cancelText, intent, childtag, action, persistDialog } = props
const { getString } = useStrings() const { getString } = useStrings()
const [params, setParams] = useState<Unknown>() const [params, setParams] = useState<Unknown>()
const { openDialog } = useConfirmationDialog({ const { openDialog } = useConfirmationDialog({
intent, intent,
persistDialog: persistDialog,
titleText: title || getString('confirmation'), titleText: title || getString('confirmation'),
contentText: message, contentText: message,
confirmButtonText: confirmText || getString('confirm'), confirmButtonText: confirmText || getString('confirm'),
@ -49,7 +52,8 @@ export const useConfirmAction = (props: UseConfirmActionDialogProps) => {
if (isConfirmed) { if (isConfirmed) {
action(params) action(params)
} }
} },
children: childtag || <></>
}) })
const confirm = useCallback( const confirm = useCallback(
(_params?: Unknown) => { (_params?: Unknown) => {

View File

@ -33,6 +33,7 @@ export interface UseConfirmationDialogProps {
canEscapeKeyClose?: boolean canEscapeKeyClose?: boolean
children?: JSX.Element children?: JSX.Element
className?: string className?: string
persistDialog?: boolean
} }
export interface UseConfirmationDialogReturn { export interface UseConfirmationDialogReturn {
@ -54,7 +55,8 @@ export const useConfirmationDialog = (props: UseConfirmationDialogProps): UseCon
canOutsideClickClose, canOutsideClickClose,
canEscapeKeyClose, canEscapeKeyClose,
children, children,
className className,
persistDialog
} = props } = props
const [showModal, hideModal] = useModalHook(() => { const [showModal, hideModal] = useModalHook(() => {
@ -81,9 +83,10 @@ export const useConfirmationDialog = (props: UseConfirmationDialogProps): UseCon
const onClose = React.useCallback( const onClose = React.useCallback(
(isConfirmed: boolean): void => { (isConfirmed: boolean): void => {
onCloseDialog?.(isConfirmed) onCloseDialog?.(isConfirmed)
hideModal() if (!isConfirmed) hideModal()
else if (persistDialog) showModal()
}, },
[hideModal, onCloseDialog] [hideModal, onCloseDialog, persistDialog]
) )
return { return {

View File

@ -0,0 +1,68 @@
import { useReducer } from 'react'
enum ActionTypes {
SET_VIOLATION = 'SET_VIOLATION',
SET_BYPASSED = 'SET_BYPASSED',
SET_BYPASSABLE = 'SET_BYPASSABLE',
SET_ALL_STATES = 'SET_ALL_STATES'
}
interface ViolationState {
violation: boolean
bypassable: boolean
bypassed: boolean
}
type ViolationAction =
| { type: ActionTypes.SET_VIOLATION; payload: boolean }
| { type: ActionTypes.SET_BYPASSED; payload: boolean }
| { type: ActionTypes.SET_BYPASSABLE; payload: boolean }
| { type: ActionTypes.SET_ALL_STATES; payload: Partial<ViolationState> }
const initialState: ViolationState = {
violation: false,
bypassable: false,
bypassed: false
}
const reducer = (state: ViolationState, action: ViolationAction): ViolationState => {
switch (action.type) {
case ActionTypes.SET_VIOLATION:
return { ...state, violation: action.payload }
case ActionTypes.SET_BYPASSABLE:
return { ...state, bypassed: action.payload }
case ActionTypes.SET_BYPASSED:
return { ...state, bypassable: action.payload }
case ActionTypes.SET_ALL_STATES:
return {
...state,
violation: action.payload.violation !== undefined ? action.payload.violation : state.violation,
bypassable: action.payload.bypassable !== undefined ? action.payload.bypassable : state.bypassable,
bypassed: action.payload.bypassed !== undefined ? action.payload.bypassed : state.bypassed
}
default:
return state
}
}
export const useRuleViolationCheck = () => {
const [state, dispatch] = useReducer(reducer, initialState)
const setViolation = (value: boolean) => dispatch({ type: ActionTypes.SET_VIOLATION, payload: value })
const setBypassable = (value: boolean) => dispatch({ type: ActionTypes.SET_BYPASSABLE, payload: value })
const setBypassed = (value: boolean) => dispatch({ type: ActionTypes.SET_BYPASSED, payload: value })
const setAllStates = (payload: Partial<ViolationState>) => {
dispatch({ type: ActionTypes.SET_ALL_STATES, payload })
}
const resetViolation = () => setAllStates({ violation: false, bypassable: false, bypassed: false })
return {
violation: state.violation,
setViolation,
bypassable: state.bypassable,
setBypassable,
bypassed: state.bypassed,
setBypassed,
setAllStates,
resetViolation
}
}

View File

@ -913,6 +913,16 @@ branchProtection:
mergePrAlertTitle: Merge pull request alert mergePrAlertTitle: Merge pull request alert
mergePrAlertText: 'Merge cannot be completed. {{ruleCount}} branch rules failed: ' mergePrAlertText: 'Merge cannot be completed. {{ruleCount}} branch rules failed: '
mergeCheckboxAlert: Bypass branch rules and merge mergeCheckboxAlert: Bypass branch rules and merge
createBranchAlertBtn: Bypass rules and create branch
createBranchAlertText: Some rules will be bypassed by creating branch
createBranchBlockText: Some rules don't allow you to create branch
deleteBranchAlertBtn: Bypass rule and confirm delete
deleteBranchAlertText: Some rules will be bypassed while deleting branch
deleteBranchBlockText: Some rules don't allow you to delete branch
commitNewBranchAlertBtn: Bypass rules and commit via new branch
commitNewBranchAlertText: Some rules will be bypassed to commit by creating branch
commitNewBranchBlockText: Some rules don't allow you to create new branch for commit
commitDirectlyBlockText: Some rules don't allow you to commit directly
codeOwner: codeOwner:
title: Code Owner title: Code Owner
changesRequested: '{count} {count|1:change,changes} requested' changesRequested: '{count} {count|1:change,changes} requested'

View File

@ -80,6 +80,14 @@
} }
} }
.warningMessage {
order: 3;
display: flex !important;
gap: 0.5rem;
align-items: center;
margin-top: 1rem !important;
}
.popover { .popover {
:global { :global {
.bp3-popover-content { .bp3-popover-content {

View File

@ -25,3 +25,4 @@ export declare const row: string
export declare const rowText: string export declare const rowText: string
export declare const spacer: string export declare const spacer: string
export declare const table: string export declare const table: string
export declare const warningMessage: string

View File

@ -18,6 +18,7 @@ import React, { useEffect, useMemo, useState } from 'react'
import { import {
Container, Container,
TableV2 as Table, TableV2 as Table,
Layout,
Text, Text,
Avatar, Avatar,
Tag, Tag,
@ -25,8 +26,10 @@ import {
StringSubstitute, StringSubstitute,
useIsMounted useIsMounted
} from '@harnessio/uicore' } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { Color, Intent } from '@harnessio/design-system' import { Color, Intent, FontVariation } from '@harnessio/design-system'
import { Render } from 'react-jsx-match'
import type { CellProps, Column } from 'react-table' import type { CellProps, Column } from 'react-table'
import { Link, useHistory } from 'react-router-dom' import { Link, useHistory } from 'react-router-dom'
import cx from 'classnames' import cx from 'classnames'
@ -43,6 +46,7 @@ import type {
import { CommitActions } from 'components/CommitActions/CommitActions' import { CommitActions } from 'components/CommitActions/CommitActions'
import { formatDate, getErrorMessage } from 'utils/Utils' import { formatDate, getErrorMessage } from 'utils/Utils'
import { useConfirmAction } from 'hooks/useConfirmAction' import { useConfirmAction } from 'hooks/useConfirmAction'
import { useRuleViolationCheck } from 'hooks/useRuleViolationCheck'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton' import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
import { CommitDivergence } from 'components/CommitDivergence/CommitDivergence' import { CommitDivergence } from 'components/CommitDivergence/CommitDivergence'
import { makeDiffRefs } from 'utils/GitUtils' import { makeDiffRefs } from 'utils/GitUtils'
@ -167,19 +171,23 @@ export function BranchesContent({ repoMetadata, searchTerm = '', branches, onDel
id: 'action', id: 'action',
width: '30px', width: '30px',
Cell: ({ row }: CellProps<RepoBranch>) => { Cell: ({ row }: CellProps<RepoBranch>) => {
const { violation, bypassable, bypassed, setAllStates } = useRuleViolationCheck()
const [persistModal, setPersistModal] = useState(true)
const { mutate: deleteBranch } = useMutate({ const { mutate: deleteBranch } = useMutate({
verb: 'DELETE', verb: 'DELETE',
path: `/api/v1/repos/${repoMetadata.path}/+/branches/${row.original.name}` path: `/api/v1/repos/${repoMetadata.path}/+/branches/${row.original.name}?bypass_rules=${bypassed}`
}) })
const { showSuccess, showError } = useToaster() const { showSuccess, showError } = useToaster()
const confirmDeleteBranch = useConfirmAction({ const confirmDeleteBranch = useConfirmAction({
title: getString('deleteBranch'), title: getString('deleteBranch'),
confirmText: getString('delete'), confirmText: !bypassable ? getString('delete') : getString('branchProtection.deleteBranchAlertBtn'),
intent: Intent.DANGER, intent: Intent.DANGER,
message: <String useRichText stringID="deleteBranchConfirm" vars={{ name: row.original.name }} />, message: <String useRichText stringID="deleteBranchConfirm" vars={{ name: row.original.name }} />,
persistDialog: persistModal,
action: async () => { action: async () => {
deleteBranch({}) deleteBranch({})
.then(() => { .then(() => {
setPersistModal(false)
showSuccess( showSuccess(
<StringSubstitute <StringSubstitute
str={getString('branchDeleted')} str={getString('branchDeleted')}
@ -192,9 +200,27 @@ export function BranchesContent({ repoMetadata, searchTerm = '', branches, onDel
onDeleteSuccess() onDeleteSuccess()
}) })
.catch(error => { .catch(error => {
showError(getErrorMessage(error), 0, 'failedToDeleteBranch') if (error.status === 422) {
setAllStates({
violation: true,
bypassed: true,
bypassable: error?.data?.violations[0]?.bypassable
})
} else showError(getErrorMessage(error), 0, 'failedToDeleteBranch')
}) })
} },
childtag: (
<Render when={violation}>
<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.deleteBranchAlertText')
: getString('branchProtection.deleteBranchBlockText')}
</Text>
</Layout.Horizontal>
</Render>
)
}) })
return ( return (

View File

@ -184,6 +184,8 @@ export interface OpenapiCommitFilesRequest {
message?: string message?: string
new_branch?: string new_branch?: string
title?: string title?: string
bypass_rules?: boolean
dry_run_rules?: boolean
} }
export type OpenapiContent = RepoFileContent | OpenapiDirContent | RepoSymlinkContent | RepoSubmoduleContent export type OpenapiContent = RepoFileContent | OpenapiDirContent | RepoSymlinkContent | RepoSubmoduleContent

View File

@ -867,7 +867,7 @@ paths:
application/json: application/json:
schema: schema:
items: items:
$ref: '#/components/schemas/GitrpcBlamePart' $ref: '#/components/schemas/GitBlamePart'
type: array type: array
description: OK description: OK
'401': '401':
@ -1047,6 +1047,13 @@ paths:
delete: delete:
operationId: deleteBranch operationId: deleteBranch
parameters: parameters:
- description: Bypass rule violations if possible.
in: query
name: bypass_rules
required: false
schema:
default: false
type: boolean
- in: path - in: path
name: repo_ref name: repo_ref
required: true required: true
@ -1814,7 +1821,7 @@ paths:
application/json: application/json:
schema: schema:
items: items:
$ref: '#/components/schemas/GitrpcFileDiff' $ref: '#/components/schemas/GitFileDiff'
type: array type: array
text/plain: text/plain:
schema: schema:
@ -4413,6 +4420,8 @@ paths:
$ref: '#/components/schemas/EnumRuleState' $ref: '#/components/schemas/EnumRuleState'
type: type:
$ref: '#/components/schemas/OpenapiRuleType' $ref: '#/components/schemas/OpenapiRuleType'
uid:
type: string
type: object type: object
responses: responses:
'200': '200':
@ -4648,6 +4657,13 @@ paths:
delete: delete:
operationId: deleteTag operationId: deleteTag
parameters: parameters:
- description: Bypass rule violations if possible.
in: query
name: bypass_rules
required: false
schema:
default: false
type: boolean
- in: path - in: path
name: repo_ref name: repo_ref
required: true required: true
@ -6899,8 +6915,8 @@ components:
EnumMergeMethod: EnumMergeMethod:
enum: enum:
- merge - merge
- squash
- rebase - rebase
- squash
type: string type: string
EnumParentResourceType: EnumParentResourceType:
enum: enum:
@ -7000,22 +7016,22 @@ components:
- tag_deleted - tag_deleted
- tag_updated - tag_updated
type: string type: string
GitrpcBlamePart: GitBlamePart:
properties: properties:
commit: commit:
$ref: '#/components/schemas/GitrpcCommit' $ref: '#/components/schemas/GitCommit'
lines: lines:
items: items:
type: string type: string
nullable: true nullable: true
type: array type: array
type: object type: object
GitrpcCommit: GitCommit:
properties: properties:
author: author:
$ref: '#/components/schemas/GitrpcSignature' $ref: '#/components/schemas/GitSignature'
committer: committer:
$ref: '#/components/schemas/GitrpcSignature' $ref: '#/components/schemas/GitSignature'
message: message:
type: string type: string
sha: sha:
@ -7023,14 +7039,14 @@ components:
title: title:
type: string type: string
type: object type: object
GitrpcFileAction: GitFileAction:
enum: enum:
- CREATE - CREATE
- UPDATE - UPDATE
- DELETE - DELETE
- MOVE - MOVE
type: string type: string
GitrpcFileDiff: GitFileDiff:
properties: properties:
additions: additions:
type: integer type: integer
@ -7056,30 +7072,30 @@ components:
sha: sha:
type: string type: string
status: status:
$ref: '#/components/schemas/GitrpcFileDiffStatus' $ref: '#/components/schemas/GitFileDiffStatus'
type: object type: object
GitrpcFileDiffStatus: GitFileDiffStatus:
type: string type: string
GitrpcIdentity: GitIdentity:
properties: properties:
email: email:
type: string type: string
name: name:
type: string type: string
type: object type: object
GitrpcPathDetails: GitPathDetails:
properties: properties:
last_commit: last_commit:
$ref: '#/components/schemas/GitrpcCommit' $ref: '#/components/schemas/GitCommit'
path: path:
type: string type: string
size: size:
type: integer type: integer
type: object type: object
GitrpcSignature: GitSignature:
properties: properties:
identity: identity:
$ref: '#/components/schemas/GitrpcIdentity' $ref: '#/components/schemas/GitIdentity'
when: when:
format: date-time format: date-time
type: string type: string
@ -7191,6 +7207,10 @@ components:
type: array type: array
branch: branch:
type: string type: string
bypass_rules:
type: boolean
dry_run_rules:
type: boolean
message: message:
type: string type: string
new_branch: new_branch:
@ -7227,6 +7247,8 @@ components:
type: string type: string
OpenapiCreateBranchRequest: OpenapiCreateBranchRequest:
properties: properties:
bypass_rules:
type: boolean
name: name:
type: string type: string
target: target:
@ -7318,6 +7340,8 @@ components:
type: object type: object
OpenapiCreateTagRequest: OpenapiCreateTagRequest:
properties: properties:
bypass_rules:
type: boolean
message: message:
type: string type: string
name: name:
@ -7477,8 +7501,6 @@ components:
type: string type: string
decision: decision:
$ref: '#/components/schemas/EnumPullReqReviewDecision' $ref: '#/components/schemas/EnumPullReqReviewDecision'
message:
type: string
type: object type: object
OpenapiReviewerAddPullReqRequest: OpenapiReviewerAddPullReqRequest:
properties: properties:
@ -7518,8 +7540,6 @@ components:
properties: properties:
is_draft: is_draft:
type: boolean type: boolean
message:
type: string
state: state:
$ref: '#/components/schemas/EnumPullReqState' $ref: '#/components/schemas/EnumPullReqState'
type: object type: object
@ -7794,7 +7814,7 @@ components:
RepoCommitFileAction: RepoCommitFileAction:
properties: properties:
action: action:
$ref: '#/components/schemas/GitrpcFileAction' $ref: '#/components/schemas/GitFileAction'
encoding: encoding:
$ref: '#/components/schemas/EnumContentEncodingType' $ref: '#/components/schemas/EnumContentEncodingType'
path: path:
@ -7861,7 +7881,7 @@ components:
properties: properties:
details: details:
items: items:
$ref: '#/components/schemas/GitrpcPathDetails' $ref: '#/components/schemas/GitPathDetails'
nullable: true nullable: true
type: array type: array
type: object type: object
@ -7994,6 +8014,12 @@ components:
properties: properties:
commit_id: commit_id:
type: string type: string
dry_run_rules:
type: boolean
rule_violations:
items:
$ref: '#/components/schemas/TypesRuleViolations'
type: array
type: object type: object
TypesConnector: TypesConnector:
properties: properties:
@ -8159,6 +8185,10 @@ components:
type: object type: object
TypesMergeResponse: TypesMergeResponse:
properties: properties:
allowed_methods:
items:
$ref: '#/components/schemas/EnumMergeMethod'
type: array
branch_deleted: branch_deleted:
type: boolean type: boolean
conflict_files: conflict_files:
@ -8457,6 +8487,8 @@ components:
type: string type: string
TypesRuleViolations: TypesRuleViolations:
properties: properties:
bypassable:
type: boolean
bypassed: bypassed:
type: boolean type: boolean
rule: rule: