From e730eb3d95832fcd61c5168d3bd03c5da80c0136 Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Fri, 19 Sep 2025 12:11:39 -0400 Subject: [PATCH 1/5] Add optional parameters for pagination --- .../client/src/components/DatasetTable.component.tsx | 4 ++++ packages/server/src/entry/resolvers/entry.resolver.ts | 6 ++++-- packages/server/src/entry/services/entry.service.ts | 11 +++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/client/src/components/DatasetTable.component.tsx b/packages/client/src/components/DatasetTable.component.tsx index bea15293..2d1510b3 100644 --- a/packages/client/src/components/DatasetTable.component.tsx +++ b/packages/client/src/components/DatasetTable.component.tsx @@ -22,6 +22,7 @@ export const DatasetTable: React.FC = (props) => { const [deleteEntryMutation] = useDeleteEntryMutation(); const confirmation = useConfirmation(); const [selectedRows, setSelectedRows] = useState([]); + const [paginationModel, setPaginationModel] = useState<{page: number, pageSize: number}>({ page: 0, pageSize: 10 }); const defaultColumns: GridColDef[] = [ { @@ -127,6 +128,9 @@ export const DatasetTable: React.FC = (props) => { getRowHeight={() => 'auto'} rows={entries} columns={columns} + paginationMode={'server'} + paginationModel={paginationModel} + onPaginationModelChange={setPaginationModel} initialState={{ pagination: { paginationModel: { diff --git a/packages/server/src/entry/resolvers/entry.resolver.ts b/packages/server/src/entry/resolvers/entry.resolver.ts index cf2d5816..173e2e0a 100644 --- a/packages/server/src/entry/resolvers/entry.resolver.ts +++ b/packages/server/src/entry/resolvers/entry.resolver.ts @@ -28,13 +28,15 @@ export class EntryResolver { @Query(() => [Entry]) async entryForDataset( @Args('dataset', { type: () => ID }, DatasetPipe) dataset: Dataset, - @TokenContext() user: TokenPayload + @TokenContext() user: TokenPayload, + @Args('page', { type: () => Number, nullable: true }) page?: number, + @Args('pageSize', { type: () => Number, nullable: true }) pageSize?: number ): Promise { if (!(await this.enforcer.enforce(user.user_id, DatasetPermissions.READ, dataset._id.toString()))) { throw new UnauthorizedException('User cannot read entries on this dataset'); } - return this.entryService.findForDataset(dataset); + return this.entryService.findForDataset(dataset, page, pageSize); } @Query(() => Entry) diff --git a/packages/server/src/entry/services/entry.service.ts b/packages/server/src/entry/services/entry.service.ts index 6b3b82ef..96e53821 100644 --- a/packages/server/src/entry/services/entry.service.ts +++ b/packages/server/src/entry/services/entry.service.ts @@ -48,7 +48,7 @@ export class EntryService { await this.entryModel.deleteOne({ _id: entry._id }); } - async findForDataset(dataset: Dataset | string): Promise { + async findForDataset(dataset: Dataset | string, page?: number, pageSize?: number): Promise { let id: string = ''; if (typeof dataset === 'string') { @@ -57,7 +57,14 @@ export class EntryService { id = dataset._id.toString(); } - return this.entryModel.find({ dataset: id, isTraining: false }); + let result = this.entryModel.find({ dataset: id, isTraining: false }); + + if (page && pageSize) { + const offset = page * pageSize; + result = result.skip(offset).limit(pageSize); + } + + return result; } async exists(entryID: string, dataset: Dataset): Promise { From 1fc9f42bcf9d94e5e29a32ba6a24594c0d3f3319 Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Fri, 19 Sep 2025 12:20:56 -0400 Subject: [PATCH 2/5] Working server pagination support --- .../client/src/components/DatasetTable.component.tsx | 4 ++-- packages/client/src/graphql/entry/entry.graphql | 4 ++-- packages/client/src/graphql/entry/entry.ts | 8 ++++++-- packages/client/src/graphql/graphql.ts | 2 ++ packages/server/src/entry/resolvers/entry.resolver.ts | 6 +++--- packages/server/src/entry/services/entry.service.ts | 9 +++++---- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/client/src/components/DatasetTable.component.tsx b/packages/client/src/components/DatasetTable.component.tsx index 2d1510b3..dda81850 100644 --- a/packages/client/src/components/DatasetTable.component.tsx +++ b/packages/client/src/components/DatasetTable.component.tsx @@ -107,10 +107,10 @@ export const DatasetTable: React.FC = (props) => { useEffect(() => { reload(); - }, [props.dataset]); + }, [props.dataset, paginationModel]); const reload = () => { - entryForDataset({ variables: { dataset: props.dataset._id }, fetchPolicy: 'network-only' }); + entryForDataset({ variables: { dataset: props.dataset._id, page: paginationModel.page, pageSize: paginationModel.pageSize }, fetchPolicy: 'network-only' }); }; // TODO: Add in logic to re-fetch data when the presigned URL expires diff --git a/packages/client/src/graphql/entry/entry.graphql b/packages/client/src/graphql/entry/entry.graphql index c92708e4..e2847ca4 100644 --- a/packages/client/src/graphql/entry/entry.graphql +++ b/packages/client/src/graphql/entry/entry.graphql @@ -1,5 +1,5 @@ -query entryForDataset($dataset: ID!) { - entryForDataset(dataset: $dataset) { +query entryForDataset($dataset: ID!, $page: Int, $pageSize: Int) { + entryForDataset(dataset: $dataset, page: $page, pageSize: $pageSize) { _id organization entryID diff --git a/packages/client/src/graphql/entry/entry.ts b/packages/client/src/graphql/entry/entry.ts index 539b3b23..e2bbb0f3 100644 --- a/packages/client/src/graphql/entry/entry.ts +++ b/packages/client/src/graphql/entry/entry.ts @@ -7,6 +7,8 @@ import * as Apollo from '@apollo/client'; const defaultOptions = {} as const; export type EntryForDatasetQueryVariables = Types.Exact<{ dataset: Types.Scalars['ID']['input']; + page?: Types.InputMaybe; + pageSize?: Types.InputMaybe; }>; @@ -28,8 +30,8 @@ export type DeleteEntryMutation = { __typename?: 'Mutation', deleteEntry: boolea export const EntryForDatasetDocument = gql` - query entryForDataset($dataset: ID!) { - entryForDataset(dataset: $dataset) { + query entryForDataset($dataset: ID!, $page: Int, $pageSize: Int) { + entryForDataset(dataset: $dataset, page: $page, pageSize: $pageSize) { _id organization entryID @@ -58,6 +60,8 @@ export const EntryForDatasetDocument = gql` * const { data, loading, error } = useEntryForDatasetQuery({ * variables: { * dataset: // value for 'dataset' + * page: // value for 'page' + * pageSize: // value for 'pageSize' * }, * }); */ diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index aaec96aa..91cbf0bb 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -507,6 +507,8 @@ export type QueryDatasetExistsArgs = { export type QueryEntryForDatasetArgs = { dataset: Scalars['ID']['input']; + page?: InputMaybe; + pageSize?: InputMaybe; }; diff --git a/packages/server/src/entry/resolvers/entry.resolver.ts b/packages/server/src/entry/resolvers/entry.resolver.ts index 173e2e0a..c2ac6624 100644 --- a/packages/server/src/entry/resolvers/entry.resolver.ts +++ b/packages/server/src/entry/resolvers/entry.resolver.ts @@ -1,4 +1,4 @@ -import { Args, ID, Resolver, Query, ResolveField, Parent, Mutation } from '@nestjs/graphql'; +import { Args, ID, Resolver, Query, ResolveField, Parent, Mutation, Int } from '@nestjs/graphql'; import { Dataset } from '../../dataset/dataset.model'; import { Entry } from '../models/entry.model'; import { EntryService } from '../services/entry.service'; @@ -29,8 +29,8 @@ export class EntryResolver { async entryForDataset( @Args('dataset', { type: () => ID }, DatasetPipe) dataset: Dataset, @TokenContext() user: TokenPayload, - @Args('page', { type: () => Number, nullable: true }) page?: number, - @Args('pageSize', { type: () => Number, nullable: true }) pageSize?: number + @Args('page', { type: () => Int, nullable: true }) page?: number, + @Args('pageSize', { type: () => Int, nullable: true }) pageSize?: number ): Promise { if (!(await this.enforcer.enforce(user.user_id, DatasetPermissions.READ, dataset._id.toString()))) { throw new UnauthorizedException('User cannot read entries on this dataset'); diff --git a/packages/server/src/entry/services/entry.service.ts b/packages/server/src/entry/services/entry.service.ts index 96e53821..111e9d7e 100644 --- a/packages/server/src/entry/services/entry.service.ts +++ b/packages/server/src/entry/services/entry.service.ts @@ -57,14 +57,15 @@ export class EntryService { id = dataset._id.toString(); } - let result = this.entryModel.find({ dataset: id, isTraining: false }); + const query = this.entryModel.find({ dataset: id, isTraining: false }); + console.log(pageSize) - if (page && pageSize) { + if (page !== undefined && pageSize !== undefined) { const offset = page * pageSize; - result = result.skip(offset).limit(pageSize); + return await query.skip(offset).limit(pageSize); } - return result; + return query; } async exists(entryID: string, dataset: Dataset): Promise { From f4dcf10d29e01d7dc5a4d37e96a872aa317edb6c Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Fri, 19 Sep 2025 12:31:11 -0400 Subject: [PATCH 3/5] Getting entry count for dataset --- .../src/components/DatasetTable.component.tsx | 15 ++++++- .../client/src/graphql/entry/entry.graphql | 4 ++ packages/client/src/graphql/entry/entry.ts | 40 +++++++++++++++++++ packages/client/src/graphql/graphql.ts | 6 +++ .../src/entry/resolvers/entry.resolver.ts | 12 ++++++ .../src/entry/services/entry.service.ts | 12 ++++++ 6 files changed, 88 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/DatasetTable.component.tsx b/packages/client/src/components/DatasetTable.component.tsx index dda81850..7a6314b2 100644 --- a/packages/client/src/components/DatasetTable.component.tsx +++ b/packages/client/src/components/DatasetTable.component.tsx @@ -1,7 +1,7 @@ import { DataGrid, GridColDef, GridRowId, GridActionsCellItem } from '@mui/x-data-grid'; import { useState, useEffect } from 'react'; import { Dataset, Entry } from '../graphql/graphql'; -import { useEntryForDatasetLazyQuery } from '../graphql/entry/entry'; +import { useEntryForDatasetLazyQuery, useCountEntryForDatasetLazyQuery } from '../graphql/entry/entry'; import { EntryView } from './EntryView.component'; import { useTranslation } from 'react-i18next'; import { useSnackbar } from '../context/Snackbar.context'; @@ -98,12 +98,14 @@ export const DatasetTable: React.FC = (props) => { }; const [entries, setEntries] = useState([]); + const [rowCount, setRowCount] = useState(0); const columns = [...defaultColumns, ...(props.additionalColumns ?? [])]; if (props.supportEntryDelete) { columns.push(deleteColumn); } const [entryForDataset, entryForDatasetResult] = useEntryForDatasetLazyQuery(); + const [entryCount, entryCountResult] = useCountEntryForDatasetLazyQuery(); useEffect(() => { reload(); @@ -111,6 +113,7 @@ export const DatasetTable: React.FC = (props) => { const reload = () => { entryForDataset({ variables: { dataset: props.dataset._id, page: paginationModel.page, pageSize: paginationModel.pageSize }, fetchPolicy: 'network-only' }); + entryCount({ variables: { dataset: props.dataset._id }}); }; // TODO: Add in logic to re-fetch data when the presigned URL expires @@ -123,10 +126,20 @@ export const DatasetTable: React.FC = (props) => { } }, [entryForDatasetResult]); + useEffect(() => { + if (entryCountResult.data) { + setRowCount(entryCountResult.data.countEntryForDataset); + } else if (entryCountResult.error) { + pushSnackbarMessage(t('errors.entryQuery'), 'error'); + console.error(entryForDatasetResult.error); + } + }, [entryCountResult]); + return ( 'auto'} rows={entries} + rowCount={rowCount} columns={columns} paginationMode={'server'} paginationModel={paginationModel} diff --git a/packages/client/src/graphql/entry/entry.graphql b/packages/client/src/graphql/entry/entry.graphql index e2847ca4..8ccdce03 100644 --- a/packages/client/src/graphql/entry/entry.graphql +++ b/packages/client/src/graphql/entry/entry.graphql @@ -14,6 +14,10 @@ query entryForDataset($dataset: ID!, $page: Int, $pageSize: Int) { } } + query countEntryForDataset($dataset: ID!) { + countEntryForDataset(dataset: $dataset) + } + query entryFromID($entry: ID!) { entryFromID(entry: $entry) { _id diff --git a/packages/client/src/graphql/entry/entry.ts b/packages/client/src/graphql/entry/entry.ts index e2bbb0f3..b77cd1a5 100644 --- a/packages/client/src/graphql/entry/entry.ts +++ b/packages/client/src/graphql/entry/entry.ts @@ -14,6 +14,13 @@ export type EntryForDatasetQueryVariables = Types.Exact<{ export type EntryForDatasetQuery = { __typename?: 'Query', entryForDataset: Array<{ __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number, isTraining: boolean }> }; +export type CountEntryForDatasetQueryVariables = Types.Exact<{ + dataset: Types.Scalars['ID']['input']; +}>; + + +export type CountEntryForDatasetQuery = { __typename?: 'Query', countEntryForDataset: number }; + export type EntryFromIdQueryVariables = Types.Exact<{ entry: Types.Scalars['ID']['input']; }>; @@ -76,6 +83,39 @@ export function useEntryForDatasetLazyQuery(baseOptions?: Apollo.LazyQueryHookOp export type EntryForDatasetQueryHookResult = ReturnType; export type EntryForDatasetLazyQueryHookResult = ReturnType; export type EntryForDatasetQueryResult = Apollo.QueryResult; +export const CountEntryForDatasetDocument = gql` + query countEntryForDataset($dataset: ID!) { + countEntryForDataset(dataset: $dataset) +} + `; + +/** + * __useCountEntryForDatasetQuery__ + * + * To run a query within a React component, call `useCountEntryForDatasetQuery` and pass it any options that fit your needs. + * When your component renders, `useCountEntryForDatasetQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useCountEntryForDatasetQuery({ + * variables: { + * dataset: // value for 'dataset' + * }, + * }); + */ +export function useCountEntryForDatasetQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(CountEntryForDatasetDocument, options); + } +export function useCountEntryForDatasetLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(CountEntryForDatasetDocument, options); + } +export type CountEntryForDatasetQueryHookResult = ReturnType; +export type CountEntryForDatasetLazyQueryHookResult = ReturnType; +export type CountEntryForDatasetQueryResult = Apollo.QueryResult; export const EntryFromIdDocument = gql` query entryFromID($entry: ID!) { entryFromID(entry: $entry) { diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 91cbf0bb..4ed9648e 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -468,6 +468,7 @@ export type ProjectPermissionModel = { export type Query = { __typename?: 'Query'; + countEntryForDataset: Scalars['Int']['output']; datasetExists: Scalars['Boolean']['output']; entryForDataset: Array; entryFromID: Entry; @@ -500,6 +501,11 @@ export type Query = { }; +export type QueryCountEntryForDatasetArgs = { + dataset: Scalars['ID']['input']; +}; + + export type QueryDatasetExistsArgs = { name: Scalars['String']['input']; }; diff --git a/packages/server/src/entry/resolvers/entry.resolver.ts b/packages/server/src/entry/resolvers/entry.resolver.ts index c2ac6624..23a7fed7 100644 --- a/packages/server/src/entry/resolvers/entry.resolver.ts +++ b/packages/server/src/entry/resolvers/entry.resolver.ts @@ -39,6 +39,18 @@ export class EntryResolver { return this.entryService.findForDataset(dataset, page, pageSize); } + @Query(() => Int) + async countEntryForDataset( + @Args('dataset', { type: () => ID }, DatasetPipe) dataset: Dataset, + @TokenContext() user: TokenPayload, + ): Promise { + if (!(await this.enforcer.enforce(user.user_id, DatasetPermissions.READ, dataset._id.toString()))) { + throw new UnauthorizedException('User cannot read entries on this dataset'); + } + + return this.entryService.countForDataset(dataset); + } + @Query(() => Entry) async entryFromID( @Args('entry', { type: () => ID }, EntryPipe) entry: Entry, diff --git a/packages/server/src/entry/services/entry.service.ts b/packages/server/src/entry/services/entry.service.ts index 111e9d7e..749da2ea 100644 --- a/packages/server/src/entry/services/entry.service.ts +++ b/packages/server/src/entry/services/entry.service.ts @@ -68,6 +68,18 @@ export class EntryService { return query; } + async countForDataset(dataset: Dataset | string) { + let id: string = ''; + + if (typeof dataset === 'string') { + id = dataset; + } else { + id = dataset._id.toString(); + } + + return this.entryModel.count({ dataset: id, isTraining: false }); + } + async exists(entryID: string, dataset: Dataset): Promise { const entry = await this.entryModel.findOne({ entryID, dataset: dataset._id }); return !!entry; From 06691af1bf8676a65528c747f052a70a10101c11 Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Fri, 19 Sep 2025 12:34:14 -0400 Subject: [PATCH 4/5] Fix formatting --- .../client/src/components/DatasetTable.component.tsx | 9 ++++++--- packages/server/src/entry/resolvers/entry.resolver.ts | 2 +- packages/server/src/entry/services/entry.service.ts | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/DatasetTable.component.tsx b/packages/client/src/components/DatasetTable.component.tsx index 7a6314b2..f795c7e1 100644 --- a/packages/client/src/components/DatasetTable.component.tsx +++ b/packages/client/src/components/DatasetTable.component.tsx @@ -22,7 +22,7 @@ export const DatasetTable: React.FC = (props) => { const [deleteEntryMutation] = useDeleteEntryMutation(); const confirmation = useConfirmation(); const [selectedRows, setSelectedRows] = useState([]); - const [paginationModel, setPaginationModel] = useState<{page: number, pageSize: number}>({ page: 0, pageSize: 10 }); + const [paginationModel, setPaginationModel] = useState<{ page: number; pageSize: number }>({ page: 0, pageSize: 10 }); const defaultColumns: GridColDef[] = [ { @@ -112,8 +112,11 @@ export const DatasetTable: React.FC = (props) => { }, [props.dataset, paginationModel]); const reload = () => { - entryForDataset({ variables: { dataset: props.dataset._id, page: paginationModel.page, pageSize: paginationModel.pageSize }, fetchPolicy: 'network-only' }); - entryCount({ variables: { dataset: props.dataset._id }}); + entryForDataset({ + variables: { dataset: props.dataset._id, page: paginationModel.page, pageSize: paginationModel.pageSize }, + fetchPolicy: 'network-only' + }); + entryCount({ variables: { dataset: props.dataset._id } }); }; // TODO: Add in logic to re-fetch data when the presigned URL expires diff --git a/packages/server/src/entry/resolvers/entry.resolver.ts b/packages/server/src/entry/resolvers/entry.resolver.ts index 23a7fed7..0089ab1f 100644 --- a/packages/server/src/entry/resolvers/entry.resolver.ts +++ b/packages/server/src/entry/resolvers/entry.resolver.ts @@ -42,7 +42,7 @@ export class EntryResolver { @Query(() => Int) async countEntryForDataset( @Args('dataset', { type: () => ID }, DatasetPipe) dataset: Dataset, - @TokenContext() user: TokenPayload, + @TokenContext() user: TokenPayload ): Promise { if (!(await this.enforcer.enforce(user.user_id, DatasetPermissions.READ, dataset._id.toString()))) { throw new UnauthorizedException('User cannot read entries on this dataset'); diff --git a/packages/server/src/entry/services/entry.service.ts b/packages/server/src/entry/services/entry.service.ts index 749da2ea..e607bdac 100644 --- a/packages/server/src/entry/services/entry.service.ts +++ b/packages/server/src/entry/services/entry.service.ts @@ -58,7 +58,7 @@ export class EntryService { } const query = this.entryModel.find({ dataset: id, isTraining: false }); - console.log(pageSize) + console.log(pageSize); if (page !== undefined && pageSize !== undefined) { const offset = page * pageSize; From 05637460e2a91069fcc9d398dd4ac6d1c352dde7 Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Fri, 19 Sep 2025 12:34:51 -0400 Subject: [PATCH 5/5] Remove console log --- packages/server/src/entry/services/entry.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/entry/services/entry.service.ts b/packages/server/src/entry/services/entry.service.ts index e607bdac..113e1bad 100644 --- a/packages/server/src/entry/services/entry.service.ts +++ b/packages/server/src/entry/services/entry.service.ts @@ -58,7 +58,6 @@ export class EntryService { } const query = this.entryModel.find({ dataset: id, isTraining: false }); - console.log(pageSize); if (page !== undefined && pageSize !== undefined) { const offset = page * pageSize;