mirror of
https://github.com/harness/drone.git
synced 2025-05-13 23:50:47 +08:00
267 lines
9.7 KiB
TypeScript
267 lines
9.7 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
import {
|
|
Container,
|
|
FlexExpander,
|
|
ButtonVariation,
|
|
Layout,
|
|
Text,
|
|
StringSubstitute,
|
|
Button,
|
|
PageError,
|
|
ButtonSize
|
|
} from '@harness/uicore'
|
|
import { Match, Case, Render } from 'react-jsx-match'
|
|
import * as Diff2Html from 'diff2html'
|
|
import cx from 'classnames'
|
|
import { useGet } from 'restful-react'
|
|
import { noop } from 'lodash-es'
|
|
import { useStrings } from 'framework/strings'
|
|
import type { GitInfoProps } from 'utils/GitUtils'
|
|
import { formatNumber, getErrorMessage, voidFn } from 'utils/Utils'
|
|
import { DiffViewer } from 'components/DiffViewer/DiffViewer'
|
|
import { useEventListener } from 'hooks/useEventListener'
|
|
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
|
|
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
|
import type { DiffFileEntry } from 'utils/types'
|
|
import {
|
|
DIFF2HTML_CONFIG,
|
|
PR_CODE_COMMENT_PAYLOAD_VERSION,
|
|
PullRequestCodeCommentPayload,
|
|
ViewStyle
|
|
} from 'components/DiffViewer/DiffViewerUtils'
|
|
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
|
import type { TypesPullReq, TypesPullReqActivity } from 'services/code'
|
|
import { useShowRequestError } from 'hooks/useShowRequestError'
|
|
// import { Render } from 'components/Render/Render'
|
|
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
|
import { ChangesDropdown } from './ChangesDropdown'
|
|
import { DiffViewConfiguration } from './DiffViewConfiguration'
|
|
import ReviewSplitButton from './ReviewSplitButton/ReviewSplitButton'
|
|
import css from './Changes.module.scss'
|
|
|
|
const STICKY_TOP_POSITION = 64
|
|
const STICKY_HEADER_HEIGHT = 150
|
|
const changedFileId = (collection: Unknown[]) => collection.filter(Boolean).join('::::')
|
|
|
|
interface ChangesProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
|
targetBranch?: string
|
|
sourceBranch?: string
|
|
readOnly?: boolean
|
|
emptyTitle: string
|
|
emptyMessage: string
|
|
pullRequestMetadata?: TypesPullReq
|
|
className?: string
|
|
onCommentUpdate: () => void
|
|
prHasChanged?: boolean
|
|
handleRefresh?: () => void
|
|
}
|
|
|
|
export const Changes: React.FC<ChangesProps> = ({
|
|
repoMetadata,
|
|
targetBranch,
|
|
sourceBranch,
|
|
readOnly,
|
|
emptyTitle,
|
|
emptyMessage,
|
|
pullRequestMetadata,
|
|
onCommentUpdate,
|
|
className,
|
|
prHasChanged,
|
|
handleRefresh
|
|
}) => {
|
|
const { getString } = useStrings()
|
|
const [viewStyle, setViewStyle] = useUserPreference(UserPreference.DIFF_VIEW_STYLE, ViewStyle.SIDE_BY_SIDE)
|
|
const [lineBreaks, setLineBreaks] = useUserPreference(UserPreference.DIFF_LINE_BREAKS, false)
|
|
const [diffs, setDiffs] = useState<DiffFileEntry[]>([])
|
|
const [isSticky, setSticky] = useState(false)
|
|
|
|
const {
|
|
data: rawDiff,
|
|
error,
|
|
loading,
|
|
refetch
|
|
} = useGet<string>({
|
|
path: `/api/v1/repos/${repoMetadata?.path}/+/${
|
|
pullRequestMetadata ? `pullreq/${pullRequestMetadata.number}/diff` : `compare/${targetBranch}...${sourceBranch}`
|
|
}`,
|
|
lazy: !targetBranch || !sourceBranch
|
|
})
|
|
const {
|
|
data: activities,
|
|
loading: loadingActivities,
|
|
error: errorActivities
|
|
// refetch: refetchActivities
|
|
} = useGet<TypesPullReqActivity[]>({
|
|
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/activities`,
|
|
lazy: !pullRequestMetadata?.number
|
|
})
|
|
|
|
const diffStats = useMemo(
|
|
() =>
|
|
(diffs || []).reduce(
|
|
(obj, diff) => {
|
|
obj.addedLines += diff.addedLines
|
|
obj.deletedLines += diff.deletedLines
|
|
return obj
|
|
},
|
|
{ addedLines: 0, deletedLines: 0 }
|
|
),
|
|
[diffs]
|
|
)
|
|
|
|
useEffect(() => {
|
|
const _raw = rawDiff && typeof rawDiff === 'string' ? rawDiff : ''
|
|
|
|
if (rawDiff) {
|
|
setDiffs(
|
|
Diff2Html.parse(_raw, DIFF2HTML_CONFIG).map(diff => {
|
|
const fileId = changedFileId([diff.oldName, diff.newName])
|
|
const containerId = `container-${fileId}`
|
|
const contentId = `content-${fileId}`
|
|
const fileTitle = diff.isDeleted
|
|
? diff.oldName
|
|
: diff.isRename
|
|
? `${diff.oldName} -> ${diff.newName}`
|
|
: diff.newName
|
|
const fileActivities: TypesPullReqActivity[] | undefined = activities?.filter(activity => {
|
|
const payload = activity.payload as PullRequestCodeCommentPayload
|
|
return payload?.file_id === fileId && payload?.version === PR_CODE_COMMENT_PAYLOAD_VERSION
|
|
})
|
|
|
|
return {
|
|
...diff,
|
|
containerId,
|
|
contentId,
|
|
fileId,
|
|
fileTitle,
|
|
fileActivities: fileActivities || [],
|
|
activities: activities || []
|
|
}
|
|
})
|
|
)
|
|
}
|
|
}, [rawDiff, activities])
|
|
|
|
useEventListener(
|
|
'scroll',
|
|
useCallback(() => setSticky(window.scrollY >= STICKY_HEADER_HEIGHT), [])
|
|
)
|
|
|
|
useShowRequestError(errorActivities)
|
|
|
|
return (
|
|
<Container className={cx(css.container, className)} {...(!!loading || !!error ? { flex: true } : {})}>
|
|
<LoadingSpinner visible={loading || loadingActivities} withBorder={true} />
|
|
<Render when={error}>
|
|
<PageError message={getErrorMessage(error || errorActivities)} onClick={voidFn(refetch)} />
|
|
</Render>
|
|
<Render when={!loading && !error}>
|
|
<Match expr={diffs?.length}>
|
|
<Case val={(len: number) => len > 0}>
|
|
<>
|
|
<Container className={cx(css.header, { [css.stickied]: isSticky })}>
|
|
<Layout.Horizontal>
|
|
<Container flex={{ alignItems: 'center' }}>
|
|
{/* Files Changed stats */}
|
|
<Text flex className={css.diffStatsLabel}>
|
|
<StringSubstitute
|
|
str={getString('pr.diffStatsLabel')}
|
|
vars={{
|
|
changedFilesLink: <ChangesDropdown diffs={diffs} />,
|
|
addedLines: formatNumber(diffStats.addedLines),
|
|
deletedLines: formatNumber(diffStats.deletedLines),
|
|
configuration: (
|
|
<DiffViewConfiguration
|
|
viewStyle={viewStyle}
|
|
setViewStyle={setViewStyle}
|
|
lineBreaks={lineBreaks}
|
|
setLineBreaks={setLineBreaks}
|
|
/>
|
|
)
|
|
}}
|
|
/>
|
|
</Text>
|
|
{!prHasChanged ? null : (
|
|
<Button
|
|
onClick={handleRefresh}
|
|
iconProps={{ className: css.refreshIcon, size: 12 }}
|
|
icon="repeat"
|
|
text={getString('refresh')}
|
|
variation={ButtonVariation.SECONDARY}
|
|
size={ButtonSize.SMALL}
|
|
padding={{ left: 'small' }}
|
|
className={css.repeatBtn}
|
|
/>
|
|
)}
|
|
|
|
{/* Show "Scroll to top" button */}
|
|
<Render when={isSticky}>
|
|
<Layout.Horizontal padding={{ left: 'small' }}>
|
|
<PipeSeparator height={10} />
|
|
<Button
|
|
variation={ButtonVariation.ICON}
|
|
icon="arrow-up"
|
|
iconProps={{ size: 14 }}
|
|
onClick={() => window.scroll({ top: 0 })}
|
|
tooltip={getString('scrollToTop')}
|
|
tooltipProps={{ isDark: true }}
|
|
/>
|
|
</Layout.Horizontal>
|
|
</Render>
|
|
</Container>
|
|
<FlexExpander />
|
|
|
|
<ReviewSplitButton
|
|
shouldHide={readOnly || pullRequestMetadata?.state === 'merged'}
|
|
repoMetadata={repoMetadata}
|
|
pullRequestMetadata={pullRequestMetadata}
|
|
refreshPr={voidFn(noop)}
|
|
/>
|
|
{/* <ReviewDecisionButton
|
|
repoMetadata={repoMetadata}
|
|
pullRequestMetadata={pullRequestMetadata}
|
|
shouldHide={readOnly || pullRequestMetadata?.state === 'merged'}
|
|
/> */}
|
|
</Layout.Horizontal>
|
|
</Container>
|
|
|
|
{/* TODO: lineBreaks is broken in line-by-line view, enable it for side-by-side only now */}
|
|
<Layout.Vertical
|
|
spacing="large"
|
|
className={cx(css.main, {
|
|
[css.enableDiffLineBreaks]: lineBreaks && viewStyle === ViewStyle.SIDE_BY_SIDE
|
|
})}>
|
|
{diffs?.map((diff, index) => (
|
|
// Note: `key={viewStyle + index + lineBreaks}` resets DiffView when view configuration
|
|
// is changed. Making it easier to control states inside DiffView itself, as it does not
|
|
// have to deal with any view configuration
|
|
<DiffViewer
|
|
readOnly={readOnly}
|
|
key={viewStyle + index + lineBreaks}
|
|
diff={diff}
|
|
viewStyle={viewStyle}
|
|
stickyTopPosition={STICKY_TOP_POSITION}
|
|
repoMetadata={repoMetadata}
|
|
pullRequestMetadata={pullRequestMetadata}
|
|
onCommentUpdate={onCommentUpdate}
|
|
/>
|
|
))}
|
|
</Layout.Vertical>
|
|
</>
|
|
</Case>
|
|
<Case val={0}>
|
|
<Container padding="xlarge">
|
|
<NoResultCard
|
|
showWhen={() => diffs?.length === 0}
|
|
forSearch={true}
|
|
title={emptyTitle}
|
|
emptySearchMessage={emptyMessage}
|
|
/>
|
|
</Container>
|
|
</Case>
|
|
</Match>
|
|
</Render>
|
|
</Container>
|
|
)
|
|
}
|