feat: [code-120]: internal page refresh button (#316)

This commit is contained in:
Calvin Lee 2023-02-09 13:00:44 -07:00 committed by GitHub
parent f2358bd4ec
commit e179696678
15 changed files with 201 additions and 15 deletions

View File

@ -40,3 +40,16 @@
.container { .container {
background-color: var(--primary-bg) !important; background-color: var(--primary-bg) !important;
} }
.hideBtn {
display: none;
}
.refreshIcon {
padding-left: var(--spacing-xsmall) !important;
padding-right: var(--spacing-xsmall);
}
.repeatBtn {
margin-left: var(--spacing-xsmall) !important;
}

View File

@ -7,5 +7,8 @@ declare const styles: {
readonly main: string readonly main: string
readonly enableDiffLineBreaks: string readonly enableDiffLineBreaks: string
readonly container: string readonly container: string
readonly hideBtn: string
readonly refreshIcon: string
readonly repeatBtn: string
} }
export default styles export default styles

View File

@ -7,7 +7,8 @@ import {
Text, Text,
StringSubstitute, StringSubstitute,
Button, Button,
PageError PageError,
ButtonSize
} from '@harness/uicore' } from '@harness/uicore'
import { Match, Case, Render } from 'react-jsx-match' import { Match, Case, Render } from 'react-jsx-match'
import * as Diff2Html from 'diff2html' import * as Diff2Html from 'diff2html'
@ -50,6 +51,8 @@ interface ChangesProps extends Pick<GitInfoProps, 'repoMetadata'> {
pullRequestMetadata?: TypesPullReq pullRequestMetadata?: TypesPullReq
className?: string className?: string
onCommentUpdate: () => void onCommentUpdate: () => void
prHasChanged?: boolean
handleRefresh?: () => void
} }
export const Changes: React.FC<ChangesProps> = ({ export const Changes: React.FC<ChangesProps> = ({
@ -61,7 +64,9 @@ export const Changes: React.FC<ChangesProps> = ({
emptyMessage, emptyMessage,
pullRequestMetadata, pullRequestMetadata,
onCommentUpdate, onCommentUpdate,
className className,
prHasChanged,
handleRefresh
}) => { }) => {
const { getString } = useStrings() const { getString } = useStrings()
const [viewStyle, setViewStyle] = useUserPreference(UserPreference.DIFF_VIEW_STYLE, ViewStyle.SIDE_BY_SIDE) const [viewStyle, setViewStyle] = useUserPreference(UserPreference.DIFF_VIEW_STYLE, ViewStyle.SIDE_BY_SIDE)
@ -174,6 +179,18 @@ export const Changes: React.FC<ChangesProps> = ({
}} }}
/> />
</Text> </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 */} {/* Show "Scroll to top" button */}
<Render when={isSticky}> <Render when={isSticky}>

View File

@ -39,3 +39,8 @@
} }
} }
} }
.refreshIcon {
padding-left: var(--spacing-xsmall) !important;
padding-right: var(--spacing-xsmall);
}

View File

@ -6,5 +6,6 @@ declare const styles: {
readonly row: string readonly row: string
readonly rowText: string readonly rowText: string
readonly label: string readonly label: string
readonly refreshIcon: string
} }
export default styles export default styles

View File

