Skip to content

Commit

Permalink
feat: Refactor <TablePageLayout> to support multiple tabs (#911)
Browse files Browse the repository at this point in the history
This is to support having both Annotations and Tomograms tabs on the
Runs page.
 - Adds support for a header above the tabs.
 - Uses our existing `<Tabs>` component.
   - Only shows tabs if more than 1 is specified.
 - Supports a different pagination query param per tab.
- This changes the query param on the Runs page from `page` to
`annotations-page`. In a subsequent PR I'll add `tomograms-page`.
- It seems like we're currently pretty far from either annotations or
tomograms getting to a point where they'd actually trigger pagination,
but I'd prefer this to work properly from the get-go rather than find
out there's a bug later.
 - Stores the currently open tab in URL params.
 - Make a few prop names clearer.
- Allow `<Tabs>` to take in components as labels and `number`s (i.e.
indexes) as values.
 - Also enable hoisting in ESLint (which TS checks anyways).
  • Loading branch information
bchu1 authored Jul 25, 2024
1 parent 7c7a578 commit 659bea3
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 150 deletions.
235 changes: 146 additions & 89 deletions frontend/packages/data-portal/app/components/TablePageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,121 @@ import { ReactNode, useEffect, useMemo } from 'react'

import { TABLE_PAGE_LAYOUT_LOG_ID } from 'app/constants/error'
import { MAX_PER_PAGE } from 'app/constants/pagination'
import { QueryParams } from 'app/constants/query'
import { TestIds } from 'app/constants/testIds'
import { LayoutContext, LayoutContextValue } from 'app/context/Layout.context'
import { cns } from 'app/utils/cns'

import { ErrorBoundary } from './ErrorBoundary'
import { TableCount } from './Table/TableCount'
import { Tabs } from './Tabs'

export interface TablePageLayoutProps {
header?: ReactNode

tabs: TableLayoutTab[] // If there is only 1 tab, the tab selector will not show.
tabsTitle?: string

downloadModal?: ReactNode
drawers?: ReactNode
}

export interface TableLayoutTab {
title: string

filterPanel?: ReactNode

table: ReactNode
noResults?: ReactNode
pageQueryParamKey?: string

filteredCount: number
totalCount: number
countLabel: string // e.g. "objects" in "1 of 3 objects".
}

/** Standard page structure for browsing + filtering list(s) of objects. */
export function TablePageLayout({
header,
tabs,
tabsTitle,
downloadModal,
drawers,
}: TablePageLayoutProps) {
const [searchParams, setSearchParams] = useSearchParams()

const activeTabTitle = searchParams.get(QueryParams.TableTab)
const activeTab = tabs.find((tab) => tab.title === activeTabTitle) ?? tabs[0]

return (
<>
{downloadModal}

<div className="flex flex-col flex-auto">
{header}

{tabs.length > 1 && (
<>
{tabsTitle && <div className="text-sds-header-l">{tabsTitle}</div>}
<Tabs
value={activeTab.title}
onChange={(tabTitle: string) => {
setSearchParams((prev) => {
prev.set(QueryParams.TableTab, tabTitle)
return prev
})
}}
tabs={tabs.map((tab) => ({
label: (
<>
<span>{tab.title}</span>
<span className="text-sds-gray-500 ml-[24px]">
{tab.filteredCount}
</span>
</>
),
value: tab.title,
}))}
/>
</>
)}
<TablePageTabContent {...activeTab} />

{drawers}
</div>
</>
)
}

/** Table + filters for 1 tab. */
function TablePageTabContent({
title,
filterPanel,
filteredCount,
filters: filterPanel,
header,
noResults,
table,
title,
noResults,
pageQueryParamKey = QueryParams.Page,
totalCount,
type,
}: {
downloadModal?: ReactNode
drawers?: ReactNode
filteredCount: number
filters?: ReactNode
header?: ReactNode
noResults?: ReactNode
table: ReactNode
title: string
totalCount: number
type: string
}) {
countLabel,
}: TableLayoutTab) {
const [searchParams, setSearchParams] = useSearchParams()
const page = +(searchParams.get('page') ?? '1')
const pageQueryParamValue = +(searchParams.get(pageQueryParamKey) ?? '1')

useEffect(() => {
if (Math.ceil(filteredCount / MAX_PER_PAGE) < page) {
if (Math.ceil(filteredCount / MAX_PER_PAGE) < pageQueryParamValue) {
setSearchParams(
(prev) => {
prev.delete('page')
prev.delete(pageQueryParamKey)
return prev
},
{ replace: true },
)
}
}, [filteredCount, page, setSearchParams])
}, [filteredCount, pageQueryParamKey, pageQueryParamValue, setSearchParams])

