Consolidate pagination for the whole codebase + Use SearchInputWithSpinner component (#222)

* Remove center layout for empty repo info - hard to read

* Consolidate pagination for the whole codebase

* Use SearchInputWithSpinner component

* Use SearchInputWithSpinner component
This commit is contained in:
Tan Nhu 2023-01-17 14:59:56 -08:00 committed by GitHub
parent 66cc979334
commit 1aa1123bd2
34 changed files with 397 additions and 249 deletions

View File

@ -1,40 +0,0 @@
import React from 'react'
import cx from 'classnames'
import { Button, ButtonSize, Container, Layout } from '@harness/uicore'
import { useStrings } from 'framework/strings'
import css from './PrevNextPagination.module.scss'
interface PrevNextPaginationProps {
onPrev?: false | (() => void)
onNext?: false | (() => void)
skipLayout?: boolean
}
export function PrevNextPagination({ onPrev, onNext, skipLayout }: PrevNextPaginationProps) {
const { getString } = useStrings()
return (
<Container className={skipLayout ? undefined : css.main}>
<Layout.Horizontal>
<Button
text={getString('prev')}
icon="arrow-left"
size={ButtonSize.SMALL}
className={cx(css.roundedButton, css.buttonLeft)}
iconProps={{ size: 12 }}
onClick={onPrev ? onPrev : undefined}
disabled={!onPrev}
/>
<Button
text={getString('next')}
rightIcon="arrow-right"
size={ButtonSize.SMALL}
className={cx(css.roundedButton, css.buttonRight)}
iconProps={{ size: 12 }}
onClick={onNext ? onNext : undefined}
disabled={!onNext}
/>
</Layout.Horizontal>
</Container>
)
}

View File

@ -1,3 +1,7 @@
.pagination {
padding-top: 0;
}
.main {
display: flex;
align-items: center;

View File

@ -1,6 +1,7 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly pagination: string
readonly main: string
readonly roundedButton: string
readonly selected: string

View File

@ -0,0 +1,107 @@
import React, { useCallback, useMemo } from 'react'
import cx from 'classnames'
import { Button, ButtonSize, Container, Layout, Pagination } from '@harness/uicore'
import { useStrings } from 'framework/strings'
import css from './ResourceListingPagination.module.scss'
interface ResourceListingPaginationProps {
response: Response | null
page: number
setPage: React.Dispatch<React.SetStateAction<number>>
scrollTop?: boolean
}
// There are two type of pagination results returned from Code API.
// One returns information that works with UICore Pagination component in which we know total pages, total items, etc... The other
// has only information to render Prev, Next.
//
// This component consolidates both cases to remove same pagination logic in pages and components.
export const ResourceListingPagination: React.FC<ResourceListingPaginationProps> = ({
response,
page,
setPage,
scrollTop = true
}) => {
const { X_NEXT_PAGE, X_PREV_PAGE, totalItems, totalPages, pageSize } = useParsePaginationInfo(response)
const _setPage = useCallback(
(_page: number) => {
if (scrollTop) {
setTimeout(() => {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
})
}, 0)
}
setPage(_page)
},
[setPage, scrollTop]
)
return totalItems ? (
page === 1 && totalItems < pageSize ? null : (
<Container margin={{ left: 'medium', right: 'medium' }}>
<Pagination
className={css.pagination}
hidePageNumbers
gotoPage={index => _setPage(index + 1)}
itemCount={totalItems}
pageCount={totalPages}
pageIndex={page - 1}
pageSize={pageSize}
/>
</Container>
)
) : page === 1 && !X_PREV_PAGE && !X_NEXT_PAGE ? null : (
<PrevNextPagination
onPrev={!!X_PREV_PAGE && (() => _setPage(page - 1))}
onNext={!!X_NEXT_PAGE && (() => _setPage(page + 1))}
/>
)
}
function useParsePaginationInfo(response: Nullable<Response>) {
const totalItems = useMemo(() => parseInt(response?.headers?.get('x-total') || '0'), [response])
const totalPages = useMemo(() => parseInt(response?.headers?.get('x-total-pages') || '0'), [response])
const pageSize = useMemo(() => parseInt(response?.headers?.get('x-per-page') || '0'), [response])
const X_NEXT_PAGE = useMemo(() => parseInt(response?.headers?.get('x-next-page') || '0'), [response])
const X_PREV_PAGE = useMemo(() => parseInt(response?.headers?.get('x-prev-page') || '0'), [response])
return { totalItems, totalPages, pageSize, X_NEXT_PAGE, X_PREV_PAGE }
}
interface PrevNextPaginationProps {
onPrev?: false | (() => void)
onNext?: false | (() => void)
skipLayout?: boolean
}
function PrevNextPagination({ onPrev, onNext, skipLayout }: PrevNextPaginationProps) {
const { getString } = useStrings()
return (
<Container className={skipLayout ? undefined : css.main}>
<Layout.Horizontal>
<Button
text={getString('prev')}
icon="arrow-left"
size={ButtonSize.SMALL}
className={cx(css.roundedButton, css.buttonLeft)}
iconProps={{ size: 12 }}
onClick={onPrev ? onPrev : undefined}
disabled={!onPrev}
/>
<Button
text={getString('next')}
rightIcon="arrow-right"
size={ButtonSize.SMALL}
className={cx(css.roundedButton, css.buttonRight)}
iconProps={{ size: 12 }}
onClick={onNext ? onNext : undefined}
disabled={!onNext}
/>
</Layout.Horizontal>
</Container>
)
}

