/* * 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 { useCallback, useEffect, useMemo, useState } from 'react' import { useGet } from 'restful-react' import { isEqual } from 'lodash-es' import { useAtom, atom } from 'jotai' import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' import type { TypesListCommitResponse, TypesPullReq, TypesPullReqActivity, TypesPullReqStats } from 'services/code' import { usePRChecksDecision } from 'hooks/usePRChecksDecision' import useSpaceSSE, { SSEEvents } from 'hooks/useSpaceSSE' import { useGetSpaceParam } from 'hooks/useGetSpaceParam' import { PullRequestSection } from 'utils/Utils' import { normalizeGitRef } from 'utils/GitUtils' /** * This hook abstracts data handling for a pull request. It's used as a * centralized data store for all tabs in Pull Request page. The hook * fetches necessary repository metadata, poll/refetch request metadata * for updates, cache data, etc... * * We use Atom to reduce React rendering cycles. Data could be re-fetched, * but their reference only updated only if the incoming one is different * from cache. This optimization reduces unnecessary React state updates, * hence improves rendering pipeline. * * The abstraction allows Pull Request tabs to do less data handling and * focus more on their specific rendering logics. */ export function useGetPullRequestInfo() { const space = useGetSpaceParam() const { repoMetadata, error: repoError, loading: repoLoading, refetch: refetchRepo, pullRequestId, pullRequestSection = PullRequestSection.CONVERSATION, commitSHA } = useGetRepositoryMetadata() const { data: pullReqData, error: pullReqError, loading: pullReqLoading, refetch: refetchPullReq } = useGet({ path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}`, lazy: !repoMetadata }) const [showEditDescription, setShowEditDescription] = useState(false) // // Listen to PULLREQ_UPDATED event and refetch PR data accordingly // useSpaceSSE({ space, events: useMemo(() => [SSEEvents.PULLREQ_UPDATED], []), onEvent: useCallback( (data: TypesPullReq) => { // Ensure this update belongs to the current PR if (data && String(data?.number) === pullRequestId) { // NOTE: We can't replace `pullReqMetadata` by `data` as events don't contain all // pr stats yet (can be optimized). refetchPullReq() } }, [pullRequestId, refetchPullReq] ) }) const [pullReqMetadata, setPullReqMetadata] = useAtom(pullReqAtom) const [pullReqStats, setPullReqStats] = useAtom(pullReqStatsAtom) // TODO: Polling from usePRChecksDecision() starts React re-rendering check // Need a better way to handle (SSE, or atom in a smaller component that // writes latest decisions in a way that does not trigger re-rendering on // page level) const pullReqChecksDecision = usePRChecksDecision({ repoMetadata, pullReqMetadata }) const { data: activities, loading: activitiesLoading, error: activitiesError, refetch: refetchActivities } = useGet({ path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}/activities`, lazy: true }) const [pullReqActivities, setPullReqActivities] = useAtom(pullReqActivitiesAtom) const [pullReqCommits, setPullReqCommits] = useAtom(pullReqCommitsAtom) const loading = useMemo( () => repoLoading || (pullReqLoading && !pullReqMetadata) || (activitiesLoading && !pullReqActivities), [repoLoading, pullReqLoading, pullReqMetadata, activitiesLoading, pullReqActivities] ) useEffect(() => { if (activities) { setPullReqActivities(oldActivities => (isEqual(oldActivities, activities) ? oldActivities : activities)) } }, [activities, setPullReqActivities]) // Reset pullReqAtom to undefined when page is unmounted to make sure no // wrong caching occurs when navigating among PRs. This is important to make sure when // switching among PRs, cached data from atoms from one PR is not used for another useEffect( function cleanupAtoms() { return () => { setPullReqMetadata(undefined) setPullReqActivities(undefined) setPullReqCommits(undefined) setPullReqStats(undefined) } }, [setPullReqMetadata, setPullReqActivities, setPullReqCommits, setPullReqStats] ) const { data: commits, error: commitsError, refetch: refetchCommits } = useGet({ path: `/api/v1/repos/${repoMetadata?.path}/+/commits`, queryParams: { limit: COMMITS_LIMIT, git_ref: normalizeGitRef(pullReqData?.source_sha), after: normalizeGitRef(pullReqData?.merge_base_sha) }, lazy: true }) // (1) pullReqMetadata holds the latest good PR data to make sure page is not broken // when polling fails. // (2) Only update pullReqMetadata when polled data is different from current one useEffect( function updatePullReqMetadata() { if (pullReqData && !isEqual(pullReqMetadata, pullReqData)) { if ( !pullReqMetadata || (pullReqMetadata && (pullReqMetadata.merge_base_sha !== pullReqData.merge_base_sha || pullReqMetadata.source_sha !== pullReqData.source_sha)) ) { refetchCommits() } setPullReqMetadata(pullReqData) if (!isEqual(pullReqStats, pullReqData.stats)) { setPullReqStats(pullReqData.stats) refetchActivities() } } }, [pullReqData, pullReqMetadata, setPullReqMetadata, setPullReqStats, pullReqStats, refetchActivities, refetchCommits] ) useEffect( function updatePullReqCommits() { if (commits && !isEqual(commits, pullReqCommits)) { setPullReqCommits(commits) } }, [commits, pullReqCommits, setPullReqCommits] ) const retryOnErrorFunc = useMemo(() => { return () => repoError ? refetchRepo() : pullReqError ? refetchPullReq() : commitsError ? refetchCommits() : refetchActivities() }, [repoError, refetchRepo, pullReqError, refetchPullReq, refetchActivities, commitsError, refetchCommits]) return { repoMetadata, refetchRepo, loading, error: repoError || pullReqError || activitiesError || commitsError, pullReqChecksDecision, showEditDescription, setShowEditDescription, pullReqMetadata, pullReqStats, pullReqCommits, pullRequestId, pullRequestSection, commitSHA, refetchActivities, refetchCommits, refetchPullReq, retryOnErrorFunc } } export type UseGetPullRequestInfoResult = ReturnType export function usePullReqActivities() { const [activities] = useAtom(pullReqActivitiesAtom) return activities } export const pullReqAtom = atom(undefined) const pullReqStatsAtom = atom(undefined) export const pullReqActivitiesAtom = atom(undefined) const pullReqCommitsAtom = atom(undefined) // Note: We just list COMMITS_LIMIT commits in PR page // possibly something we can improve if required const COMMITS_LIMIT = 500