mirror of
https://github.com/harness/drone.git
synced 2025-05-12 23:20:10 +08:00
290 lines
11 KiB
TypeScript
290 lines
11 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
|
import {
|
|
Button,
|
|
ButtonVariation,
|
|
Color,
|
|
Container,
|
|
FlexExpander,
|
|
Icon,
|
|
Layout,
|
|
SplitButton,
|
|
StringSubstitute,
|
|
Text,
|
|
useToaster
|
|
} from '@harness/uicore'
|
|
import { useMutate } from 'restful-react'
|
|
import { Case, Else, Match, Render, Truthy } from 'react-jsx-match'
|
|
import { Menu, PopoverPosition, Icon as BIcon } from '@blueprintjs/core'
|
|
import cx from 'classnames'
|
|
import ReactTimeago from 'react-timeago'
|
|
import type {
|
|
EnumMergeMethod,
|
|
OpenapiMergePullReq,
|
|
OpenapiStatePullReqRequest,
|
|
RepoMergeCheck,
|
|
TypesPullReq
|
|
} from 'services/code'
|
|
import { useStrings } from 'framework/strings'
|
|
import { CodeIcon, GitInfoProps, PullRequestFilterOption, PullRequestState } from 'utils/GitUtils'
|
|
import { useShowRequestError } from 'hooks/useShowRequestError'
|
|
import { getErrorMessage } from 'utils/Utils'
|
|
import css from './PullRequestActionsBox.module.scss'
|
|
|
|
interface PullRequestActionsBoxProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
|
|
onPRStateChanged: () => void
|
|
}
|
|
|
|
interface PRMergeOption {
|
|
method: EnumMergeMethod | 'close'
|
|
title: string
|
|
desc: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
|
repoMetadata,
|
|
pullRequestMetadata,
|
|
onPRStateChanged
|
|
}) => {
|
|
const { getString } = useStrings()
|
|
const { showError } = useToaster()
|
|
const { mutate: mergePR, loading } = useMutate({
|
|
verb: 'POST',
|
|
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/merge`
|
|
})
|
|
const { mutate: updatePRState, loading: loadingState } = useMutate({
|
|
verb: 'POST',
|
|
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/state`
|
|
})
|
|
const {
|
|
mutate: mergeCheck,
|
|
error: errorMergeCheck,
|
|
loading: loadingMergeCheck
|
|
} = useMutate<RepoMergeCheck>({
|
|
verb: 'POST',
|
|
path: `/api/v1/repos/${repoMetadata.path}/+/merge-check/${pullRequestMetadata.target_branch}..${pullRequestMetadata.source_branch}`
|
|
})
|
|
const [mergeable, setMergable] = useState<boolean | undefined>()
|
|
const isDraft = pullRequestMetadata.is_draft
|
|
const mergeOptions: PRMergeOption[] = [
|
|
{
|
|
method: 'squash',
|
|
title: getString('pr.mergeOptions.squashAndMerge'),
|
|
desc: getString('pr.mergeOptions.squashAndMergeDesc'),
|
|
disabled: true
|
|
},
|
|
{
|
|
method: 'merge',
|
|
title: getString('pr.mergeOptions.createMergeCommit'),
|
|
desc: getString('pr.mergeOptions.createMergeCommitDesc')
|
|
},
|
|
{
|
|
method: 'rebase',
|
|
title: getString('pr.mergeOptions.rebaseAndMerge'),
|
|
desc: getString('pr.mergeOptions.rebaseAndMergeDesc'),
|
|
disabled: true
|
|
},
|
|
{
|
|
method: 'close',
|
|
title: getString('pr.mergeOptions.close'),
|
|
desc: getString('pr.mergeOptions.closeDesc')
|
|
}
|
|
]
|
|
const [mergeOption, setMergeOption] = useState<PRMergeOption>(mergeOptions[1])
|
|
|
|
useShowRequestError(errorMergeCheck)
|
|
|
|
useEffect(() => {
|
|
if (!isDraft && pullRequestMetadata.state === PullRequestState.OPEN) {
|
|
mergeCheck({}).then(response => {
|
|
setMergable(response.mergeable)
|
|
})
|
|
}
|
|
}, [isDraft, mergeCheck, pullRequestMetadata])
|
|
|
|
if (pullRequestMetadata.state === PullRequestFilterOption.MERGED) {
|
|
return <MergeInfo pullRequestMetadata={pullRequestMetadata} />
|
|
}
|
|
|
|
return (
|
|
<Container className={cx(css.main, { [css.error]: mergeable === false })}>
|
|
<Layout.Vertical spacing="xlarge">
|
|
<Container>
|
|
<Layout.Horizontal spacing="small" flex={{ alignItems: 'center' }} className={css.layout}>
|
|
{!loadingMergeCheck && (
|
|
<>
|
|
<Icon
|
|
name={isDraft ? CodeIcon.Draft : mergeable === false ? 'warning-sign' : 'tick-circle'}
|
|
size={20}
|
|
color={isDraft ? Color.ORANGE_900 : mergeable === false ? Color.RED_500 : Color.GREEN_700}
|
|
/>
|
|
<Text className={css.sub}>
|
|
{getString(
|
|
isDraft
|
|
? 'prState.draftHeading'
|
|
: mergeable === false
|
|
? 'pr.cantBeMerged'
|
|
: 'pr.branchHasNoConflicts'
|
|
)}
|
|
</Text>
|
|
</>
|
|
)}
|
|
<FlexExpander />
|
|
<Render when={loading || loadingState || loadingMergeCheck}>
|
|
<Icon name={CodeIcon.InputSpinner} size={16} margin={{ right: 'xsmall' }} />
|
|
</Render>
|
|
<Match expr={isDraft}>
|
|
<Truthy>
|
|
<Button
|
|
className={css.secondaryButton}
|
|
text={getString('pr.readyForReview')}
|
|
variation={ButtonVariation.TERTIARY}
|
|
onClick={() => {
|
|
const payload: OpenapiStatePullReqRequest = { is_draft: false, state: 'open' }
|
|
|
|
updatePRState(payload)
|
|
.then(onPRStateChanged)
|
|
.catch(exception => showError(getErrorMessage(exception)))
|
|
}}
|
|
/>
|
|
</Truthy>
|
|
<Else>
|
|
<Container>
|
|
<Match expr={pullRequestMetadata.state}>
|
|
<Case val={PullRequestState.CLOSED}>
|
|
<Button
|
|
className={css.secondaryButton}
|
|
text={getString('pr.openForReview')}
|
|
variation={ButtonVariation.TERTIARY}
|
|
onClick={() => {
|
|
const payload: OpenapiStatePullReqRequest = { state: 'open' }
|
|
|
|
updatePRState(payload)
|
|
.then(onPRStateChanged)
|
|
.catch(exception => showError(getErrorMessage(exception)))
|
|
}}
|
|
/>
|
|
</Case>
|
|
<Case val={PullRequestState.OPEN}>
|
|
<Layout.Horizontal
|
|
inline
|
|
spacing="huge"
|
|
className={cx({
|
|
[css.btnWrapper]: mergeOption.method !== 'close',
|
|
[css.hasError]: mergeable === false
|
|
})}>
|
|
<SplitButton
|
|
text={mergeOption.title}
|
|
disabled={loading || loadingMergeCheck}
|
|
// dropdownDisabled={mergeButtonDisabled}
|
|
className={cx({ [css.secondaryButton]: mergeOption.method === 'close' })}
|
|
variation={
|
|
mergeOption.method === 'close' || mergeable === false
|
|
? ButtonVariation.TERTIARY
|
|
: ButtonVariation.PRIMARY
|
|
}
|
|
popoverProps={{
|
|
interactionKind: 'click',
|
|
usePortal: true,
|
|
popoverClassName: css.popover,
|
|
position: PopoverPosition.BOTTOM_RIGHT,
|
|
transitionDuration: 1000
|
|
}}
|
|
onClick={() => {
|
|
if (mergeOption.method !== 'close') {
|
|
const payload: OpenapiMergePullReq = { method: mergeOption.method }
|
|
|
|
mergePR(payload)
|
|
.then(onPRStateChanged)
|
|
.catch(exception => showError(getErrorMessage(exception)))
|
|
} else {
|
|
const payload: OpenapiStatePullReqRequest = { state: 'closed' }
|
|
|
|
updatePRState(payload)
|
|
.then(onPRStateChanged)
|
|
.catch(exception => showError(getErrorMessage(exception)))
|
|
}
|
|
}}>
|
|
{/* TODO: These two items are used for creating a PR
|
|
<Menu.Item
|
|
className={css.menuItem}
|
|
text={
|
|
<>
|
|
<BIcon icon="blank" />
|
|
<strong>Create pull request</strong>
|
|
<p>Open a pull request that is ready for review</p>
|
|
<p>Automatically request reviews from code owners</p>
|
|
</>
|
|
}
|
|
/>
|
|
<Menu.Item
|
|
className={css.menuItem}
|
|
text={
|
|
<>
|
|
<BIcon icon="blank" />
|
|
<strong>Create draft pull request</strong>
|
|
<p>Does not request code reviews and cannot be merged</p>
|
|
<p>Cannot be merged until marked ready for review</p>
|
|
</>
|
|
}
|
|
/> */}
|
|
{mergeOptions.map(option => {
|
|
return (
|
|
<Menu.Item
|
|
key={option.method}
|
|
className={css.menuItem}
|
|
disabled={option.disabled}
|
|
text={
|
|
<>
|
|
<BIcon icon={mergeOption.method === option.method ? 'tick' : 'blank'} />
|
|
<strong>{option.title}</strong>
|
|
<p>{option.desc}</p>
|
|
</>
|
|
}
|
|
onClick={() => setMergeOption(option)}
|
|
/>
|
|
)
|
|
})}
|
|
</SplitButton>
|
|
</Layout.Horizontal>
|
|
</Case>
|
|
</Match>
|
|
</Container>
|
|
</Else>
|
|
</Match>
|
|
</Layout.Horizontal>
|
|
</Container>
|
|
</Layout.Vertical>
|
|
</Container>
|
|
)
|
|
}
|
|
|
|
const MergeInfo: React.FC<{ pullRequestMetadata: TypesPullReq }> = ({ pullRequestMetadata }) => {
|
|
const { getString } = useStrings()
|
|
|
|
return (
|
|
<Container className={cx(css.main, css.merged)}>
|
|
<Layout.Horizontal spacing="medium" flex={{ alignItems: 'center' }} className={css.layout}>
|
|
<Container width={24} height={24} className={css.mergeContainer}>
|
|
<Icon name={CodeIcon.Merged} size={20} color={Color.PURPLE_700} />
|
|
</Container>
|
|
<Container>
|
|
{/* <Text className={css.heading}>{getString('pr.prMerged')}</Text> */}
|
|
<Text className={css.sub}>
|
|
<StringSubstitute
|
|
str={getString('pr.prMergedInfo')}
|
|
vars={{
|
|
user: <strong>{pullRequestMetadata.merger?.display_name}</strong>,
|
|
source: <strong>{pullRequestMetadata.source_branch}</strong>,
|
|
target: <strong>{pullRequestMetadata.target_branch} </strong>,
|
|
time: <ReactTimeago date={pullRequestMetadata.merged as number} />
|
|
}}
|
|
/>
|
|
</Text>
|
|
</Container>
|
|
<FlexExpander />
|
|
</Layout.Horizontal>
|
|
</Container>
|
|
)
|
|
}
|