View File

@ -0,0 +1,20 @@
.main {
&,
.layout {
display: inline-flex;
align-items: center;
justify-content: center;
}
.wrapper {
padding: 0 0 0 var(--spacing-small) !important;
margin-bottom: 0 !important;
.input {
span[data-icon],
span[icon] {
margin-top: 10px !important;
}
}
}
}

View File

@ -0,0 +1,9 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly main: string
readonly layout: string
readonly wrapper: string
readonly input: string
}
export default styles

View File

@ -0,0 +1,44 @@
import React from 'react'
import { Color, Container, Icon, IconName, Layout, TextInput } from '@harness/uicore'
import { useStrings } from 'framework/strings'
import css from './SearchInputWithSpinner.module.scss'
interface SearchInputWithSpinnerProps {
query?: string
setQuery: (value: string) => void
loading?: boolean
width?: number
placeholder?: string
icon?: IconName
spinnerIcon?: IconName
}
export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
query = '',
setQuery,
loading = false,
width = 250,
placeholder,
icon = 'search',
spinnerIcon = 'spinner'
}) => {
const { getString } = useStrings()
return (
<Container className={css.main}>
<Layout.Horizontal className={css.layout}>
{loading && <Icon name={spinnerIcon as IconName} color={Color.PRIMARY_7} />}
<TextInput
value={query}
wrapperClassName={css.wrapper}
className={css.input}
placeholder={placeholder || getString('search')}
leftIcon={icon as IconName}
style={{ width }}
autoFocus
onFocus={event => event.target.select()}
onInput={event => setQuery(event.currentTarget.value || '')}
/>
</Layout.Horizontal>
</Container>
)
}

View File