@ -1,5 +1,16 @@
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { Container, Color, TableV2 as Table, Text, Avatar, Layout } from '@harness/uicore' import {
Container,
Color,
TableV2 as Table,
Text,
Avatar,
Layout,
ButtonVariation,
ButtonSize,
Button,
FlexExpander
} from '@harness/uicore'
import type { CellProps, Column } from 'react-table' import type { CellProps, Column } from 'react-table'
import { orderBy } from 'lodash-es' import { orderBy } from 'lodash-es'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
@ -16,9 +27,18 @@ interface CommitsViewProps extends Pick<GitInfoProps, 'repoMetadata'> {
commits: TypesCommit[] commits: TypesCommit[]
emptyTitle: string emptyTitle: string
emptyMessage: string emptyMessage: string
prHasChanged: boolean
handleRefresh: () => void
} }
export function CommitsView({ repoMetadata, commits, emptyTitle, emptyMessage }: CommitsViewProps) { export function CommitsView({
repoMetadata,
commits,
emptyTitle,
emptyMessage,
handleRefresh,
prHasChanged
}: CommitsViewProps) {
const { getString } = useStrings() const { getString } = useStrings()
const { routes } = useAppContext() const { routes } = useAppContext()
const columns: Column<TypesCommit>[] = useMemo( const columns: Column<TypesCommit>[] = useMemo(
@ -79,6 +99,20 @@ export function CommitsView({ repoMetadata, commits, emptyTitle, emptyMessage }:
return ( return (
<Container className={css.container}> <Container className={css.container}>
<Layout.Horizontal>
<FlexExpander />
{!prHasChanged ? null : (
<Button
onClick={handleRefresh}
iconProps={{ className: css.refreshIcon, size: 12 }}
icon="repeat"
text={getString('refresh')}
variation={ButtonVariation.SECONDARY}
size={ButtonSize.SMALL}
margin={{ bottom: 'small' }}
/>
)}
</Layout.Horizontal>
{!!commits.length && {!!commits.length &&
Object.entries(commitsGroupedByDate).map(([date, commitsByDate]) => { Object.entries(commitsGroupedByDate).map(([date, commitsByDate]) => {
return ( return (

View File

@ -220,6 +220,7 @@ export interface StringsMap {
pullRequests: string pullRequests: string
quote: string quote: string
readMe: string readMe: string
refresh: string
rejected: string rejected: string
remove: string remove: string
renameFile: string renameFile: string

View File

@ -329,4 +329,5 @@ required: Required
noneYet: None Yet noneYet: None Yet
noOptionalReviewers: No Optional Reviewers noOptionalReviewers: No Optional Reviewers
noRequiredReviewers: No Required Reviewers noRequiredReviewers: No Required Reviewers
reviewers: Reviewers reviewers: Reviewers
refresh: Refresh

View File

@ -82,3 +82,32 @@
box-shadow: none; box-shadow: none;
} }
} }
.refreshIcon {
padding-left: var(--spacing-xsmall) !important;
padding-right: var(--spacing-xsmall);
}
.repeatBtn:hover {
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.04), 0px 2px 4px rgba(96, 97, 112, 0.16) !important;
}
.repeatBtn {
margin-left: var(--spacing-xsmall) !important;
padding: 5px 1px !important;
background-color: var(--orange-100) !important;
color: #c05809 !important;
--background-color-hover: var(--grey-50) !important;
--background-color-active: var(--grey-100) !important;
--border: none !important;
--padding: 12px;
--icon-padding: 4px;
--font-size: 13px;
--font-weight: 500;
min-height: 24px !important;
height: 24px !important;
.bp3-button-text {
color: var(--orange-300);
}
}

View File

@ -9,5 +9,8 @@ declare const styles: {
readonly snapshotContent: string readonly snapshotContent: string
readonly newCommentCreated: string readonly newCommentCreated: string
readonly clear: string readonly clear: string
readonly refreshIcon: string
readonly repeatBtn: string
readonly bp3ButtonText: string
} }
export default styles export default styles

View File

@ -1,6 +1,9 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { import {
Avatar, Avatar,
Button,
ButtonSize,
ButtonVariation,
Color, Color,
Container, Container,
FlexExpander, FlexExpander,
@ -38,9 +41,17 @@ import css from './Conversation.module.scss'
interface ConversationProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> { interface ConversationProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
onCommentUpdate: () => void onCommentUpdate: () => void
prHasChanged?: boolean
handleRefresh?: () => void
} }
export const Conversation: React.FC<ConversationProps> = ({ repoMetadata, pullRequestMetadata, onCommentUpdate }) => { export const Conversation: React.FC<ConversationProps> = ({
repoMetadata,
pullRequestMetadata,
onCommentUpdate,
prHasChanged,
handleRefresh
}) => {
const { getString } = useStrings() const { getString } = useStrings()
const { currentUser } = useAppContext() const { currentUser } = useAppContext()
const { const {
@ -126,6 +137,21 @@ export const Conversation: React.FC<ConversationProps> = ({ repoMetadata, pullRe
onPRStateChanged={refreshPR} onPRStateChanged={refreshPR}
/> />
<Container> <Container>
<Layout.Horizontal width={`70%`}>
<FlexExpander />
{!prHasChanged ? null : (
<Button
onClick={handleRefresh}
iconProps={{ className: css.refreshIcon, size: 12 }}
icon="repeat"
text={getString('refresh')}
variation={ButtonVariation.SECONDARY}
size={ButtonSize.SMALL}
margin={{ bottom: 'small' }}
/>
)}
</Layout.Horizontal>
<Layout.Horizontal> <Layout.Horizontal>
<Container width={`70%`}> <Container width={`70%`}>
<Layout.Vertical spacing="xlarge"> <Layout.Vertical spacing="xlarge">

View File

@ -74,9 +74,9 @@ const PullRequestSideBar = (props: PullRequestSideBarProps) => {
{getString('required')} {getString('required')}
</Text> </Text>
{reviewers && reviewers?.length !== 0 ? ( {reviewers && reviewers?.length !== 0 ? (
reviewers.map((reviewer: { reviewer: { display_name: string }; review_decision: string }): any => { reviewers.map((reviewer: { reviewer: { display_name: string, id:number }; review_decision: string }): any => {
return ( return (
<Layout.Horizontal> <Layout.Horizontal key={reviewer.reviewer.id}>
<Icon className={css.reviewerPadding} {...generateReviewDecisionIcon(reviewer.review_decision)} /> <Icon className={css.reviewerPadding} {...generateReviewDecisionIcon(reviewer.review_decision)} />
<Avatar <Avatar
className={css.reviewerAvatar} className={css.reviewerAvatar}
@ -129,9 +129,9 @@ const PullRequestSideBar = (props: PullRequestSideBarProps) => {
{getString('optional')} {getString('optional')}
</Text> </Text>
{reviewers && reviewers?.length !== 0 ? ( {reviewers && reviewers?.length !== 0 ? (
reviewers.map((reviewer: { reviewer: { display_name: string }; review_decision: string }) => { reviewers.map((reviewer: { reviewer: { display_name: string, id: number }; review_decision: string }) => {
return ( return (
<Layout.Horizontal> <Layout.Horizontal key={reviewer.reviewer.id}>
<Icon className={css.reviewerPadding} name="dot" /> <Icon className={css.reviewerPadding} name="dot" />
<Avatar <Avatar
className={css.reviewerAvatar} className={css.reviewerAvatar}

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { import {
Container, Container,
PageBody, PageBody,
@ -21,7 +21,7 @@ import { useAppContext } from 'AppContext'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader' import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
import { voidFn, getErrorMessage } from 'utils/Utils' import { voidFn, getErrorMessage, PR_POLLING_LIMIT } from 'utils/Utils'
import { CodeIcon, GitInfoProps } from 'utils/GitUtils' import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
import type { TypesPullReq, TypesRepository } from 'services/code' import type { TypesPullReq, TypesRepository } from 'services/code'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
@ -41,6 +41,7 @@ export default function PullRequest() {
const history = useHistory() const history = useHistory()
const { getString } = useStrings() const { getString } = useStrings()
const { routes } = useAppContext() const { routes } = useAppContext()
const [prHasChanged, setPrHasChanged] = useState(false)
const { const {
repoMetadata, repoMetadata,
error, error,
@ -49,6 +50,7 @@ export default function PullRequest() {
pullRequestId, pullRequestId,
pullRequestSection = PullRequestSection.CONVERSATION pullRequestSection = PullRequestSection.CONVERSATION
} = useGetRepositoryMetadata() } = useGetRepositoryMetadata()
const { const {
data: prData, data: prData,
error: prError, error: prError,
@ -58,6 +60,40 @@ export default function PullRequest() {
path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}`, path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}`,
lazy: !repoMetadata lazy: !repoMetadata
}) })
const {
data: pollPrData,
error: pollPrError,
refetch: refetchPollPullRequest
} = useGet<TypesPullReq>({
path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}`,
lazy: !repoMetadata
})
const [newPrData, setNewPrData] = useState<TypesPullReq>()
const handleRefresh = () => {
refetchPullRequest()
setNewPrData(prData as TypesPullReq)
setPrHasChanged(false)
}
useEffect(() => {
const interval = window.setTimeout(() => {
refetchPollPullRequest()
setNewPrData(pollPrData as TypesPullReq)
}, PR_POLLING_LIMIT)
return () => window.clearTimeout(interval)
}, [pollPrData])
useEffect(() => {
if (prData?.stats && newPrData?.stats) {
const prStatsChanged =
prData.stats.commits !== newPrData.stats.commits || prData.stats.files_changed !== newPrData.stats.files_changed
if (prStatsChanged) {
setPrHasChanged(prStatsChanged)
}
}
}, [newPrData])
const activeTab = useMemo( const activeTab = useMemo(
() => () =>
Object.values(PullRequestSection).find(value => value === pullRequestSection) Object.values(PullRequestSection).find(value => value === pullRequestSection)
@ -65,6 +101,7 @@ export default function PullRequest() {
: PullRequestSection.CONVERSATION, : PullRequestSection.CONVERSATION,
[pullRequestSection] [pullRequestSection]
) )
// /repos/${paramsInPath.repo_ref}/pullreq/${paramsInPath.pullreq_number}/metadata
return ( return (
<Container className={css.main}> <Container className={css.main}>
@ -81,7 +118,7 @@ export default function PullRequest() {
] ]
} }
/> />
<PageBody error={getErrorMessage(error || prError)} retryOnError={voidFn(refetch)}> <PageBody error={getErrorMessage(error || prError || pollPrError)} retryOnError={voidFn(refetch)}>
<LoadingSpinner visible={loading || prLoading} withBorder={!!prData && prLoading} /> <LoadingSpinner visible={loading || prLoading} withBorder={!!prData && prLoading} />
<Render when={repoMetadata && prData}> <Render when={repoMetadata && prData}>
@ -116,6 +153,8 @@ export default function PullRequest() {
repoMetadata={repoMetadata as TypesRepository} repoMetadata={repoMetadata as TypesRepository}
pullRequestMetadata={prData as TypesPullReq} pullRequestMetadata={prData as TypesPullReq}
onCommentUpdate={voidFn(refetchPullRequest)} onCommentUpdate={voidFn(refetchPullRequest)}
prHasChanged={prHasChanged}
handleRefresh={voidFn(handleRefresh)}
/> />
) )
}, },
@ -132,6 +171,8 @@ export default function PullRequest() {
<PullRequestCommits <PullRequestCommits
repoMetadata={repoMetadata as TypesRepository} repoMetadata={repoMetadata as TypesRepository}
pullRequestMetadata={prData as TypesPullReq} pullRequestMetadata={prData as TypesPullReq}
prHasChanged={prHasChanged}
handleRefresh={voidFn(handleRefresh)}
/> />
) )
}, },
@ -154,6 +195,8 @@ export default function PullRequest() {
emptyTitle={getString('noChanges')} emptyTitle={getString('noChanges')}
emptyMessage={getString('noChangesPR')} emptyMessage={getString('noChangesPR')}
onCommentUpdate={voidFn(refetchPullRequest)} onCommentUpdate={voidFn(refetchPullRequest)}
prHasChanged={prHasChanged}
handleRefresh={voidFn(handleRefresh)}
/> />
</Container> </Container>
) )

View File

@ -9,9 +9,16 @@ import { ResourceListingPagination } from 'components/ResourceListingPagination/
import { CommitsView } from 'components/CommitsView/CommitsView' import { CommitsView } from 'components/CommitsView/CommitsView'
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper' import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
export const PullRequestCommits: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = ({ interface CommitProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
prHasChanged: boolean
handleRefresh: () => void
}
export const PullRequestCommits: React.FC<CommitProps> = ({
repoMetadata, repoMetadata,
pullRequestMetadata pullRequestMetadata,
prHasChanged,
handleRefresh
}) => { }) => {
const limit = LIST_FETCHING_LIMIT const limit = LIST_FETCHING_LIMIT
const [page, setPage] = usePageIndex() const [page, setPage] = usePageIndex()
@ -40,6 +47,8 @@ export const PullRequestCommits: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'p
repoMetadata={repoMetadata} repoMetadata={repoMetadata}
emptyTitle={getString('noCommits')} emptyTitle={getString('noCommits')}
emptyMessage={getString('noCommitsPR')} emptyMessage={getString('noCommitsPR')}
prHasChanged={prHasChanged}
handleRefresh={voidFn(handleRefresh)}
/> />
<ResourceListingPagination response={response} page={page} setPage={setPage} /> <ResourceListingPagination response={response} page={page} setPage={setPage} />

View File

@ -10,6 +10,7 @@ export const DEFAULT_BRANCH_NAME = 'main'
export const REGEX_VALID_REPO_NAME = /^[a-zA-Z_][0-9a-zA-Z-_.$]*$/ export const REGEX_VALID_REPO_NAME = /^[a-zA-Z_][0-9a-zA-Z-_.$]*$/
export const SUGGESTED_BRANCH_NAMES = [DEFAULT_BRANCH_NAME, 'master'] export const SUGGESTED_BRANCH_NAMES = [DEFAULT_BRANCH_NAME, 'master']
export const FILE_SEPERATOR = '/' export const FILE_SEPERATOR = '/'
export const PR_POLLING_LIMIT = 15000
/** This utility shows a toaster without being bound to any component. /** This utility shows a toaster without being bound to any component.
* It's useful to show cross-page/component messages */ * It's useful to show cross-page/component messages */