function setPage(nextPage: number) {
setSearchParams((prev) => {
prev.set('page', `${nextPage}`)
prev.set(pageQueryParamKey, `${nextPage}`)
return prev
})
}
Expand All @@ -65,83 +132,73 @@ export function TablePageLayout({

return (
<LayoutContext.Provider value={contextValue}>
{downloadModal}

<div className="flex flex-col flex-auto">
{header}

<div className="flex flex-auto">
{filterPanel && (
<div
className={cns(
'flex flex-col flex-shrink-0 w-[235px]',
'border-t border-r border-sds-gray-300',
)}
>
{filterPanel}
</div>
<div className="flex flex-auto">
{filterPanel && (
<div
className={cns(
'flex flex-col flex-shrink-0 w-[235px]',
'border-t border-r border-sds-gray-300',
)}
>
{filterPanel}
</div>
)}

<div
className={cns(
'flex flex-col flex-auto screen-2040:items-center',
'pt-sds-xl pb-sds-xxl',
'border-t border-sds-gray-300',
'overflow-x-scroll max-w-full',
)}

>
<div
className={cns(
'flex flex-col flex-auto screen-2040:items-center',
'pt-sds-xl pb-sds-xxl',
'border-t border-sds-gray-300',
'overflow-x-scroll max-w-full',
'flex flex-col flex-auto w-full',

// Translate to the left by half the filter panel width to align with the header
filterPanel && 'screen-2040:translate-x-[-100px] max-w-content',
)}
>
<div
className={cns(
'flex flex-col flex-auto w-full',
<div className="px-sds-xl flex items-center gap-x-sds-xl">
<p className="text-sds-header-l leading-sds-header-l font-semibold">
{title}
</p>

<TableCount
filteredCount={filteredCount}
totalCount={totalCount}
type={countLabel}
/>
</div>

// Translate to the left by half the filter panel width to align with the header
filterPanel && 'screen-2040:translate-x-[-100px] max-w-content',
<ErrorBoundary logId={TABLE_PAGE_LAYOUT_LOG_ID}>
<div className="overflow-x-scroll">{table}</div>
</ErrorBoundary>

<div className="px-sds-xl">
{filteredCount === 0 && noResults}

{filteredCount > MAX_PER_PAGE && (
<div
className="w-full flex justify-center mt-sds-xxl"
data-testid={TestIds.Pagination}
>
<Pagination
currentPage={pageQueryParamValue}
pageSize={MAX_PER_PAGE}
totalCount={
totalCount === filteredCount ? totalCount : filteredCount
}
onNextPage={() => setPage(pageQueryParamValue + 1)}
onPreviousPage={() => setPage(pageQueryParamValue - 1)}
onPageChange={(nextPage) => setPage(nextPage)}
/>
</div>
)}
>
<div className="px-sds-xl flex items-center gap-x-sds-xl">
<p className="text-sds-header-l leading-sds-header-l font-semibold">
{title}
</p>

<TableCount
filteredCount={filteredCount}
totalCount={totalCount}
type={type}
/>
</div>

<ErrorBoundary logId={TABLE_PAGE_LAYOUT_LOG_ID}>
<div className="overflow-x-scroll">{table}</div>
</ErrorBoundary>

<div className="px-sds-xl">
{filteredCount === 0 && noResults}

{filteredCount > MAX_PER_PAGE && (
<div
className="w-full flex justify-center mt-sds-xxl"
data-testid={TestIds.Pagination}
>
<Pagination
currentPage={page}
pageSize={MAX_PER_PAGE}
totalCount={
totalCount === filteredCount
? totalCount
: filteredCount
}
onNextPage={() => setPage(page + 1)}
onPreviousPage={() => setPage(page - 1)}
onPageChange={(nextPage) => setPage(nextPage)}
/>
</div>
)}
</div>
</div>
</div>
</div>

{drawers}
</div>
</LayoutContext.Provider>
)
Expand Down
9 changes: 5 additions & 4 deletions frontend/packages/data-portal/app/components/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import Tab from '@mui/material/Tab'
import MUITabs, { TabsProps } from '@mui/material/Tabs'
import { ReactNode } from 'react'

import { cns } from 'app/utils/cns'

export interface TabData<T extends string> {
label: string
export interface TabData<T> {
label: ReactNode
value: T
}

export function Tabs<T extends string>({
export function Tabs<T>({
tabs,
value,
onChange,
Expand Down Expand Up @@ -43,7 +44,7 @@ export function Tabs<T extends string>({
),
selected: '!text-black',
}}
key={tab.value}
key={String(tab.value)}
{...tab}
/>
))}
Expand Down
2 changes: 2 additions & 0 deletions frontend/packages/data-portal/app/constants/query.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum QueryParams {
AnnotationId = 'annotation_id',
AnnotationsPage = 'annotations-page',
AnnotationSoftware = 'annotation-software',
AuthorName = 'author',
AuthorOrcid = 'author_orcid',
Expand All @@ -26,6 +27,7 @@ export enum QueryParams {
ReconstructionMethod = 'reconstruction_method',
ReconstructionSoftware = 'reconstruction_software',
Tab = 'tab',
TableTab = 'table-tab',
TiltRangeMax = 'tilt_max',
TiltRangeMin = 'tilt_min',
TomogramProcessing = 'tomogram-processing',
Expand Down
10 changes: 5 additions & 5 deletions frontend/packages/data-portal/app/graphql/getRunById.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { MAX_PER_PAGE } from 'app/constants/pagination'
import { FilterState, getFilterState } from 'app/hooks/useFilter'

const GET_RUN_BY_ID_QUERY = gql(`
query GetRunById($id: Int, $limit: Int, $offset: Int, $filter: annotations_bool_exp, $fileFilter: annotation_files_bool_exp) {
query GetRunById($id: Int, $limit: Int, $annotationsOffset: Int, $filter: annotations_bool_exp, $fileFilter: annotation_files_bool_exp) {
runs(where: { id: { _eq: $id } }) {
id
name
Expand Down Expand Up @@ -128,7 +128,7 @@ const GET_RUN_BY_ID_QUERY = gql(`
annotation_table: tomogram_voxel_spacings {
annotations(
limit: $limit,
offset: $offset,
offset: $annotationsOffset,
where: $filter,
order_by: [
{ ground_truth_status: desc }
Expand Down Expand Up @@ -343,20 +343,20 @@ function getFileFilter(filterState: FilterState) {
export async function getRunById({
client,
id,
page = 1,
annotationsPage,
params = new URLSearchParams(),
}: {
client: ApolloClient<NormalizedCacheObject>
id: number
page?: number
annotationsPage: number
params?: URLSearchParams
}): Promise<ApolloQueryResult<GetRunByIdQuery>> {
return client.query({
query: GET_RUN_BY_ID_QUERY,
variables: {
id,
limit: MAX_PER_PAGE,
offset: (page - 1) * MAX_PER_PAGE,
annotationsOffset: (annotationsPage - 1) * MAX_PER_PAGE,
filter: getFilter(getFilterState(params)),
fileFilter: getFileFilter(getFilterState(params)),
},
Expand Down
Loading

0 comments on commit 659bea3

Please sign in to comment.