@ -221,6 +221,7 @@ export interface StringsMap {
webhookCreated: string
webhookDeleted: string
webhookDetails: string
webhookEmpty: string
webhookEventsLabel: string
webhookListingContent: string
webhookSelectAllEvents: string

View File

@ -1,11 +0,0 @@
import { useMemo } from 'react'
export function useGetPaginationInfo(response: Nullable<Response>) {
const totalItems = useMemo(() => parseInt(response?.headers?.get('x-total') || '0'), [response])
const totalPages = useMemo(() => parseInt(response?.headers?.get('x-total-pages') || '0'), [response])
const pageSize = useMemo(() => parseInt(response?.headers?.get('x-per-page') || '0'), [response])
const X_NEXT_PAGE = useMemo(() => parseInt(response?.headers?.get('x-next-page') || '0'), [response])
const X_PREV_PAGE = useMemo(() => parseInt(response?.headers?.get('x-prev-page') || '0'), [response])
return { totalItems, totalPages, pageSize, X_NEXT_PAGE, X_PREV_PAGE }
}

View File

@ -1,5 +1,5 @@
import { useState } from 'react'
export function usePageIndex(index = 0) {
export function usePageIndex(index = 1) {
return useState(index)
}

View File

@ -0,0 +1,14 @@
import { useToaster } from '@harness/uicore'
import { useEffect } from 'react'
import type { GetDataError } from 'restful-react'
import { getErrorMessage } from 'utils/Utils'
export function useShowRequestError(error: GetDataError<Unknown> | null) {
const { showError } = useToaster()
useEffect(() => {
if (error) {
showError(getErrorMessage(error))
}
}, [error, showError])
}

View File

@ -264,3 +264,4 @@ repoEmptyMarkdown: |
```
You might need [to create an API token](CREATE_API_TOKEN_URL) in order to pull from or push into this repository.
webhookEmpty: Here is no WebHooks. Try to

View File

@ -12,8 +12,8 @@ import { makeDiffRefs } from 'utils/GitUtils'
import { CommitsView } from 'components/CommitsView/CommitsView'
import { Changes } from 'components/Changes/Changes'
import type { RepoCommit } from 'services/code'
import { PrevNextPagination } from 'components/PrevNextPagination/PrevNextPagination'
import { usePageIndex } from 'hooks/usePageIndex'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { CompareContentHeader } from './CompareContentHeader/CompareContentHeader'
import css from './Compare.module.scss'
@ -24,17 +24,18 @@ export default function Compare() {
const { repoMetadata, error, loading, diffRefs } = useGetRepositoryMetadata()
const [sourceGitRef, setSourceGitRef] = useState(diffRefs.sourceGitRef)
const [targetGitRef, setTargetGitRef] = useState(diffRefs.targetGitRef)
const [pageIndex, setPageIndex] = usePageIndex()
const [page, setPage] = usePageIndex()
const limit = LIST_FETCHING_LIMIT
const {
data: commits,
error: commitsError,
refetch
refetch,
response
} = useGet<RepoCommit[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/commits`,
queryParams: {
limit,
page: pageIndex + 1,
page,
git_ref: sourceGitRef,
after: targetGitRef
},
@ -87,7 +88,7 @@ export default function Compare() {
id="branchesTags"
defaultSelectedTabId="diff"
large={false}
onChange={() => setPageIndex(0)}
onChange={() => setPage(1)}
tabList={[
{
id: 'commits',
@ -95,10 +96,7 @@ export default function Compare() {
panel: (
<Container padding="xlarge">
{!!commits?.length && <CommitsView commits={commits} repoMetadata={repoMetadata} />}
<PrevNextPagination
onPrev={pageIndex > 0 && (() => setPageIndex(pageIndex - 1))}
onNext={commits?.length === limit && (() => setPageIndex(pageIndex + 1))}
/>
<ResourceListingPagination response={response} page={page} setPage={setPage} />
</Container>
)
},

View File

@ -4,8 +4,8 @@ import type { RepoCommit } from 'services/code'
import type { GitInfoProps } from 'utils/GitUtils'
import { voidFn, LIST_FETCHING_LIMIT } from 'utils/Utils'
import { usePageIndex } from 'hooks/usePageIndex'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { CommitsView } from 'components/CommitsView/CommitsView'
import { PrevNextPagination } from 'components/PrevNextPagination/PrevNextPagination'
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
export const PullRequestCommits: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'>> = ({
@ -13,17 +13,18 @@ export const PullRequestCommits: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'p
pullRequestMetadata
}) => {
const limit = LIST_FETCHING_LIMIT
const [pageIndex, setPageIndex] = usePageIndex()
const [page, setPage] = usePageIndex()
const {
data: commits,
error,
loading,
refetch
refetch,
response
} = useGet<RepoCommit[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/commits`,
queryParams: {
limit,
page: pageIndex + 1,
page,
git_ref: pullRequestMetadata.source_branch,
after: pullRequestMetadata.target_branch
},
@ -34,10 +35,7 @@ export const PullRequestCommits: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'p
<PullRequestTabContentWrapper loading={loading} error={error} onRetry={voidFn(refetch)}>
{!!commits?.length && <CommitsView commits={commits} repoMetadata={repoMetadata} />}
<PrevNextPagination
onPrev={pageIndex > 0 && (() => setPageIndex(pageIndex - 1))}
onNext={commits?.length === limit && (() => setPageIndex(pageIndex + 1))}
/>
<ResourceListingPagination response={response} page={page} setPage={setPage} />
</PullRequestTabContentWrapper>
)
}

View File

@ -25,6 +25,7 @@ import { voidFn, getErrorMessage, LIST_FETCHING_LIMIT } from 'utils/Utils'
import emptyStateImage from 'images/empty-state.svg'
import { usePageIndex } from 'hooks/usePageIndex'
import type { TypesPullReq } from 'services/code'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { PullRequestsContentHeader } from './PullRequestsContentHeader/PullRequestsContentHeader'
import prImgOpen from './pull-request-open.svg'
import prImgMerged from './pull-request-merged.svg'
@ -39,17 +40,18 @@ export default function PullRequests() {
const { routes } = useAppContext()
const [searchTerm, setSearchTerm] = useState('')
const [filter, setFilter] = useState<string>(PullRequestFilterOption.OPEN)
const [pageIndex, setPageIndex] = usePageIndex()
const [page, setPage] = usePageIndex()
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
const {
data,
error: prError,
loading: prLoading
loading: prLoading,
response
} = useGet<TypesPullReq[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq`,
queryParams: {
limit: String(LIST_FETCHING_LIMIT),
page: String(pageIndex + 1),
page,
sort: filter == PullRequestFilterOption.MERGED ? 'merged' : 'number',
order: 'desc',
query: searchTerm,
@ -113,30 +115,33 @@ export default function PullRequests() {
repoMetadata={repoMetadata}
onPullRequestFilterChanged={_filter => {
setFilter(_filter)
setPageIndex(0)
setPage(1)
}}
onSearchTermChanged={value => {
setSearchTerm(value)
setPageIndex(0)
setPage(1)
}}
/>
<Container padding="xlarge">
{!!data?.length && (
<TableV2<TypesPullReq>
className={css.table}
hideHeaders
columns={columns}
data={data}
getRowClassName={() => css.row}
onRowClick={row => {
history.push(
routes.toCODEPullRequest({
repoPath: repoMetadata.path as string,
pullRequestId: String(row.number)
})
)
}}
/>
<>
<TableV2<TypesPullReq>
className={css.table}
hideHeaders
columns={columns}
data={data}
getRowClassName={() => css.row}
onRowClick={row => {
history.push(
routes.toCODEPullRequest({
repoPath: repoMetadata.path as string,
pullRequestId: String(row.number)
})
)
}}
/>
<ResourceListingPagination response={response} page={page} setPage={setPage} />
</>
)}
{data?.length === 0 && (
<Container className={css.noData}>

View File

@ -9,15 +9,6 @@
> div {
align-items: center;
}
.input {
margin-bottom: 0 !important;
span[data-icon],
span[icon] {
margin-top: 10px !important;
}
}
}
.branchDropdown {

View File

@ -2,7 +2,6 @@
// this is an auto-generated file
declare const styles: {
readonly main: string
readonly input: string
readonly branchDropdown: string
}
export default styles

View File

@ -1,9 +1,10 @@
import { useHistory } from 'react-router-dom'
import React, { useMemo, useState } from 'react'
import { Container, Layout, FlexExpander, DropDown, ButtonVariation, TextInput, Button } from '@harness/uicore'
import { Container, Layout, FlexExpander, DropDown, ButtonVariation, Button } from '@harness/uicore'
import { useStrings } from 'framework/strings'
import { CodeIcon, GitInfoProps, makeDiffRefs, PullRequestFilterOption } from 'utils/GitUtils'
import { useAppContext } from 'AppContext'
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
import css from './PullRequestsContentHeader.module.scss'
interface PullRequestsContentHeaderProps extends Pick<GitInfoProps, 'repoMetadata'> {
@ -36,7 +37,6 @@ export function PullRequestsContentHeader({
],
[getString]
)
const showSpinner = useMemo(() => loading, [loading])
return (
<Container className={css.main} padding="xlarge">
@ -51,18 +51,13 @@ export function PullRequestsContentHeader({
popoverClassName={css.branchDropdown}
/>
<FlexExpander />
<TextInput
className={css.input}
placeholder={getString('search')}
autoFocus
onFocus={event => event.target.select()}
value={searchTerm}
onInput={event => {
const value = event.currentTarget.value
<SearchInputWithSpinner
loading={loading}
query={searchTerm}
setQuery={value => {
setSearchTerm(value)
onSearchTermChanged(value)
}}
leftIcon={showSpinner ? CodeIcon.InputSpinner : CodeIcon.InputSearch}
/>
<Button
variation={ButtonVariation.PRIMARY}

View File

@ -13,6 +13,10 @@
}
}
.layout {
align-items: center;
}
.input {
span[data-icon],
span[icon] {
@ -98,7 +102,3 @@
padding-top: var(--spacing-xsmall) !important;
}
}
.pagination {
padding-top: 0;
}

View File

@ -2,6 +2,7 @@
// this is an auto-generated file
declare const styles: {
readonly main: string
readonly layout: string
readonly input: string
readonly withError: string
readonly table: string
@ -13,6 +14,5 @@ declare const styles: {
readonly repoName: string
readonly repoScope: string
readonly desc: string
readonly pagination: string
}
export default styles

View File

@ -9,9 +9,7 @@ import {
TableV2 as Table,
Text,
Color,
Pagination,
Icon,
TextInput
Icon
} from '@harness/uicore'
import type { CellProps, Column } from 'react-table'
import Keywords from 'react-keywords'
@ -23,10 +21,10 @@ import { voidFn, formatDate, getErrorMessage, LIST_FETCHING_LIMIT } from 'utils/
import { NewRepoModalButton } from 'components/NewRepoModalButton/NewRepoModalButton'
import type { TypesRepository } from 'services/code'
import { usePageIndex } from 'hooks/usePageIndex'
import { useGetPaginationInfo } from 'hooks/useGetPaginationInfo'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
import { useAppContext } from 'AppContext'
import { CodeIcon } from 'utils/GitUtils'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import emptyStateImage from './empty-state.svg'
import css from './RepositoriesListing.module.scss'
@ -38,7 +36,7 @@ export default function RepositoriesListing() {
const space = useGetSpaceParam()
const [searchTerm, setSearchTerm] = useState<string | undefined>()
const { routes } = useAppContext()
const [pageIndex, setPageIndex] = usePageIndex()
const [page, setPage] = usePageIndex()
const {
data: repositories,
error,
@ -47,14 +45,13 @@ export default function RepositoriesListing() {
response
} = useGet<TypesRepository[]>({
path: `/api/v1/spaces/${space}/+/repos`,
queryParams: { page: pageIndex + 1, limit: LIST_FETCHING_LIMIT, query: searchTerm }
queryParams: { page, limit: LIST_FETCHING_LIMIT, query: searchTerm }
})
const { totalItems, totalPages, pageSize } = useGetPaginationInfo(response)
useEffect(() => {
setSearchTerm(undefined)
setPageIndex(0)
}, [space, setPageIndex])
setPage(1)
}, [space, setPage])
const columns: Column<TypesRepository>[] = useMemo(
() => [
@ -138,20 +135,10 @@ export default function RepositoriesListing() {
button: NewRepoButton
}}>
<Container padding="xlarge">
<Layout.Horizontal spacing="large">
<Layout.Horizontal spacing="large" className={css.layout}>
{NewRepoButton}
<FlexExpander />
<TextInput
className={css.input}
placeholder={getString('search')}
leftIcon={loading && searchTerm !== undefined ? CodeIcon.InputSpinner : CodeIcon.InputSearch}
style={{ width: 250 }}
autoFocus
onInput={event => {
setSearchTerm(event.currentTarget.value || '')
setPageIndex(0)
}}
/>
<SearchInputWithSpinner loading={loading} query={searchTerm} setQuery={setSearchTerm} />
</Layout.Horizontal>
<Container margin={{ top: 'medium' }}>
<Table<TypesRepository>
@ -164,19 +151,7 @@ export default function RepositoriesListing() {
getRowClassName={row => cx(css.row, !row.original.description && css.noDesc)}
/>
</Container>
{!!repositories?.length && (
<Container margin={{ left: 'medium', right: 'medium' }}>
<Pagination
className={css.pagination}
hidePageNumbers
gotoPage={index => setPageIndex(index)}
itemCount={totalItems}
pageCount={totalPages}
pageIndex={pageIndex}
pageSize={pageSize}
/>
</Container>
)}
<ResourceListingPagination response={response} page={page} setPage={setPage} />
</Container>
</PageBody>
</Container>

View File

@ -1,11 +1,12 @@
import React from 'react'
import { Container, Color, Layout, Button, FlexExpander, ButtonVariation, Heading } from '@harness/uicore'
import { Container, Color, Layout, Button, FlexExpander, ButtonVariation, Heading, Icon } from '@harness/uicore'
import { useHistory } from 'react-router-dom'
import { useGet } from 'restful-react'
import cx from 'classnames'
import { MarkdownViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
import { useAppContext } from 'AppContext'
import type { OpenapiContentInfo, OpenapiGetContentOutput, RepoFileContent, TypesRepository } from 'services/code'
import { useShowRequestError } from 'hooks/useShowRequestError'
import { CodeIcon } from 'utils/GitUtils'
import css from './Readme.module.scss'
@ -21,7 +22,7 @@ function ReadmeViewer({ metadata, gitRef, readmeInfo, contentOnly, maxWidth }: F
const history = useHistory()
const { routes } = useAppContext()
const { data /*error, loading, refetch, response */ } = useGet<OpenapiGetContentOutput>({
const { data, error, loading } = useGet<OpenapiGetContentOutput>({
path: `/api/v1/repos/${metadata.path}/+/content/${readmeInfo.path}`,
queryParams: {
include_commit: false,
@ -29,6 +30,8 @@ function ReadmeViewer({ metadata, gitRef, readmeInfo, contentOnly, maxWidth }: F
}
})
useShowRequestError(error)
return (
<Container
className={cx(css.readmeContainer, contentOnly ? css.contentOnly : '')}
@ -38,6 +41,7 @@ function ReadmeViewer({ metadata, gitRef, readmeInfo, contentOnly, maxWidth }: F
<Layout.Horizontal padding="small" className={css.heading}>
<Heading level={5}>{readmeInfo.name}</Heading>
<FlexExpander />
{loading && <Icon name="spinner" color={Color.PRIMARY_7} />}
<Button
variation={ButtonVariation.ICON}
icon={CodeIcon.Edit}

View File

@ -1,11 +1,13 @@
import React, { useMemo, useState } from 'react'
import { Container, Layout, FlexExpander, DropDown, ButtonVariation, TextInput } from '@harness/uicore'
import { Container, Layout, FlexExpander, DropDown, ButtonVariation } from '@harness/uicore'
import { useStrings } from 'framework/strings'
import { GitBranchType, CodeIcon, GitInfoProps } from 'utils/GitUtils'
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
import { CreateBranchModalButton } from 'components/CreateBranchModal/CreateBranchModal'
import css from './BranchesContentHeader.module.scss'
interface BranchesContentHeaderProps extends Pick<GitInfoProps, 'repoMetadata'> {
loading?: boolean
activeBranchType?: GitBranchType
onBranchTypeSwitched: (branchType: GitBranchType) => void
onSearchTermChanged: (searchTerm: string) => void
@ -17,7 +19,8 @@ export function BranchesContentHeader({
onSearchTermChanged,
activeBranchType = GitBranchType.ALL,
repoMetadata,
onNewBranchCreated
onNewBranchCreated,
loading
}: BranchesContentHeaderProps) {
const { getString } = useStrings()
const [branchType, setBranchType] = useState(activeBranchType)
@ -45,13 +48,10 @@ export function BranchesContentHeader({
popoverClassName={css.branchDropdown}
/>
<FlexExpander />
<TextInput
placeholder={getString('searchBranches')}
autoFocus
onFocus={event => event.target.select()}
value={searchTerm}
onInput={event => {
const value = event.currentTarget.value
<SearchInputWithSpinner
loading={loading}
query={searchTerm}
setQuery={value => {
setSearchTerm(value)
onSearchTermChanged(value)
}}

View File

@ -4,11 +4,11 @@ import { useGet } from 'restful-react'
import { useHistory } from 'react-router-dom'
import type { RepoBranch } from 'services/code'
import { usePageIndex } from 'hooks/usePageIndex'
import { useGetPaginationInfo } from 'hooks/useGetPaginationInfo'
import { LIST_FETCHING_LIMIT } from 'utils/Utils'
import { useAppContext } from 'AppContext'
import type { GitInfoProps } from 'utils/GitUtils'
import { PrevNextPagination } from 'components/PrevNextPagination/PrevNextPagination'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { useShowRequestError } from 'hooks/useShowRequestError'
import { BranchesContentHeader } from './BranchesContentHeader/BranchesContentHeader'
import { BranchesContent } from './BranchesContent/BranchesContent'
import css from './RepositoryBranchesContent.module.scss'
@ -17,30 +17,34 @@ export function RepositoryBranchesContent({ repoMetadata }: Pick<GitInfoProps, '
const { routes } = useAppContext()
const history = useHistory()
const [searchTerm, setSearchTerm] = useState('')
const [pageIndex, setPageIndex] = usePageIndex()
const [page, setPage] = usePageIndex()
const {
data: branches,
response /*error, loading,*/,
response,
error,
loading,
refetch
} = useGet<RepoBranch[]>({
path: `/api/v1/repos/${repoMetadata.path}/+/branches`,
queryParams: {
limit: LIST_FETCHING_LIMIT,
page: pageIndex + 1,
page,
sort: 'date',
order: 'desc',
include_commit: true,
query: searchTerm
}
})
const { X_NEXT_PAGE, X_PREV_PAGE } = useGetPaginationInfo(response)
useShowRequestError(error)
return (
<Container padding="xlarge" className={css.resourceContent}>
<BranchesContentHeader
loading={loading}
repoMetadata={repoMetadata}
onBranchTypeSwitched={gitRef => {
setPageIndex(0)
setPage(1)
history.push(
routes.toCODECommits({
repoPath: repoMetadata.path as string,
@ -50,7 +54,7 @@ export function RepositoryBranchesContent({ repoMetadata }: Pick<GitInfoProps, '
}}
onSearchTermChanged={value => {
setSearchTerm(value)
setPageIndex(0)
setPage(1)
}}
onNewBranchCreated={refetch}
/>
@ -64,12 +68,7 @@ export function RepositoryBranchesContent({ repoMetadata }: Pick<GitInfoProps, '
/>
)}
<PrevNextPagination
onPrev={!!X_PREV_PAGE && (() => setPageIndex(pageIndex - 1))}
onNext={!!X_NEXT_PAGE && (() => setPageIndex(pageIndex + 1))}
/>
<ResourceListingPagination response={response} page={page} setPage={setPage} />
</Container>
)
}
// TODO: Handle loading and error

View File

@ -7,10 +7,6 @@
background-color: var(--primary-bg);
}
.pagination {
padding-top: 0;
}
.contentHeader {
> div {
align-items: center;

View File

@ -3,7 +3,6 @@
declare const styles: {
readonly main: string
readonly resourceContent: string
readonly pagination: string
readonly contentHeader: string
}
export default styles

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Container, FlexExpander, Layout, PageBody, Pagination } from '@harness/uicore'
import { Container, FlexExpander, Layout, PageBody } from '@harness/uicore'
import { useHistory } from 'react-router-dom'
import { useGet } from 'restful-react'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
@ -7,11 +7,11 @@ import { useAppContext } from 'AppContext'
import { usePageIndex } from 'hooks/usePageIndex'
import type { RepoCommit } from 'services/code'
import { voidFn, getErrorMessage, LIST_FETCHING_LIMIT } from 'utils/Utils'
import { useGetPaginationInfo } from 'hooks/useGetPaginationInfo'
import { useStrings } from 'framework/strings'
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
import { CommitsView } from '../../components/CommitsView/CommitsView'
import { CommitsView } from 'components/CommitsView/CommitsView'
import css from './RepositoryCommits.module.scss'
export default function RepositoryCommits() {
@ -19,7 +19,7 @@ export default function RepositoryCommits() {
const { routes } = useAppContext()
const history = useHistory()
const { getString } = useStrings()
const [pageIndex, setPageIndex] = usePageIndex()
const [page, setPage] = usePageIndex()
const {
data: commits,
response,
@ -29,12 +29,11 @@ export default function RepositoryCommits() {
path: `/api/v1/repos/${repoMetadata?.path}/+/commits`,
queryParams: {
limit: LIST_FETCHING_LIMIT,
page: pageIndex + 1,
page,
git_ref: commitRef || repoMetadata?.default_branch
},
lazy: !repoMetadata
})
const { totalItems, totalPages, pageSize } = useGetPaginationInfo(response)
return (
<Container className={css.main}>
@ -58,7 +57,7 @@ export default function RepositoryCommits() {
disableViewAllBranches
gitRef={commitRef || (repoMetadata.default_branch as string)}
onSelect={ref => {
setPageIndex(0)
setPage(1)
history.push(
routes.toCODECommits({
repoPath: repoMetadata.path as string,
@ -73,17 +72,7 @@ export default function RepositoryCommits() {
<CommitsView commits={commits} repoMetadata={repoMetadata} />
<Container margin={{ left: 'large', right: 'large' }}>
<Pagination
className={css.pagination}
hidePageNumbers
gotoPage={index => setPageIndex(index)}
itemCount={totalItems}
pageCount={totalPages}
pageIndex={pageIndex}
pageSize={pageSize}
/>
</Container>
<ResourceListingPagination response={response} page={page} setPage={setPage} />
</Container>
)) ||
null}

View File

@ -13,4 +13,8 @@
}
}
}
.noData > div {
height: calc(100vh - var(--page-header-height, 64px) - 120px) !important;
}
}

View File

@ -5,5 +5,6 @@ declare const styles: {
readonly table: string
readonly row: string
readonly title: string
readonly noData: string
}
export default styles

View File

@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React, { useMemo, useState } from 'react'
import {
Button,
Container,
@ -11,7 +11,8 @@ import {
Icon,
Utils,
useToaster,
IconName
IconName,
NoDataCard
} from '@harness/uicore'
import { useHistory } from 'react-router-dom'
import { useGet, useMutate } from 'restful-react'
@ -26,6 +27,7 @@ import emptyStateImage from 'images/empty-state.svg'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
import { useConfirmAct } from 'hooks/useConfirmAction'
import { usePageIndex } from 'hooks/usePageIndex'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import type { OpenapiWebhookType } from 'services/code'
import { WebhooksHeader } from './WebhooksHeader/WebhooksHeader'
import css from './Webhooks.module.scss'
@ -34,20 +36,23 @@ export default function Webhooks() {
const { getString } = useStrings()
const history = useHistory()
const { routes } = useAppContext()
const [pageIndex, setPageIndex] = usePageIndex()
const [page, setPage] = usePageIndex()
const [searchTerm, setSearchTerm] = useState('')
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
const {
data: webhooks,
loading: webhooksLoading,
error: webhooksError,
refetch: refetchWebhooks
refetch: refetchWebhooks,
response
} = useGet<OpenapiWebhookType[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/webhooks`,
queryParams: {
limit: LIST_FETCHING_LIMIT,
page: pageIndex + 1,
page,
sort: 'date',
order: 'asc'
order: 'desc',
query: searchTerm
},
lazy: !repoMetadata
})
@ -115,7 +120,7 @@ export default function Webhooks() {
deleteWebhook({})
.then(() => {
showSuccess(getString('webhookDeleted'), 5000)
setPageIndex(0)
setPage(1)
refetchWebhooks()
})
.catch(exception => {
@ -132,53 +137,68 @@ export default function Webhooks() {
}
}
],
[history, getString, refetchWebhooks, repoMetadata?.path, routes, setPageIndex]
[history, getString, refetchWebhooks, repoMetadata?.path, routes, setPage]
)
return (
<Container className={css.main}>
<RepositoryPageHeader repoMetadata={repoMetadata} title={getString('webhooks')} dataTooltipId="webhooks" />
<PageBody
loading={loading || webhooksLoading}
error={getErrorMessage(error || webhooksError)}
retryOnError={voidFn(refetch)}
noData={{
// TODO: Use NoDataCard, this won't render toolbar
// when search returns empty response
when: () => webhooks?.length === 0,
message: getString('noWebHooks'),
image: emptyStateImage,
button: (
<Button
variation={ButtonVariation.PRIMARY}
text={getString('createWebhook')}
icon={CodeIcon.Add}
onClick={() => history.push(routes.toCODEWebhookNew({ repoPath: repoMetadata?.path as string }))}
/>
)
}}>
<PageBody loading={loading} error={getErrorMessage(error || webhooksError)} retryOnError={voidFn(refetch)}>
{repoMetadata && (
<Layout.Vertical>
<WebhooksHeader repoMetadata={repoMetadata} />
{!!webhooks?.length && (
<Container padding="xlarge">
<TableV2<OpenapiWebhookType>
className={css.table}
hideHeaders
columns={columns}
data={webhooks}
getRowClassName={() => css.row}
onRowClick={row => {
history.push(
routes.toCODEWebhookDetails({
repoPath: repoMetadata.path as string,
webhookId: String(row.id)
})
)
}}
/>
</Container>
)}
<WebhooksHeader
repoMetadata={repoMetadata}
loading={webhooksLoading}
onSearchTermChanged={value => {
setSearchTerm(value)
setPage(1)
}}
/>
<Container padding="xlarge">
{!!webhooks?.length && (
<>
<TableV2<OpenapiWebhookType>
className={css.table}
hideHeaders
columns={columns}
data={webhooks}
getRowClassName={() => css.row}
onRowClick={row => {
history.push(
routes.toCODEWebhookDetails({
repoPath: repoMetadata.path as string,
webhookId: String(row.id)
})
)
}}
/>
<ResourceListingPagination response={response} page={page} setPage={setPage} />
</>
)}
{webhooks?.length === 0 && (
<Container className={css.noData}>
<NoDataCard
image={emptyStateImage}
message={getString('webhookEmpty')}
button={
<Button
variation={ButtonVariation.PRIMARY}
text={getString('createWebhook')}
icon={CodeIcon.Add}
onClick={() => {
history.push(
routes.toCODEWebhookNew({
repoPath: repoMetadata?.path as string
})
)
}}
/>
}
/>
</Container>
)}
</Container>
</Layout.Vertical>
)}
</PageBody>

View File

@ -1,4 +1,6 @@
.main {
padding-bottom: 0 !important;
div[class*='TextInput'] {
margin-bottom: 0 !important;
margin-left: 0 !important;
@ -8,7 +10,14 @@
align-items: center;
}
padding-bottom: 0 !important;
.input {
margin-bottom: 0 !important;
span[data-icon],
span[icon] {
margin-top: 10px !important;
}
}
}
.branchDropdown {

View File

@ -2,6 +2,7 @@
// this is an auto-generated file
declare const styles: {
readonly main: string
readonly input: string
readonly branchDropdown: string
}
export default styles

View File

@ -1,13 +1,20 @@
import { useHistory } from 'react-router-dom'
import React from 'react'
import React, { useState } from 'react'
import { Container, Layout, FlexExpander, ButtonVariation, Button } from '@harness/uicore'
import { useStrings } from 'framework/strings'
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
import { useAppContext } from 'AppContext'
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
import css from './WebhooksHeader.module.scss'
export function WebhooksHeader({ repoMetadata }: Pick<GitInfoProps, 'repoMetadata'>) {
interface WebhooksHeaderProps extends Pick<GitInfoProps, 'repoMetadata'> {
loading?: boolean
onSearchTermChanged: (searchTerm: string) => void
}
export function WebhooksHeader({ repoMetadata, loading, onSearchTermChanged }: WebhooksHeaderProps) {
const history = useHistory()
const [searchTerm, setSearchTerm] = useState('')
const { routes } = useAppContext()
const { getString } = useStrings()
@ -15,6 +22,14 @@ export function WebhooksHeader({ repoMetadata }: Pick<GitInfoProps, 'repoMetadat
<Container className={css.main} padding="xlarge">
<Layout.Horizontal spacing="medium">
<FlexExpander />
<SearchInputWithSpinner
loading={loading}
query={searchTerm}
setQuery={value => {
setSearchTerm(value)
onSearchTermChanged(value)
}}
/>
<Button
variation={ButtonVariation.PRIMARY}
text={getString('createWebhook')}

View File

@ -86,7 +86,7 @@ export const CodeIcon = {
Repo: 'code-repo' as IconName,
Settings: 'code-settings' as IconName,
Webhook: 'code-webhook' as IconName,
InputSpinner: 'steps-spinner' as IconName,
InputSpinner: 'spinner' as IconName,
InputSearch: 'search' as IconName,
Chat: 'code-chat' as IconName
}