/* * 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, useMemo, useState } from 'react' import { Button, ButtonVariation, Checkbox, Container, FlexExpander, Layout, SplitButton, StringSubstitute, Text, useToaster } from '@harnessio/uicore' import { Icon } from '@harnessio/icons' import { Color } from '@harnessio/design-system' 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 { EnumPullReqState, OpenapiMergePullReq, OpenapiStatePullReqRequest, TypesPullReq, TypesRuleViolations } from 'services/code' import { useStrings } from 'framework/strings' import { CodeIcon, PullRequestFilterOption, PullRequestState } from 'utils/GitUtils' import { useGetSpaceParam } from 'hooks/useGetSpaceParam' import { useAppContext } from 'AppContext' import { Images } from 'images' import { extractInfoFromRuleViolationArr, getErrorMessage, MergeCheckStatus, permissionProps, PRDraftOption, PRMergeOption, PullRequestActionsBoxProps, Violation } from 'utils/Utils' import { UserPreference, useUserPreference } from 'hooks/useUserPreference' import ReviewSplitButton from 'components/Changes/ReviewSplitButton/ReviewSplitButton' import RuleViolationAlertModal from 'components/RuleViolationAlertModal/RuleViolationAlertModal' import css from './PullRequestActionsBox.module.scss' const POLLING_INTERVAL = 60000 export const PullRequestActionsBox: React.FC = ({ repoMetadata, pullRequestMetadata, onPRStateChanged, refetchReviewers }) => { const [isActionBoxOpen, setActionBoxOpen] = useState(false) const { getString } = useStrings() const { showError } = useToaster() const { currentUser } = useAppContext() const { hooks, standalone } = useAppContext() const space = useGetSpaceParam() const { mutate: mergePR, loading } = useMutate({ verb: 'POST', path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/merge` }) const [ruleViolation, setRuleViolation] = useState(false) const [ruleViolationArr, setRuleViolationArr] = useState<{ data: { rule_violations: TypesRuleViolations[] } }>() const [length, setLength] = useState(0) const [notBypassable, setNotBypassable] = useState(false) const [finalRulesArr, setFinalRulesArr] = useState() const [bypass, setBypass] = useState(false) const { mutate: updatePRState, loading: loadingState } = useMutate({ verb: 'POST', path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/state` }) const mergeable = useMemo( () => pullRequestMetadata.merge_check_status === MergeCheckStatus.MERGEABLE, [pullRequestMetadata] ) const isClosed = pullRequestMetadata.state === PullRequestState.CLOSED const isOpen = pullRequestMetadata.state === PullRequestState.OPEN const isConflict = pullRequestMetadata.merge_check_status === MergeCheckStatus.CONFLICT const unchecked = useMemo( () => pullRequestMetadata.merge_check_status === MergeCheckStatus.UNCHECKED && !isClosed, [pullRequestMetadata, isClosed] ) useEffect(() => { if (ruleViolationArr && !isDraft && ruleViolationArr.data.rule_violations) { const { checkIfBypassAllowed, violationArr, uniqueViolations } = extractInfoFromRuleViolationArr( ruleViolationArr.data.rule_violations ) setNotBypassable(checkIfBypassAllowed) setFinalRulesArr(violationArr) setLength(uniqueViolations.size) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ruleViolationArr]) const dryMerge = () => { if (!isClosed && pullRequestMetadata.state !== PullRequestState.MERGED) { mergePR({ bypass_rules: true, dry_run: true, source_sha: pullRequestMetadata?.source_sha }) .then(res => { if (res?.rule_violations?.length > 0) { setRuleViolation(true) setRuleViolationArr({ data: { rule_violations: res?.rule_violations } }) setAllowedStrats(res.allowed_methods) } else { setRuleViolation(false) setAllowedStrats(res.allowed_methods) } }) .catch(err => { if (err.status === 422) { setRuleViolation(true) setRuleViolationArr(err) setAllowedStrats(err.allowed_methods) } else { showError(getErrorMessage(err)) } }) } } useEffect(() => { dryMerge() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { // dryMerge() const intervalId = setInterval(async () => { dryMerge() }, POLLING_INTERVAL) // Poll every 20 seconds // Cleanup interval on component unmount return () => { clearInterval(intervalId) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [onPRStateChanged]) const isDraft = pullRequestMetadata.is_draft const mergeOptions: PRMergeOption[] = [ { method: 'squash', title: getString('pr.mergeOptions.squashAndMerge'), desc: getString('pr.mergeOptions.squashAndMergeDesc'), disabled: mergeable === false }, { method: 'merge', title: getString('pr.mergeOptions.createMergeCommit'), desc: getString('pr.mergeOptions.createMergeCommitDesc'), disabled: mergeable === false }, { method: 'rebase', title: getString('pr.mergeOptions.rebaseAndMerge'), desc: getString('pr.mergeOptions.rebaseAndMergeDesc'), disabled: mergeable === false }, { method: 'close', title: getString('pr.mergeOptions.close'), desc: getString('pr.mergeOptions.closeDesc') } ] const [allowedStrats, setAllowedStrats] = useState([ mergeOptions[0].method, mergeOptions[1].method, mergeOptions[2].method, mergeOptions[3].method ]) const draftOptions: PRDraftOption[] = [ { method: 'open', title: getString('pr.draftOpenForReview.title'), desc: getString('pr.draftOpenForReview.desc') }, { method: 'close', title: getString('pr.mergeOptions.close'), desc: getString('pr.mergeOptions.closeDesc') } ] const [mergeOption, setMergeOption, resetMergeOption] = useUserPreference( UserPreference.PULL_REQUEST_MERGE_STRATEGY, mergeOptions[0], option => option.method !== 'close' ) useEffect(() => { if (allowedStrats) { const matchingMethods = mergeOptions.filter(option => allowedStrats.includes(option.method)) if (matchingMethods.length > 0) { setMergeOption(matchingMethods[0]) } } else { setMergeOption(mergeOptions[3]) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [allowedStrats]) const [draftOption, setDraftOption] = useState(draftOptions[0]) const permPushResult = hooks?.usePermissionTranslate?.( { resource: { resourceType: 'CODE_REPOSITORY' }, permissions: ['code_repo_push'] }, [space] ) const isActiveUserPROwner = useMemo(() => { return ( !!currentUser?.uid && !!pullRequestMetadata?.author?.uid && currentUser?.uid === pullRequestMetadata?.author?.uid ) }, [currentUser, pullRequestMetadata]) if (pullRequestMetadata.state === PullRequestFilterOption.MERGED) { return } return ( {(unchecked && ) || ( )} {getString( isDraft ? 'prState.draftHeading' : isClosed ? 'pr.prClosed' : unchecked ? 'pr.checkingToMerge' : mergeable === false && isOpen ? 'pr.cantBeMerged' : ruleViolation ? 'branchProtection.prFailedText' : 'pr.branchHasNoConflicts', ruleViolation ? { ruleCount: length } : { ruleCount: 0 } )} {ruleViolation && mergeable && !isDraft ? (