diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 8e79c161e..f14bd8877 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -1,7 +1,9 @@ name: Release Build on: push: - branches: main + branches: + - main + - develop tags: '*' jobs: @@ -44,6 +46,9 @@ jobs: if [ "${{ github.ref }}" = "refs/heads/main" ] then echo TAG_NAME=snapshot >> $GITHUB_ENV + elif [ "${{ github.ref }}" = "refs/heads/develop" ] + then + echo TAG_NAME=snapshot-develop >> $GITHUB_ENV else echo TAG_NAME=`basename ${{ github.ref }}` >> $GITHUB_ENV fi @@ -78,7 +83,7 @@ jobs: tag_name: ${{ env.TAG_NAME }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }} - name: Create/update release uses: johnwbyrd/update-release@v1.0.0 @@ -87,5 +92,5 @@ jobs: files: ./datagateway-dataview-${{ env.TAG_NAME }}.tar.gz ./datagateway-download-${{ env.TAG_NAME }}.tar.gz ./datagateway-search-${{ env.TAG_NAME }}.tar.gz release: Release ${{ env.TAG_NAME }} tag: ${{ env.TAG_NAME }} - prerelease: ${{ github.ref == 'refs/heads/main' }} + prerelease: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }} draft: false diff --git a/.gitignore b/.gitignore index c9f50b263..678ed6de8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -**/public/settings.json -**/public/datagateway-dataview-settings.json -**/public/datagateway-download-settings.json -**/public/datagateway-search-settings.json +**/public/*settings*.json +!**/public/*settings.example.json + diff --git a/CHANGELOG-BASE.md b/CHANGELOG-BASE.md new file mode 100644 index 000000000..2f1e9e7d0 --- /dev/null +++ b/CHANGELOG-BASE.md @@ -0,0 +1,5 @@ +# Changelog + +## [v1.0.0](https://github.com/ral-facilities/datagateway/tree/v1.0.0) (2022-03-31) + +Initial release for ISIS diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f1e9e7d0..c7cd836fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## [v1.1.0](https://github.com/ral-facilities/datagateway/tree/v1.1.0) (2022-06-21) + +[Full Changelog](https://github.com/ral-facilities/datagateway/compare/v1.0.0...v1.1.0) + +**Implemented enhancements:** + +- \#1174 - changed Title icon to Subject icon [\#1259](https://github.com/ral-facilities/datagateway/pull/1259) ([LunaBarrett](https://github.com/LunaBarrett)) +- Customising notification message if it concerns session expiry \#1209 [\#1248](https://github.com/ral-facilities/datagateway/pull/1248) ([sam-glendenning](https://github.com/sam-glendenning)) +- Adding alert label if file/size limit exceeded in selections table \#1183 [\#1228](https://github.com/ral-facilities/datagateway/pull/1228) ([sam-glendenning](https://github.com/sam-glendenning)) +- Download tables sort by requested time descending by default \#1176 [\#1226](https://github.com/ral-facilities/datagateway/pull/1226) ([sam-glendenning](https://github.com/sam-glendenning)) +- Adding ability to use DatePicker or DateTimePicker in date filter column \#1175 [\#1216](https://github.com/ral-facilities/datagateway/pull/1216) ([sam-glendenning](https://github.com/sam-glendenning)) +- Refactor download cart to improve performance [\#1185](https://github.com/ral-facilities/datagateway/pull/1185) ([louise-davies](https://github.com/louise-davies)) + +**Fixed bugs:** + +- Errors when multi selecting items for the cart [\#1210](https://github.com/ral-facilities/datagateway/issues/1210) +- Bugfix/disable download button \#1261 [\#1266](https://github.com/ral-facilities/datagateway/pull/1266) ([louise-davies](https://github.com/louise-davies)) +- Bugfix/fix token refresh error \#1257 [\#1262](https://github.com/ral-facilities/datagateway/pull/1262) ([louise-davies](https://github.com/louise-davies)) +- \#1196 - Use MUI theme background colour for breadcrumb gaps [\#1253](https://github.com/ral-facilities/datagateway/pull/1253) ([louise-davies](https://github.com/louise-davies)) +- Fix breadcrumbs visual glitch [\#1252](https://github.com/ral-facilities/datagateway/pull/1252) ([louise-davies](https://github.com/louise-davies)) +- Prevent querying for cart on homepage & fix admin download table queries [\#1250](https://github.com/ral-facilities/datagateway/pull/1250) ([louise-davies](https://github.com/louise-davies)) +- Preventing download of empty files \#1195 [\#1232](https://github.com/ral-facilities/datagateway/pull/1232) ([sam-glendenning](https://github.com/sam-glendenning)) +- Fix selection errors \#1210 [\#1231](https://github.com/ral-facilities/datagateway/pull/1231) ([louise-davies](https://github.com/louise-davies)) + +**Security fixes:** + +- Bump async from 2.6.3 to 2.6.4 [\#1274](https://github.com/ral-facilities/datagateway/pull/1274) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump minimist from 1.2.5 to 1.2.6 [\#1197](https://github.com/ral-facilities/datagateway/pull/1197) ([dependabot[bot]](https://github.com/apps/dependabot)) + +**Merged pull requests:** + +- Merge main into develop [\#1288](https://github.com/ral-facilities/datagateway/pull/1288) ([louise-davies](https://github.com/louise-davies)) +- Bump ejs from 3.1.6 to 3.1.7 [\#1245](https://github.com/ral-facilities/datagateway/pull/1245) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump node-forge from 1.2.1 to 1.3.0 [\#1179](https://github.com/ral-facilities/datagateway/pull/1179) ([dependabot[bot]](https://github.com/apps/dependabot)) + ## [v1.0.0](https://github.com/ral-facilities/datagateway/tree/v1.0.0) (2022-03-31) Initial release for ISIS + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/lerna.json b/lerna.json index 427ac247d..152423978 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,8 @@ { - "packages": ["packages/*"], - "version": "0.0.0", + "packages": [ + "packages/*" + ], + "version": "1.1.0", "npmClient": "yarn", "useWorkspaces": true } diff --git a/package.json b/package.json index 0ac61f06d..a0e9bafc3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "datagateway", "private": true, - "version": "1.0.0", + "version": "1.1.0", "workspaces": [ "packages/*" ], diff --git a/packages/datagateway-common/package.json b/packages/datagateway-common/package.json index 0ec07bcb5..91aa9f7c1 100644 --- a/packages/datagateway-common/package.json +++ b/packages/datagateway-common/package.json @@ -1,6 +1,6 @@ { "name": "datagateway-common", - "version": "1.0.0", + "version": "1.1.0", "private": true, "files": [ "lib" @@ -115,4 +115,4 @@ ], "resetMocks": false } -} \ No newline at end of file +} diff --git a/packages/datagateway-common/src/api/cart.test.tsx b/packages/datagateway-common/src/api/cart.test.tsx index a09eb0796..2178a1591 100644 --- a/packages/datagateway-common/src/api/cart.test.tsx +++ b/packages/datagateway-common/src/api/cart.test.tsx @@ -119,10 +119,15 @@ describe('Cart api functions', () => { expect(result.current.data).toEqual(mockData.cartItems); }); - it('sends axios request to add item to cart once mutate function is called and calls handleICATError on failure', async () => { - (axios.post as jest.Mock).mockRejectedValue({ - message: 'Test error message', - }); + it('sends axios request to add item to cart once mutate function is called and calls handleICATError on failure, with a retry on code 431', async () => { + (axios.post as jest.MockedFunction) + .mockRejectedValueOnce({ + code: '431', + message: 'Test 431 error message', + }) + .mockRejectedValue({ + message: 'Test error message', + }); const { result, waitFor } = renderHook(() => useAddToCart('dataset'), { wrapper: createReactQueryWrapper(), @@ -133,8 +138,10 @@ describe('Cart api functions', () => { result.current.mutate([1, 2]); - await waitFor(() => result.current.isError); + await waitFor(() => result.current.isError, { timeout: 2000 }); + expect(result.current.failureCount).toBe(2); + expect(handleICATError).toHaveBeenCalledTimes(1); expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error message', }); @@ -143,7 +150,7 @@ describe('Cart api functions', () => { describe('useRemoveFromCart', () => { it('sends axios request to remove item from cart once mutate function is called and returns successful response', async () => { - (axios.delete as jest.Mock).mockResolvedValue({ + (axios.post as jest.Mock).mockResolvedValue({ data: mockData, }); @@ -161,22 +168,28 @@ describe('Cart api functions', () => { await waitFor(() => result.current.isSuccess); - expect(axios.delete).toHaveBeenCalledWith( + const params = new URLSearchParams(); + params.append('sessionId', ''); + params.append('items', 'dataset 1, dataset 2'); + params.append('remove', 'true'); + + expect(axios.post).toHaveBeenCalledWith( 'https://example.com/topcat/user/cart/TEST/cartItems', - { - params: { - sessionId: null, - items: 'dataset 1, dataset 2', - }, - } + + params ); expect(result.current.data).toEqual(mockData.cartItems); }); - it('sends axios request to remove item from cart once mutate function is called and calls handleICATError on failure', async () => { - (axios.delete as jest.Mock).mockRejectedValue({ - message: 'Test error message', - }); + it('sends axios request to remove item from cart once mutate function is called and calls handleICATError on failure, with a retry on code 431', async () => { + (axios.post as jest.MockedFunction) + .mockRejectedValueOnce({ + code: '431', + message: 'Test 431 error message', + }) + .mockRejectedValue({ + message: 'Test error message', + }); const { result, waitFor } = renderHook( () => useRemoveFromCart('dataset'), @@ -185,13 +198,15 @@ describe('Cart api functions', () => { } ); - expect(axios.delete).not.toHaveBeenCalled(); + expect(axios.post).not.toHaveBeenCalled(); expect(result.current.isIdle).toBe(true); result.current.mutate([1, 2]); - await waitFor(() => result.current.isError); + await waitFor(() => result.current.isError, { timeout: 2000 }); + expect(result.current.failureCount).toBe(2); + expect(handleICATError).toHaveBeenCalledTimes(1); expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error message', }); diff --git a/packages/datagateway-common/src/api/cart.tsx b/packages/datagateway-common/src/api/cart.tsx index f2c6a8eb0..2a01942f1 100644 --- a/packages/datagateway-common/src/api/cart.tsx +++ b/packages/datagateway-common/src/api/cart.tsx @@ -11,8 +11,9 @@ import { useMutation, UseMutationResult, } from 'react-query'; +import retryICATErrors from './retryICATErrors'; -const fetchDownloadCart = (config: { +export const fetchDownloadCart = (config: { facilityName: string; downloadApiUrl: string; }): Promise => { @@ -27,15 +28,19 @@ const fetchDownloadCart = (config: { .then((response) => response.data.cartItems); }; -const addToCart = ( +const addOrRemoveFromCart = ( entityType: 'investigation' | 'dataset' | 'datafile', entityIds: number[], - config: { facilityName: string; downloadApiUrl: string } + config: { facilityName: string; downloadApiUrl: string }, + remove?: boolean ): Promise => { const { facilityName, downloadApiUrl } = config; const params = new URLSearchParams(); params.append('sessionId', readSciGatewayToken().sessionId || ''); params.append('items', `${entityType} ${entityIds.join(`, ${entityType} `)}`); + if (typeof remove !== 'undefined') { + params.append('remove', remove.toString()); + } return axios .post( @@ -45,26 +50,6 @@ const addToCart = ( .then((response) => response.data.cartItems); }; -const removeFromCart = ( - entityType: 'investigation' | 'dataset' | 'datafile', - entityIds: number[], - config: { facilityName: string; downloadApiUrl: string } -): Promise => { - const { facilityName, downloadApiUrl } = config; - - return axios - .delete( - `${downloadApiUrl}/user/cart/${facilityName}/cartItems`, - { - params: { - sessionId: readSciGatewayToken().sessionId, - items: `${entityType} ${entityIds.join(`, ${entityType} `)}`, - }, - } - ) - .then((response) => response.data.cartItems); -}; - export const useCart = (): UseQueryResult => { const downloadApiUrl = useSelector( (state: StateType) => state.dgcommon.urls.downloadApiUrl @@ -86,6 +71,7 @@ export const useCart = (): UseQueryResult => { onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, staleTime: 0, } ); @@ -104,7 +90,7 @@ export const useAddToCart = ( return useMutation( (entityIds: number[]) => - addToCart(entityType, entityIds, { + addOrRemoveFromCart(entityType, entityIds, { facilityName, downloadApiUrl, }), @@ -112,6 +98,14 @@ export const useAddToCart = ( onSuccess: (data) => { queryClient.setQueryData('cart', data); }, + retry: (failureCount, error) => { + // if we get 431 we know this is an intermittent error so retry + if (error.code === '431' && failureCount < 3) { + return true; + } else { + return false; + } + }, onError: (error) => { handleICATError(error); }, @@ -132,14 +126,27 @@ export const useRemoveFromCart = ( return useMutation( (entityIds: number[]) => - removeFromCart(entityType, entityIds, { - facilityName, - downloadApiUrl, - }), + addOrRemoveFromCart( + entityType, + entityIds, + { + facilityName, + downloadApiUrl, + }, + true + ), { onSuccess: (data) => { queryClient.setQueryData('cart', data); }, + retry: (failureCount, error) => { + // if we get 431 we know this is an intermittent error so retry + if (error.code === '431' && failureCount < 3) { + return true; + } else { + return false; + } + }, onError: (error) => { handleICATError(error); }, diff --git a/packages/datagateway-common/src/api/datafiles.tsx b/packages/datagateway-common/src/api/datafiles.tsx index f53ab4dc2..25afebc76 100644 --- a/packages/datagateway-common/src/api/datafiles.tsx +++ b/packages/datagateway-common/src/api/datafiles.tsx @@ -18,6 +18,7 @@ import { useInfiniteQuery, UseInfiniteQueryResult, } from 'react-query'; +import retryICATErrors from './retryICATErrors'; const fetchDatafiles = ( apiUrl: string, @@ -94,6 +95,7 @@ export const useDatafilesPaginated = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -126,6 +128,7 @@ export const useDatafilesInfinite = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -182,6 +185,7 @@ export const useDatafileCount = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -229,6 +233,7 @@ export const useDatafileDetails = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; diff --git a/packages/datagateway-common/src/api/datasets.tsx b/packages/datagateway-common/src/api/datasets.tsx index cdd3b172d..08c274f01 100644 --- a/packages/datagateway-common/src/api/datasets.tsx +++ b/packages/datagateway-common/src/api/datasets.tsx @@ -24,6 +24,7 @@ import { } from 'react-query'; import useDeepCompareEffect from 'use-deep-compare-effect'; import { fetchDatafileCountQuery } from './datafiles'; +import retryICATErrors from './retryICATErrors'; const fetchDatasets = ( apiUrl: string, @@ -88,6 +89,7 @@ export const useDataset = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -132,6 +134,7 @@ export const useDatasetsPaginated = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -164,6 +167,7 @@ export const useDatasetsInfinite = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -214,6 +218,8 @@ export const useDatasetSize = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, + enabled: false, } ); @@ -252,6 +258,7 @@ export const useDatasetSizes = ( onError: (error) => { handleICATError(error, false); }, + retry: retryICATErrors, staleTime: Infinity, }; }); @@ -334,6 +341,7 @@ export const useDatasetsDatafileCount = ( onError: (error) => { handleICATError(error, false); }, + retry: retryICATErrors, staleTime: Infinity, }; }); @@ -432,6 +440,7 @@ export const useDatasetCount = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -466,6 +475,7 @@ export const useDatasetDetails = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; diff --git a/packages/datagateway-common/src/api/facilityCycles.tsx b/packages/datagateway-common/src/api/facilityCycles.tsx index 8eace79ad..750a4e5c4 100644 --- a/packages/datagateway-common/src/api/facilityCycles.tsx +++ b/packages/datagateway-common/src/api/facilityCycles.tsx @@ -13,6 +13,7 @@ import { useInfiniteQuery, UseInfiniteQueryResult, } from 'react-query'; +import retryICATErrors from './retryICATErrors'; const fetchFacilityCycles = ( apiUrl: string, @@ -69,6 +70,7 @@ export const useAllFacilityCycles = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, enabled, } ); @@ -116,6 +118,8 @@ export const useFacilityCyclesByInvestigation = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, + enabled: !!investigationStartDate, } ); @@ -166,6 +170,7 @@ export const useFacilityCyclesPaginated = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -198,6 +203,7 @@ export const useFacilityCyclesInfinite = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -244,6 +250,7 @@ export const useFacilityCycleCount = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; diff --git a/packages/datagateway-common/src/api/index.tsx b/packages/datagateway-common/src/api/index.tsx index f5b0d9015..9246ab744 100644 --- a/packages/datagateway-common/src/api/index.tsx +++ b/packages/datagateway-common/src/api/index.tsx @@ -24,6 +24,7 @@ import { useSelector } from 'react-redux'; import { StateType } from '../state/app.types'; import format from 'date-fns/format'; import { isValid } from 'date-fns'; +import retryICATErrors from './retryICATErrors'; export * from './cart'; export * from './facilityCycles'; @@ -597,6 +598,7 @@ export const useIds = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, enabled, } ); @@ -681,6 +683,7 @@ export const useCustomFilter = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -788,6 +791,7 @@ export const useCustomFilterCount = ( onError: (error) => { handleICATError(error, false); }, + retry: retryICATErrors, staleTime: Infinity, }; }); diff --git a/packages/datagateway-common/src/api/instruments.tsx b/packages/datagateway-common/src/api/instruments.tsx index a7c5e3733..e2822d446 100644 --- a/packages/datagateway-common/src/api/instruments.tsx +++ b/packages/datagateway-common/src/api/instruments.tsx @@ -18,6 +18,7 @@ import { useInfiniteQuery, UseInfiniteQueryResult, } from 'react-query'; +import retryICATErrors from './retryICATErrors'; const fetchInstruments = ( apiUrl: string, @@ -89,6 +90,7 @@ export const useInstrumentsPaginated = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -121,6 +123,7 @@ export const useInstrumentsInfinite = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -162,6 +165,7 @@ export const useInstrumentCount = (): UseQueryResult => { onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -196,6 +200,7 @@ export const useInstrumentDetails = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; diff --git a/packages/datagateway-common/src/api/investigations.tsx b/packages/datagateway-common/src/api/investigations.tsx index 87d591e73..8b639ec82 100644 --- a/packages/datagateway-common/src/api/investigations.tsx +++ b/packages/datagateway-common/src/api/investigations.tsx @@ -24,6 +24,7 @@ import { } from 'react-query'; import { fetchDatasetCountQuery } from './datasets'; import useDeepCompareEffect from 'use-deep-compare-effect'; +import retryICATErrors from './retryICATErrors'; const fetchInvestigations = ( apiUrl: string, @@ -89,6 +90,7 @@ export const useInvestigation = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -142,6 +144,7 @@ export const useInvestigationsPaginated = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -181,6 +184,7 @@ export const useInvestigationsInfinite = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -234,6 +238,7 @@ export const useInvestigationSize = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, enabled: false, } ); @@ -279,6 +284,7 @@ export const useInvestigationSizes = ( onError: (error) => { handleICATError(error, false); }, + retry: retryICATErrors, staleTime: Infinity, }; }); @@ -366,6 +372,7 @@ export const useInvestigationsDatasetCount = ( onError: (error) => { handleICATError(error, false); }, + retry: retryICATErrors, staleTime: Infinity, }; }); @@ -465,6 +472,7 @@ export const useInvestigationCount = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -502,6 +510,7 @@ export const useInvestigationDetails = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -642,6 +651,7 @@ export const useISISInvestigationsPaginated = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -719,6 +729,7 @@ export const useISISInvestigationsInfinite = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -796,6 +807,7 @@ export const useISISInvestigationCount = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -877,6 +889,7 @@ export const useISISInvestigationIds = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, enabled, } ); diff --git a/packages/datagateway-common/src/api/lucene.tsx b/packages/datagateway-common/src/api/lucene.tsx index 6e1fea6c0..84e15d759 100644 --- a/packages/datagateway-common/src/api/lucene.tsx +++ b/packages/datagateway-common/src/api/lucene.tsx @@ -6,6 +6,7 @@ import { useSelector } from 'react-redux'; import { StateType } from '..'; import handleICATError from '../handleICATError'; import { readSciGatewayToken } from '../parseTokens'; +import retryICATErrors from './retryICATErrors'; interface QueryParameters { target: string; @@ -107,6 +108,7 @@ export const useLuceneSearch = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, // we want to trigger search manually via refetch // so disable the query to disable automatic fetching enabled: false, diff --git a/packages/datagateway-common/src/api/retryICATErrors.test.ts b/packages/datagateway-common/src/api/retryICATErrors.test.ts new file mode 100644 index 000000000..49ba4974e --- /dev/null +++ b/packages/datagateway-common/src/api/retryICATErrors.test.ts @@ -0,0 +1,61 @@ +import { AxiosError } from 'axios'; +import retryICATErrors from './retryICATErrors'; + +// have to unmock here as we mock "globally" in setupTests.tsx +jest.unmock('./retryICATErrors'); + +describe('retryICATErrors', () => { + let error: AxiosError; + + beforeEach(() => { + error = { + isAxiosError: true, + config: {}, + response: { + data: { message: 'Test error message (response data)' }, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + config: {}, + }, + name: 'Test error name', + message: 'Test error message', + toJSON: jest.fn(), + }; + }); + + it('returns false if error code is 403', () => { + error.response.status = 403; + const result = retryICATErrors(0, error); + expect(result).toBe(false); + }); + + it('returns false if SESSION appears in error response', () => { + error.response.data = { + message: 'Session id: test has expired', + }; + let result = retryICATErrors(0, error); + expect(result).toBe(false); + + error.response = undefined; + error.message = 'Session id: test has expired'; + result = retryICATErrors(0, error); + expect(result).toBe(false); + }); + + it('returns false if failureCount is 3 or greater', () => { + let result = retryICATErrors(3, error); + expect(result).toBe(false); + + result = retryICATErrors(4, error); + expect(result).toBe(false); + }); + + it('returns true if non-auth error and failureCount is less than 3', () => { + let result = retryICATErrors(0, error); + expect(result).toBe(true); + + result = retryICATErrors(2, error); + expect(result).toBe(true); + }); +}); diff --git a/packages/datagateway-common/src/api/retryICATErrors.ts b/packages/datagateway-common/src/api/retryICATErrors.ts new file mode 100644 index 000000000..421def391 --- /dev/null +++ b/packages/datagateway-common/src/api/retryICATErrors.ts @@ -0,0 +1,15 @@ +import { AxiosError } from 'axios'; + +const retryICATErrors = (failureCount: number, error: AxiosError): boolean => { + const message = error.response?.data.message ?? error.message; + if ( + error.response?.status === 403 || + // TopCAT doesn't set 403 for session ID failure, so detect by looking at the message + message.toUpperCase().includes('SESSION') || + failureCount >= 3 + ) + return false; + return true; +}; + +export default retryICATErrors; diff --git a/packages/datagateway-common/src/api/studies.tsx b/packages/datagateway-common/src/api/studies.tsx index c6e144674..facc2b10b 100644 --- a/packages/datagateway-common/src/api/studies.tsx +++ b/packages/datagateway-common/src/api/studies.tsx @@ -14,6 +14,7 @@ import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import { getApiParams, parseSearchToQuery } from '.'; import { StateType } from '..'; +import retryICATErrors from './retryICATErrors'; const fetchStudies = ( apiUrl: string, @@ -92,6 +93,7 @@ export const useStudiesPaginated = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -124,6 +126,7 @@ export const useStudiesInfinite = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -162,6 +165,7 @@ export const useStudy = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; @@ -214,6 +218,7 @@ export const useStudyCount = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; diff --git a/packages/datagateway-common/src/app.types.tsx b/packages/datagateway-common/src/app.types.tsx index 50a1389f7..50781ef99 100644 --- a/packages/datagateway-common/src/app.types.tsx +++ b/packages/datagateway-common/src/app.types.tsx @@ -208,14 +208,13 @@ export interface Download { transport: string; userName: string; email?: string; - - [key: string]: string | number | boolean | DownloadItem[] | undefined; } export interface FormattedDownload extends Omit { isDeleted: string; status: string; + [key: string]: string | number | boolean | DownloadItem[] | undefined; } export interface SubmitCart { diff --git a/packages/datagateway-common/src/card/advancedFilter.component.test.tsx b/packages/datagateway-common/src/card/advancedFilter.component.test.tsx index 08faf1d91..a9aec3f1b 100644 --- a/packages/datagateway-common/src/card/advancedFilter.component.test.tsx +++ b/packages/datagateway-common/src/card/advancedFilter.component.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { createShallow } from '@material-ui/core/test-utils'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import FingerprintIcon from '@material-ui/icons/Fingerprint'; import PublicIcon from '@material-ui/icons/Public'; import ConfirmationNumberIcon from '@material-ui/icons/ConfirmationNumber'; @@ -171,7 +171,7 @@ describe('AdvancedFilter', () => { ).toEqual('advanced_filters.hide'); }); - it('TitleIcon displays correctly', () => { + it('SubjectIcon displays correctly', () => { const wrapper = shallow( { .first() .simulate('click'); wrapper.update(); - expect(wrapper.exists(TitleIcon)).toBeTruthy(); + expect(wrapper.exists(SubjectIcon)).toBeTruthy(); }); it('FingerprintIcon displays correctly', () => { diff --git a/packages/datagateway-common/src/card/advancedFilter.component.tsx b/packages/datagateway-common/src/card/advancedFilter.component.tsx index 3c2f7a9b0..96ad0f6a5 100644 --- a/packages/datagateway-common/src/card/advancedFilter.component.tsx +++ b/packages/datagateway-common/src/card/advancedFilter.component.tsx @@ -9,7 +9,7 @@ import { Theme, } from '@material-ui/core'; import { CardViewDetails } from './cardView.component'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import FingerprintIcon from '@material-ui/icons/Fingerprint'; import PublicIcon from '@material-ui/icons/Public'; import ConfirmationNumberIcon from '@material-ui/icons/ConfirmationNumber'; @@ -71,7 +71,7 @@ export const UnmemoisedAdvancedFilter = ( returnObjects: true, }) as string[]).includes(label) ) { - return ; + return ; } else if ( (t('advanced_filters.icons.fingerprint', { returnObjects: true, diff --git a/packages/datagateway-common/src/handleICATError.test.ts b/packages/datagateway-common/src/handleICATError.test.ts index 7e431129b..2894b441f 100644 --- a/packages/datagateway-common/src/handleICATError.test.ts +++ b/packages/datagateway-common/src/handleICATError.test.ts @@ -91,47 +91,123 @@ describe('handleICATError', () => { expect(events.length).toBe(0); }); - it('sends an invalidate token message to SciGateway on 403 response', () => { - if (error.response) error.response.status = 403; - handleICATError(error); - - expect(log.error).toHaveBeenCalledWith( - 'Test error message (response data)' + describe('sends messages to SciGateway on authentication error', () => { + const localStorageGetItemMock = jest.spyOn( + window.localStorage.__proto__, + 'getItem' ); - expect(events.length).toBe(2); - expect(events[1].detail).toEqual({ - type: InvalidateTokenType, - }); - }); - - it('sends an invalidate token message to SciGateway on TopCAT authentication error', () => { - if (error.response) - error.response.data = { - message: 'Unable to find user by sessionid: null', - }; - handleICATError(error); - expect(log.error).toHaveBeenCalledWith( - 'Unable to find user by sessionid: null' - ); - expect(events.length).toBe(2); - expect(events[1].detail).toEqual({ - type: InvalidateTokenType, + afterAll(() => { + jest.clearAllMocks(); }); - (log.error as jest.Mock).mockClear(); - events = []; + it('just sends invalidate token message if broadcast is false ', () => { + error.response.status = 403; + handleICATError(error, false); + + expect(log.error).toHaveBeenCalledWith( + 'Test error message (response data)' + ); + expect(events.length).toBe(1); + expect(events[0].detail).toEqual({ + type: InvalidateTokenType, + }); + }); - if (error.response) - error.response.data = { - message: 'Session id:null has expired', - }; - handleICATError(error); + describe('sends invalidate token message and notifies user to reload the page if autoLogin true', () => { + beforeEach(() => { + localStorageGetItemMock.mockImplementation(() => 'true'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('if error code is 403', () => { + error.response.status = 403; + handleICATError(error); + + expect(log.error).toHaveBeenCalledWith( + 'Test error message (response data)' + ); + expect(localStorage.getItem).toBeCalledWith('autoLogin'); + expect(events.length).toBe(1); + expect(events[0].detail).toEqual({ + type: InvalidateTokenType, + payload: { + severity: 'error', + message: 'Your session has expired, please reload the page', + }, + }); + }); + + it('if SESSION appears in error response', () => { + error.response.data = { + message: 'Unable to find user by sessionid: null', + }; + handleICATError(error); + + expect(log.error).toHaveBeenCalledWith( + 'Unable to find user by sessionid: null' + ); + expect(localStorage.getItem).toBeCalledWith('autoLogin'); + expect(events.length).toBe(1); + expect(events[0].detail).toEqual({ + type: InvalidateTokenType, + payload: { + severity: 'error', + message: 'Your session has expired, please reload the page', + }, + }); + }); + }); - expect(log.error).toHaveBeenCalledWith('Session id:null has expired'); - expect(events.length).toBe(2); - expect(events[1].detail).toEqual({ - type: InvalidateTokenType, + describe('sends invalidate token message and notifies user to login again if autoLogin false', () => { + beforeEach(() => { + localStorageGetItemMock.mockImplementation(() => 'false'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('if error code is 403', () => { + error.response.status = 403; + handleICATError(error); + + expect(log.error).toHaveBeenCalledWith( + 'Test error message (response data)' + ); + expect(localStorage.getItem).toBeCalledWith('autoLogin'); + expect(events.length).toBe(1); + expect(events[0].detail).toEqual({ + type: InvalidateTokenType, + payload: { + severity: 'error', + message: 'Your session has expired, please login again', + }, + }); + }); + + it('if SESSION appears in error response', () => { + error.response.data = { + message: 'Unable to find user by sessionid: null', + }; + handleICATError(error); + + expect(log.error).toHaveBeenCalledWith( + 'Unable to find user by sessionid: null' + ); + expect(localStorage.getItem).toBeCalledWith('autoLogin'); + expect(events.length).toBe(1); + expect(events[0].detail).toEqual({ + type: InvalidateTokenType, + payload: { + severity: 'error', + message: 'Your session has expired, please login again', + }, + }); + }); }); }); }); diff --git a/packages/datagateway-common/src/handleICATError.ts b/packages/datagateway-common/src/handleICATError.ts index 13c58feb9..7b8f82197 100644 --- a/packages/datagateway-common/src/handleICATError.ts +++ b/packages/datagateway-common/src/handleICATError.ts @@ -7,38 +7,52 @@ import { import { MicroFrontendId } from './app.types'; const handleICATError = (error: AxiosError, broadcast = true): void => { - let message; - if (error.response && error.response.data.message) { - message = error.response.data.message; - } else { - message = error.message; - } + const message = error.response?.data.message ?? error.message; log.error(message); if (broadcast) { - document.dispatchEvent( - new CustomEvent(MicroFrontendId, { - detail: { - type: NotificationType, - payload: { - severity: 'error', - message: message, + if ( + // don't broadcast session invalidation errors directly as they may be fixed + // by scigateway refreshing the session ID - instead pass the message payload + // in the token invalidation event + !( + error.response?.status === 403 || + // TopCAT doesn't set 403 for session ID failure, so detect by looking at the message + message.toUpperCase().includes('SESSION') + ) + ) { + document.dispatchEvent( + new CustomEvent(MicroFrontendId, { + detail: { + type: NotificationType, + payload: { + severity: 'error', + message: message, + }, }, - }, - }) - ); + }) + ); + } } if ( - error.response && - error.response.status && - (error.response.status === 403 || - // TopCAT doesn't set 403 for session ID failure, so detect by looking at the message - (error.response.data.message && - error.response.data.message.toUpperCase().includes('SESSION'))) + error.response?.status === 403 || + // TopCAT doesn't set 403 for session ID failure, so detect by looking at the message + message.toUpperCase().includes('SESSION') ) { document.dispatchEvent( new CustomEvent(MicroFrontendId, { detail: { type: InvalidateTokenType, + ...(broadcast + ? { + payload: { + severity: 'error', + message: + localStorage.getItem('autoLogin') === 'true' + ? 'Your session has expired, please reload the page' + : 'Your session has expired, please login again', + }, + } + : {}), }, }) ); diff --git a/packages/datagateway-common/src/index.tsx b/packages/datagateway-common/src/index.tsx index 5e78c895c..aa77e92fa 100644 --- a/packages/datagateway-common/src/index.tsx +++ b/packages/datagateway-common/src/index.tsx @@ -48,6 +48,7 @@ export type DGCommonState = StateType; export * from './parseTokens'; export { default as handleICATError } from './handleICATError'; +export { default as retryICATErrors } from './api/retryICATErrors'; export { default as ArrowTooltip, diff --git a/packages/datagateway-common/src/setupTests.tsx b/packages/datagateway-common/src/setupTests.tsx index fd6a8c060..40081fad6 100644 --- a/packages/datagateway-common/src/setupTests.tsx +++ b/packages/datagateway-common/src/setupTests.tsx @@ -49,6 +49,12 @@ setLogger({ error: jest.fn(), }); +// mock retry function to ensure it doesn't slow down query failure tests +jest.mock('./api/retryICATErrors', () => ({ + __esModule: true, + default: jest.fn().mockReturnValue(false), +})); + export const createTestQueryClient = (): QueryClient => new QueryClient({ defaultOptions: { diff --git a/packages/datagateway-common/src/table/cellRenderers/actionCell.component.tsx b/packages/datagateway-common/src/table/cellRenderers/actionCell.component.tsx index a61731f9a..ab31d3794 100644 --- a/packages/datagateway-common/src/table/cellRenderers/actionCell.component.tsx +++ b/packages/datagateway-common/src/table/cellRenderers/actionCell.component.tsx @@ -10,7 +10,7 @@ type CellRendererProps = TableCellProps & { const ActionCell = React.memo( (props: CellRendererProps): React.ReactElement => { - const { className, actions, rowData } = props; + const { className, actions, rowData, rowIndex } = props; return ( {actions.map((TableAction, index) => ( diff --git a/packages/datagateway-common/src/table/columnFilters/__snapshots__/dateColumnFilter.component.test.tsx.snap b/packages/datagateway-common/src/table/columnFilters/__snapshots__/dateColumnFilter.component.test.tsx.snap index 4cfefac25..79401e666 100644 --- a/packages/datagateway-common/src/table/columnFilters/__snapshots__/dateColumnFilter.component.test.tsx.snap +++ b/packages/datagateway-common/src/table/columnFilters/__snapshots__/dateColumnFilter.component.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Date filter component renders correctly 1`] = ` +exports[`Date filter component DatePicker functionality useTextFilter hook returns a function which can generate a working text filter 1`] = `
`; -exports[`Date filter component useTextFilter hook returns a function which can generate a working text filter 1`] = ` +exports[`Date filter component DateTimePicker functionality useTextFilter hook returns a function which can generate a working text filter 1`] = ` `; + +exports[`Date filter component renders correctly 1`] = ` +
+ + +
+`; diff --git a/packages/datagateway-common/src/table/columnFilters/dateColumnFilter.component.test.tsx b/packages/datagateway-common/src/table/columnFilters/dateColumnFilter.component.test.tsx index 535035651..31c909755 100644 --- a/packages/datagateway-common/src/table/columnFilters/dateColumnFilter.component.test.tsx +++ b/packages/datagateway-common/src/table/columnFilters/dateColumnFilter.component.test.tsx @@ -204,195 +204,419 @@ describe('Date filter component', () => { }); }); - it('calls the onChange method correctly when filling out the date inputs', () => { - const onChange = jest.fn(); - - const baseProps = { - label: 'test', - onChange, - }; + describe('DatePicker functionality', () => { + it('calls the onChange method correctly when filling out the date inputs', () => { + const onChange = jest.fn(); - const wrapper = mount(); + const baseProps = { + label: 'test', + onChange, + }; - const startDateFilterInput = wrapper.find('input').first(); - startDateFilterInput.instance().value = '2019-08-06'; - startDateFilterInput.simulate('change'); + const wrapper = mount(); - expect(onChange).toHaveBeenLastCalledWith({ - startDate: '2019-08-06', - }); + const startDateFilterInput = wrapper.find('input').first(); + startDateFilterInput.instance().value = '2019-08-06'; + startDateFilterInput.simulate('change'); - wrapper.setProps({ ...baseProps, value: { startDate: '2019-08-06' } }); - const endDateFilterInput = wrapper.find('input').last(); - endDateFilterInput.instance().value = '2019-08-06'; - endDateFilterInput.simulate('change'); + expect(onChange).toHaveBeenLastCalledWith({ + startDate: '2019-08-06', + }); - expect(onChange).toHaveBeenLastCalledWith({ - startDate: '2019-08-06', - endDate: '2019-08-06', - }); + wrapper.setProps({ ...baseProps, value: { startDate: '2019-08-06' } }); + const endDateFilterInput = wrapper.find('input').last(); + endDateFilterInput.instance().value = '2019-08-06'; + endDateFilterInput.simulate('change'); - wrapper.setProps({ - ...baseProps, - value: { + expect(onChange).toHaveBeenLastCalledWith({ startDate: '2019-08-06', endDate: '2019-08-06', - }, - }); - startDateFilterInput.instance().value = ''; - startDateFilterInput.simulate('change'); + }); - expect(onChange).toHaveBeenLastCalledWith({ - endDate: '2019-08-06', - }); + wrapper.setProps({ + ...baseProps, + value: { + startDate: '2019-08-06', + endDate: '2019-08-06', + }, + }); + startDateFilterInput.instance().value = ''; + startDateFilterInput.simulate('change'); - wrapper.setProps({ - ...baseProps, - value: { + expect(onChange).toHaveBeenLastCalledWith({ endDate: '2019-08-06', - }, - }); - endDateFilterInput.instance().value = ''; - endDateFilterInput.simulate('change'); + }); - expect(onChange).toHaveBeenLastCalledWith(null); - }); + wrapper.setProps({ + ...baseProps, + value: { + endDate: '2019-08-06', + }, + }); + endDateFilterInput.instance().value = ''; + endDateFilterInput.simulate('change'); - it('handles invalid date values correctly by not calling onChange, unless there was previously a value there', () => { - const onChange = jest.fn(); + expect(onChange).toHaveBeenLastCalledWith(null); + }); - const baseProps = { - label: 'test', - onChange, - }; + it('handles invalid date values correctly by not calling onChange, unless there was previously a value there', () => { + const onChange = jest.fn(); - const wrapper = mount(); + const baseProps = { + label: 'test', + onChange, + }; - const startDateFilterInput = wrapper.find('input').first(); - startDateFilterInput.instance().value = '2'; - startDateFilterInput.simulate('change'); + const wrapper = mount(); - expect(onChange).not.toHaveBeenCalled(); + const startDateFilterInput = wrapper.find('input').first(); + startDateFilterInput.instance().value = '2'; + startDateFilterInput.simulate('change'); - const endDateFilterInput = wrapper.find('input').last(); - endDateFilterInput.instance().value = '201'; - endDateFilterInput.simulate('change'); + expect(onChange).not.toHaveBeenCalled(); - expect(onChange).not.toHaveBeenCalled(); + const endDateFilterInput = wrapper.find('input').last(); + endDateFilterInput.instance().value = '201'; + endDateFilterInput.simulate('change'); - startDateFilterInput.instance().value = '2019-08-06'; - startDateFilterInput.simulate('change'); + expect(onChange).not.toHaveBeenCalled(); - expect(onChange).toHaveBeenLastCalledWith({ - startDate: '2019-08-06', - }); + startDateFilterInput.instance().value = '2019-08-06'; + startDateFilterInput.simulate('change'); - wrapper.setProps({ ...baseProps, value: { startDate: '2019-08-06' } }); - endDateFilterInput.instance().value = '2019-08-07'; - endDateFilterInput.simulate('change'); + expect(onChange).toHaveBeenLastCalledWith({ + startDate: '2019-08-06', + }); - expect(onChange).toHaveBeenLastCalledWith({ - startDate: '2019-08-06', - endDate: '2019-08-07', - }); + wrapper.setProps({ ...baseProps, value: { startDate: '2019-08-06' } }); + endDateFilterInput.instance().value = '2019-08-07'; + endDateFilterInput.simulate('change'); - wrapper.setProps({ - ...baseProps, - value: { + expect(onChange).toHaveBeenLastCalledWith({ startDate: '2019-08-06', endDate: '2019-08-07', - }, + }); + + wrapper.setProps({ + ...baseProps, + value: { + startDate: '2019-08-06', + endDate: '2019-08-07', + }, + }); + startDateFilterInput.instance().value = '2'; + startDateFilterInput.simulate('change'); + + expect(onChange).toHaveBeenLastCalledWith({ + endDate: '2019-08-07', + }); + + wrapper.setProps({ + ...baseProps, + value: { + endDate: '2019-08-07', + }, + }); + endDateFilterInput.instance().value = '201'; + endDateFilterInput.simulate('change'); + + expect(onChange).toHaveBeenLastCalledWith(null); }); - startDateFilterInput.instance().value = '2'; - startDateFilterInput.simulate('change'); - expect(onChange).toHaveBeenLastCalledWith({ - endDate: '2019-08-07', + it('displays error for invalid date', () => { + const onChange = jest.fn(); + + const baseProps = { + label: 'test', + onChange, + value: { + startDate: '2019-13-09', + endDate: '2019-08-32', + }, + }; + + const wrapper = mount(); + + expect(wrapper.find('p.Mui-error')).toHaveLength(2); + expect(wrapper.find('p.Mui-error').first().text()).toEqual( + 'Date format: yyyy-MM-dd.' + ); }); - wrapper.setProps({ - ...baseProps, - value: { - endDate: '2019-08-07', - }, + it('displays error for invalid date range', () => { + const onChange = jest.fn(); + + const baseProps = { + label: 'test', + onChange, + value: { + startDate: '2019-08-09', + endDate: '2019-08-08', + }, + }; + + const wrapper = mount(); + + expect(wrapper.find('p.Mui-error')).toHaveLength(2); + expect(wrapper.find('p.Mui-error').first().text()).toEqual( + 'Invalid date range' + ); }); - endDateFilterInput.instance().value = '201'; - endDateFilterInput.simulate('change'); - expect(onChange).toHaveBeenLastCalledWith(null); - }); + it('useTextFilter hook returns a function which can generate a working text filter', () => { + const pushFilter = jest.fn(); + (usePushFilter as jest.Mock).mockImplementation(() => pushFilter); + + const { result } = renderHook(() => useDateFilter({})); + let dateFilter; - it('displays error for invalid date', () => { - const onChange = jest.fn(); + act(() => { + dateFilter = result.current('Start Date', 'startDate'); + }); - const baseProps = { - label: 'test', - onChange, - value: { - startDate: '2019-13-09', - endDate: '2019-08-32', - }, - }; + const shallowWrapper = shallow(dateFilter); + expect(shallowWrapper).toMatchSnapshot(); - const wrapper = mount(); + const mountWrapper = mount(dateFilter); + const startDateFilterInput = mountWrapper.find('input').first(); + startDateFilterInput.instance().value = '2021-08-09'; + startDateFilterInput.simulate('change'); - expect(wrapper.find('p.Mui-error')).toHaveLength(2); - expect(wrapper.find('p.Mui-error').first().text()).toEqual( - 'Date format: yyyy-MM-dd.' - ); + expect(pushFilter).toHaveBeenLastCalledWith('startDate', { + startDate: '2021-08-09', + }); + + mountWrapper.setProps({ + ...mountWrapper.props(), + value: { startDate: '2021-08-09' }, + }); + startDateFilterInput.instance().value = ''; + startDateFilterInput.simulate('change'); + + expect(pushFilter).toHaveBeenCalledTimes(2); + expect(pushFilter).toHaveBeenLastCalledWith('startDate', null); + }); }); - it('displays error for invalid date range', () => { - const onChange = jest.fn(); + describe('DateTimePicker functionality', () => { + it('calls the onChange method correctly when filling out the date/time inputs', () => { + const onChange = jest.fn(); - const baseProps = { - label: 'test', - onChange, - value: { - startDate: '2019-08-09', - endDate: '2019-08-08', - }, - }; + const baseProps = { + label: 'test', + onChange, + }; - const wrapper = mount(); + const wrapper = mount(); - expect(wrapper.find('p.Mui-error')).toHaveLength(2); - expect(wrapper.find('p.Mui-error').first().text()).toEqual( - 'Invalid date range' - ); - }); + const startDateFilterInput = wrapper.find('input').first(); + startDateFilterInput.instance().value = '2019-08-06 00:00'; + startDateFilterInput.simulate('change'); - it('useTextFilter hook returns a function which can generate a working text filter', () => { - const pushFilter = jest.fn(); - (usePushFilter as jest.Mock).mockImplementation(() => pushFilter); + expect(onChange).toHaveBeenLastCalledWith({ + startDate: '2019-08-06 00:00', + }); - const { result } = renderHook(() => useDateFilter({})); - let dateFilter; + wrapper.setProps({ + ...baseProps, + value: { startDate: '2019-08-06 00:00' }, + }); + const endDateFilterInput = wrapper.find('input').last(); + endDateFilterInput.instance().value = '2019-08-06 23:59'; + endDateFilterInput.simulate('change'); + + expect(onChange).toHaveBeenLastCalledWith({ + startDate: '2019-08-06 00:00', + endDate: '2019-08-06 23:59', + }); + + wrapper.setProps({ + ...baseProps, + value: { + startDate: '2019-08-06 00:00', + endDate: '2019-08-06 23:59', + }, + }); + startDateFilterInput.instance().value = ''; + startDateFilterInput.simulate('change'); + + expect(onChange).toHaveBeenLastCalledWith({ + endDate: '2019-08-06 23:59', + }); + + wrapper.setProps({ + ...baseProps, + value: { + endDate: '2019-08-06 23:59', + }, + }); + endDateFilterInput.instance().value = ''; + endDateFilterInput.simulate('change'); + + expect(onChange).toHaveBeenLastCalledWith(null); + }); + + it('handles invalid date values correctly by not calling onChange, unless there was previously a value there', () => { + const onChange = jest.fn(); + + const baseProps = { + label: 'test', + onChange, + }; + + const wrapper = mount(); + + const startDateFilterInput = wrapper.find('input').first(); + startDateFilterInput.instance().value = '2'; + startDateFilterInput.simulate('change'); + + expect(onChange).not.toHaveBeenCalled(); + + const endDateFilterInput = wrapper.find('input').last(); + endDateFilterInput.instance().value = '201'; + endDateFilterInput.simulate('change'); + + expect(onChange).not.toHaveBeenCalled(); + + startDateFilterInput.instance().value = '2019-08-06 00:00'; + startDateFilterInput.simulate('change'); + + expect(onChange).toHaveBeenLastCalledWith({ + startDate: '2019-08-06 00:00', + }); + + wrapper.setProps({ + ...baseProps, + value: { startDate: '2019-08-06 00:00' }, + }); + endDateFilterInput.instance().value = '2019-08-07 00:00'; + endDateFilterInput.simulate('change'); - act(() => { - dateFilter = result.current('Start Date', 'startDate'); + expect(onChange).toHaveBeenLastCalledWith({ + startDate: '2019-08-06 00:00', + endDate: '2019-08-07 00:00', + }); + + wrapper.setProps({ + ...baseProps, + value: { + startDate: '2019-08-06 00:00', + endDate: '2019-08-07 00:00', + }, + }); + startDateFilterInput.instance().value = '2'; + startDateFilterInput.simulate('change'); + + expect(onChange).toHaveBeenLastCalledWith({ + endDate: '2019-08-07 00:00', + }); + + wrapper.setProps({ + ...baseProps, + value: { + endDate: '2019-08-07 00:00', + }, + }); + endDateFilterInput.instance().value = '201'; + endDateFilterInput.simulate('change'); + + expect(onChange).toHaveBeenLastCalledWith(null); }); - const shallowWrapper = shallow(dateFilter); - expect(shallowWrapper).toMatchSnapshot(); + it('displays error for invalid date', () => { + const onChange = jest.fn(); + + const baseProps = { + label: 'test', + onChange, + value: { + startDate: '2019-13-09 00:00', + endDate: '2019-08-32 00:00', + }, + }; + + const wrapper = mount(); + + expect(wrapper.find('p.Mui-error')).toHaveLength(2); + expect(wrapper.find('p.Mui-error').first().text()).toEqual( + 'Date format: yyyy-MM-dd HH:mm.' + ); + }); - const mountWrapper = mount(dateFilter); - const startDateFilterInput = mountWrapper.find('input').first(); - startDateFilterInput.instance().value = '2021-08-09'; - startDateFilterInput.simulate('change'); + it('displays error for invalid time', () => { + const onChange = jest.fn(); - expect(pushFilter).toHaveBeenLastCalledWith('startDate', { - startDate: '2021-08-09', + const baseProps = { + label: 'test', + onChange, + value: { + startDate: '2019-13-09 00:60', + endDate: '2019-08-32 24:00', + }, + }; + + const wrapper = mount(); + + expect(wrapper.find('p.Mui-error')).toHaveLength(2); + expect(wrapper.find('p.Mui-error').first().text()).toEqual( + 'Date format: yyyy-MM-dd HH:mm.' + ); }); - mountWrapper.setProps({ - ...mountWrapper.props(), - value: { startDate: '2021-08-09' }, + it('displays error for invalid date/time range', () => { + const onChange = jest.fn(); + + const baseProps = { + label: 'test', + onChange, + value: { + startDate: '2019-08-08 12:00', + endDate: '2019-08-08 11:00', + }, + }; + + const wrapper = mount(); + + expect(wrapper.find('p.Mui-error')).toHaveLength(2); + expect(wrapper.find('p.Mui-error').first().text()).toEqual( + 'Invalid date/time range' + ); }); - startDateFilterInput.instance().value = ''; - startDateFilterInput.simulate('change'); - expect(pushFilter).toHaveBeenCalledTimes(2); - expect(pushFilter).toHaveBeenLastCalledWith('startDate', null); + // I don't believe this works with date+time due to time values not appearing in URL params + // TODO remove this? + it.skip('useTextFilter hook returns a function which can generate a working text filter', () => { + const pushFilter = jest.fn(); + (usePushFilter as jest.Mock).mockImplementation(() => pushFilter); + + const { result } = renderHook(() => useDateFilter({})); + let dateFilter; + + act(() => { + dateFilter = result.current('Start Date', 'startDate'); + }); + + const shallowWrapper = shallow(dateFilter); + expect(shallowWrapper).toMatchSnapshot(); + + const mountWrapper = mount(dateFilter); + const startDateFilterInput = mountWrapper.find('input').first(); + startDateFilterInput.instance().value = '2021-08-09 00:00'; + startDateFilterInput.simulate('change'); + + expect(pushFilter).toHaveBeenLastCalledWith('startDate', { + startDate: '2021-08-09 00:00', + }); + + mountWrapper.setProps({ + ...mountWrapper.props(), + value: { startDate: '2021-08-09 00:00' }, + }); + startDateFilterInput.instance().value = ''; + startDateFilterInput.simulate('change'); + + expect(pushFilter).toHaveBeenCalledTimes(2); + expect(pushFilter).toHaveBeenLastCalledWith('startDate', null); + }); }); }); diff --git a/packages/datagateway-common/src/table/columnFilters/dateColumnFilter.component.tsx b/packages/datagateway-common/src/table/columnFilters/dateColumnFilter.component.tsx index f0f2a8b6d..ae6cac9e8 100644 --- a/packages/datagateway-common/src/table/columnFilters/dateColumnFilter.component.tsx +++ b/packages/datagateway-common/src/table/columnFilters/dateColumnFilter.component.tsx @@ -3,7 +3,10 @@ import DateFnsUtils from '@date-io/date-fns'; import { format, isValid, isEqual } from 'date-fns'; import { KeyboardDatePicker, + KeyboardDateTimePicker, MuiPickersUtilsProvider, + KeyboardDatePickerProps, + KeyboardDateTimePickerProps, } from '@material-ui/pickers'; import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date'; import { FiltersType, DateFilter } from '../../app.types'; @@ -31,6 +34,7 @@ interface UpdateFilterParams { otherDate: MaterialUiPickersDate; startDateOrEndDateChanged: 'startDate' | 'endDate'; onChange: (value: { startDate?: string; endDate?: string } | null) => void; + filterByTime?: boolean; } export function updateFilter({ @@ -39,6 +43,7 @@ export function updateFilter({ otherDate, startDateOrEndDateChanged, onChange, + filterByTime, }: UpdateFilterParams): void { if (!datesEqual(date, prevDate)) { if ( @@ -49,21 +54,29 @@ export function updateFilter({ } else { onChange({ [startDateOrEndDateChanged]: - date && isValid(date) ? format(date, 'yyyy-MM-dd') : undefined, + date && isValid(date) + ? format(date, filterByTime ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd') + : undefined, [startDateOrEndDateChanged === 'startDate' ? 'endDate' : 'startDate']: otherDate && isValid(otherDate) - ? format(otherDate, 'yyyy-MM-dd') + ? format( + otherDate, + filterByTime ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd' + ) : undefined, }); } } } -const DateColumnFilter = (props: { +interface DateColumnFilterProps { label: string; onChange: (value: { startDate?: string; endDate?: string } | null) => void; value: { startDate?: string; endDate?: string } | undefined; -}): React.ReactElement => { + filterByTime?: boolean; +} + +const DateColumnFilter = (props: DateColumnFilterProps): React.ReactElement => { //Need state to change otherwise wont update error messages for an invalid date const [startDate, setStartDate] = useState( props.value?.startDate ? new Date(props.value.startDate) : null @@ -77,76 +90,128 @@ const DateColumnFilter = (props: { // eslint-disable-next-line @typescript-eslint/no-explicit-any const buttonColour = (theme as any).colours?.blue; + const datePickerProps: Partial = { + clearable: true, + allowKeyboardControl: true, + invalidDateMessage: 'Date format: yyyy-MM-dd.', + format: 'yyyy-MM-dd', + color: 'secondary', + okLabel: OK, + cancelLabel: Cancel, + clearLabel: Clear, + style: { whiteSpace: 'nowrap' }, + 'aria-hidden': 'true', + inputProps: { 'aria-label': `${props.label} filter` }, + views: ['year', 'month', 'date'], + }; + + const dateTimePickerProps: Partial = { + ...datePickerProps, + invalidDateMessage: 'Date format: yyyy-MM-dd HH:mm.', + format: 'yyyy-MM-dd HH:mm', + strictCompareDates: true, + views: ['year', 'month', 'date', 'hours', 'minutes'], + }; + return (
- - + {props.filterByTime ? ( + + { + setStartDate(date); + updateFilter({ + date, + prevDate: startDate, + otherDate: endDate, + startDateOrEndDateChanged: 'startDate', + onChange: props.onChange, + filterByTime: true, + }); + }} + /> + { + setEndDate(date); + updateFilter({ + date, + prevDate: endDate, + otherDate: startDate, + startDateOrEndDateChanged: 'endDate', + onChange: props.onChange, + filterByTime: true, + }); + }} + /> + + ) : ( + + { + setStartDate(date); + updateFilter({ + date, + prevDate: startDate, + otherDate: endDate, + startDateOrEndDateChanged: 'startDate', + onChange: props.onChange, + }); + }} + /> + { + setEndDate(date); + updateFilter({ + date, + prevDate: endDate, + otherDate: startDate, + startDateOrEndDateChanged: 'endDate', + onChange: props.onChange, + }); + }} + /> + + )}
); }; diff --git a/packages/datagateway-common/src/table/table.component.tsx b/packages/datagateway-common/src/table/table.component.tsx index 8cca1d92b..7f0f67d5c 100644 --- a/packages/datagateway-common/src/table/table.component.tsx +++ b/packages/datagateway-common/src/table/table.component.tsx @@ -146,7 +146,6 @@ const VirtualizedTable = React.memo( allIds, onCheck, onUncheck, - loadMoreRows, loading, totalRowCount, detailsPanel, @@ -156,8 +155,8 @@ const VirtualizedTable = React.memo( } = props; if ( - (loadMoreRows && typeof totalRowCount === 'undefined') || - (totalRowCount && typeof loadMoreRows === 'undefined') + (props.loadMoreRows && typeof totalRowCount === 'undefined') || + (totalRowCount && typeof props.loadMoreRows === 'undefined') ) throw new Error( 'Only one of loadMoreRows and totalRowCount was defined - either define both for infinite loading functionality or neither for no infinite loading' @@ -272,6 +271,11 @@ const VirtualizedTable = React.memo( [widthProps, setWidthProps] ); + const loadMoreRows = React.useMemo( + () => props.loadMoreRows || (() => Promise.resolve()), + [props.loadMoreRows] + ); + const tableCellClass = clsx(classes.tableCell, classes.flexContainer); const tableCellNoPaddingClass = clsx( classes.tableCell, @@ -305,7 +309,7 @@ const VirtualizedTable = React.memo( return ( Promise.resolve())} + loadMoreRows={loadMoreRows} rowCount={rowCount} minimumBatchSize={25} > diff --git a/packages/datagateway-common/src/views/downloadButton.component.test.tsx b/packages/datagateway-common/src/views/downloadButton.component.test.tsx index 659fc08bb..1bf646eef 100644 --- a/packages/datagateway-common/src/views/downloadButton.component.test.tsx +++ b/packages/datagateway-common/src/views/downloadButton.component.test.tsx @@ -60,28 +60,30 @@ describe('Generic download button', () => { }); it('renders correctly', () => { - const textButtonWrapper = createWrapper({ + const props: DownloadButtonProps = { entityType: 'datafile', entityName: 'test', entityId: 1, - }); + entitySize: 1, + }; + const textButtonWrapper = createWrapper(props); expect(textButtonWrapper.find('button').text()).toBe('buttons.download'); const iconButtonWrapper = createWrapper({ - entityType: 'datafile', - entityName: 'test', - entityId: 1, + ...props, variant: 'icon', }); expect(iconButtonWrapper.find('button').text()).toBe(''); }); it('calls download investigation on button press for both text and icon buttons', () => { - let wrapper = createWrapper({ + const props: DownloadButtonProps = { entityType: 'investigation', entityName: 'test', entityId: 1, - }); + entitySize: 1, + }; + let wrapper = createWrapper(props); wrapper.find('#download-btn-1').first().simulate('click'); expect(downloadInvestigation).toHaveBeenCalledWith( @@ -93,9 +95,7 @@ describe('Generic download button', () => { jest.clearAllMocks(); wrapper = createWrapper({ - entityType: 'investigation', - entityName: 'test', - entityId: 1, + ...props, variant: 'icon', }); @@ -108,11 +108,13 @@ describe('Generic download button', () => { }); it('calls download dataset on button press for both text and icon buttons', () => { - let wrapper = createWrapper({ + const props: DownloadButtonProps = { entityType: 'dataset', entityName: 'test', entityId: 1, - }); + entitySize: 1, + }; + let wrapper = createWrapper(props); wrapper.find('#download-btn-1').first().simulate('click'); expect(downloadDataset).toHaveBeenCalledWith( @@ -124,9 +126,7 @@ describe('Generic download button', () => { jest.clearAllMocks(); wrapper = createWrapper({ - entityType: 'dataset', - entityName: 'test', - entityId: 1, + ...props, variant: 'icon', }); @@ -139,11 +139,13 @@ describe('Generic download button', () => { }); it('calls download datafile on button press for both text and icon buttons', () => { - let wrapper = createWrapper({ + const props: DownloadButtonProps = { entityType: 'datafile', entityName: 'test', entityId: 1, - }); + entitySize: 1, + }; + let wrapper = createWrapper(props); wrapper.find('#download-btn-1').first().simulate('click'); expect(downloadDatafile).toHaveBeenCalledWith( @@ -155,14 +157,12 @@ describe('Generic download button', () => { jest.clearAllMocks(); wrapper = createWrapper({ - entityType: 'dataset', - entityName: 'test', - entityId: 1, + ...props, variant: 'icon', }); wrapper.find('#download-btn-1').first().simulate('click'); - expect(downloadDataset).toHaveBeenCalledWith( + expect(downloadDatafile).toHaveBeenCalledWith( 'https://www.example.com/ids', 1, 'test' @@ -174,8 +174,34 @@ describe('Generic download button', () => { entityType: 'datafile', entityName: undefined, entityId: 1, + entitySize: 1, }); expect(wrapper.find(DownloadButton).children().length).toBe(0); }); + + it('renders a tooltip and disabled button if entity size is zero', () => { + const props: DownloadButtonProps = { + entityType: 'datafile', + entityName: 'test', + entityId: 1, + entitySize: 0, + }; + let wrapper = createWrapper(props); + + expect(wrapper.exists('#tooltip-1')); + wrapper.find('#download-btn-1').first().simulate('click'); + expect(downloadDatafile).not.toHaveBeenCalled(); + + jest.clearAllMocks(); + + wrapper = createWrapper({ + ...props, + variant: 'icon', + }); + + expect(wrapper.exists('#tooltip-1')); + wrapper.find('#download-btn-1').first().simulate('click'); + expect(downloadDatafile).not.toHaveBeenCalled(); + }); }); diff --git a/packages/datagateway-common/src/views/downloadButton.component.tsx b/packages/datagateway-common/src/views/downloadButton.component.tsx index 1d45f320f..8b708f7dd 100644 --- a/packages/datagateway-common/src/views/downloadButton.component.tsx +++ b/packages/datagateway-common/src/views/downloadButton.component.tsx @@ -1,4 +1,5 @@ -import { Button, IconButton } from '@material-ui/core'; +import { Button, IconButton, Tooltip, Typography } from '@material-ui/core'; +import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'; import { GetApp } from '@material-ui/icons'; import { downloadDatafile } from '../api/datafiles'; import { downloadDataset } from '../api/datasets'; @@ -8,17 +9,32 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; +const useStylesTooltip = makeStyles((theme: Theme) => + createStyles({ + tooltip: { + backgroundColor: theme.palette.common.black, + fontSize: '0.875rem', + }, + arrow: { + color: theme.palette.common.black, + }, + }) +); + export interface DownloadButtonProps { entityType: 'investigation' | 'dataset' | 'datafile'; entityId: number; entityName: string | undefined; + entitySize: number; variant?: 'text' | 'outlined' | 'contained' | 'icon'; } const DownloadButton: React.FC = ( props: DownloadButtonProps ) => { - const { entityType, entityId, entityName, variant } = props; + const { entityType, entityId, entityName, variant, entitySize } = props; + const { ...classes } = useStylesTooltip(); + const [t] = useTranslation(); const idsUrl = useSelector((state: StateType) => state.dgcommon.urls.idsUrl); @@ -31,7 +47,7 @@ const DownloadButton: React.FC = ( downloadInvestigation(idsUrl, entityId, entityName); } else if (entityType === 'dataset') { downloadDataset(idsUrl, entityId, entityName); - } else if (entityType === 'datafile') { + } else { downloadDatafile(idsUrl, entityId, entityName); } }; @@ -39,32 +55,87 @@ const DownloadButton: React.FC = ( if (!entityName) return null; if (variant === 'icon') { return ( - { - downloadData(entityType, entityId, entityName); - }} - className="tour-dataview-download" - > - - +
+ {entitySize <= 0 ? ( + {t('buttons.unable_to_download_tooltip')} + } + id={`tooltip-${entityId}`} + placement="left" + arrow + classes={classes} + > + + + + + + + ) : ( + { + downloadData(entityType, entityId, entityName); + }} + className="tour-dataview-download" + > + + + )} +
); } else { return ( - +
+ {entitySize <= 0 ? ( + {t('buttons.unable_to_download_tooltip')} + } + id={`tooltip-${entityId}`} + placement="bottom" + arrow + classes={classes} + > + + + + + ) : ( + + )} +
); } }; diff --git a/packages/datagateway-dataview/package.json b/packages/datagateway-dataview/package.json index 2bf9983bc..8f1c47208 100644 --- a/packages/datagateway-dataview/package.json +++ b/packages/datagateway-dataview/package.json @@ -1,6 +1,6 @@ { "name": "datagateway-dataview", - "version": "1.0.0", + "version": "1.1.0", "private": true, "dependencies": { "@material-ui/core": "^4.11.3", @@ -14,7 +14,7 @@ "axios": "^0.26.0", "connected-react-router": "^6.9.1", "custom-event-polyfill": "^1.0.7", - "datagateway-common": "1.0.0", + "datagateway-common": "^1.1.0", "date-fns": "^2.28.0", "history": "^4.10.1", "i18next": "^21.6.13", @@ -23,7 +23,6 @@ "lodash.debounce": "^4.0.8", "lodash.memoize": "^4.1.2", "loglevel": "^1.8.0", - "p-limit": "^4.0.0", "react": "^16.13.1", "react-app-polyfill": "^3.0.0", "react-dom": "^16.11.0", @@ -127,4 +126,4 @@ "serve": "^13.0.2", "start-server-and-test": "^1.14.0" } -} \ No newline at end of file +} diff --git a/packages/datagateway-dataview/public/res/default.json b/packages/datagateway-dataview/public/res/default.json index 3d1fb58fa..a60da05b0 100644 --- a/packages/datagateway-dataview/public/res/default.json +++ b/packages/datagateway-dataview/public/res/default.json @@ -211,7 +211,8 @@ "buttons": { "add_to_cart": "Add to selection", "remove_from_cart": "Remove from selection", - "download": "Download" + "download": "Download", + "unable_to_download_tooltip": "Unable to download - this item is empty" }, "advanced_filters": { "show": "Show Advanced Filters", diff --git a/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx b/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx index 789c9524a..5e9756c99 100644 --- a/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx +++ b/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx @@ -15,6 +15,7 @@ import { handleICATError, parseSearchToQuery, readSciGatewayToken, + retryICATErrors, } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -64,51 +65,37 @@ const breadcrumbsStyles = (theme: Theme): StyleRules => position: 'relative', /* Positions breadcrumb */ - height: '30px', - lineHeight: '30px', - padding: '0 5px 0 2px', + lineHeight: '28px', + padding: ' 0 4px 0 14px', textAlign: 'center', - /* Adds between breadcrumbs */ - marginRight: '1px', - '&:before, &:after': { + /* Add the arrow between breadcrumbs */ + '&:after': { content: '""', position: 'absolute', - top: 0, - border: `0 solid ${theme.palette.primary.light}`, - borderWidth: '15px 7px', - width: 0, - height: 0, - }, - '&:before': { - left: '-14px', - borderLeftColor: 'transparent', - }, - '&:after': { - left: '100%', - - /* Gap in between chevrons */ - borderColor: 'transparent', - borderLeftColor: theme.palette.primary.light, + top: '3px', + // half the width/height + right: '-11px', + // width/height same as lineHeight - 2* top height + height: '22px', + width: '22px', + // change skew to alter how shallow the arrow is + transform: 'scale(0.707) rotate(45deg) skew(15deg,15deg)', + zIndex: 1, + boxShadow: `2px -2px 0 2px ${theme.palette.background.default}`, + borderRadius: ' 0 5px 0 50px', + backgroundColor: theme.palette.primary.light, }, '&:hover': { backgroundColor: theme.palette.primary.light, - '&:before': { - borderColor: theme.palette.primary.light, - borderLeftColor: 'transparent', - }, '&:after': { - borderLeftColor: theme.palette.primary.light, + backgroundColor: theme.palette.primary.light, }, }, '&:active': { backgroundColor: theme.palette.grey[600], - '&:before': { - borderColor: `${theme.palette.grey[600]} !important`, - borderLeftColor: 'transparent !important', - }, '&:after': { - borderLeftColor: `${theme.palette.grey[600]} !important`, + backgroundColor: `${theme.palette.grey[600]} !important`, }, }, }, @@ -117,31 +104,23 @@ const breadcrumbsStyles = (theme: Theme): StyleRules => '& li:nth-child(4n + 3)': { '& a, p': { backgroundColor: theme.palette.primary.main, - '&:before': { - borderColor: theme.palette.primary.main, - borderLeftColor: 'transparent', - }, '&:after': { - borderLeftColor: theme.palette.primary.main, + backgroundColor: theme.palette.primary.main, }, }, }, '& li:first-child': { '& a, p': { - paddingLeft: '4px', - '&:before': { - border: 'none', - }, + paddingLeft: '14px', }, }, '& li:last-child': { '& a, p': { - paddingRight: '7px', - /* Curve the last breadcrumb border */ - borderRadius: '0 4px 4px 0', + borderRadius: '0 5px 5px 0', + paddingLeft: '14px', '&:after': { - border: 'none', + content: 'none', }, }, }, @@ -156,6 +135,10 @@ const breadcrumbsStyles = (theme: Theme): StyleRules => textOverflow: 'ellipsis', }, }, + separator: { + marginLeft: 0, + marginRight: 0, + }, }); const fetchEntityInformation = async ( @@ -272,6 +255,7 @@ const useEntityInformation = ( onError: (error) => { handleICATError(error, false); }, + retry: retryICATErrors, staleTime: Infinity, select: (data: string) => ({ displayName: data, diff --git a/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx b/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx index ebff32faa..943799510 100644 --- a/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx +++ b/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx @@ -424,4 +424,11 @@ describe('PageContainer - Tests', () => { wrapper.find('PageBreadcrumbs').prop('landingPageEntities') ).toEqual(['study', 'investigation', 'dataset']); }); + + it('does not fetch cart when on homepage (cart request errors when user is viewing homepage unauthenticated)', () => { + history.replace(paths.homepage); + createWrapper(); + + expect(useCart).not.toHaveBeenCalled(); + }); }); diff --git a/packages/datagateway-dataview/src/page/pageContainer.component.tsx b/packages/datagateway-dataview/src/page/pageContainer.component.tsx index cb607d785..2497fbe9a 100644 --- a/packages/datagateway-dataview/src/page/pageContainer.component.tsx +++ b/packages/datagateway-dataview/src/page/pageContainer.component.tsx @@ -524,7 +524,7 @@ const getToggle = (pathname: string, view: ViewsType): boolean => { : false; }; -const PageContainer: React.FC = () => { +const DataviewPageContainer: React.FC = () => { const location = useLocation(); const { push } = useHistory(); const prevLocationRef = React.useRef(location); @@ -652,6 +652,92 @@ const PageContainer: React.FC = () => { } }; + return ( + + + + + + + {/* Toggle between the table and card view */} + + } + /> + ( + + )} + /> + ( + + )} + /> + + + + + + + + {/* Show loading progress if data is still being loaded */} + {loading && ( + + + + )} + + {/* Hold the view for remainder of the page */} + + + + + + ); +}; + +const PageContainer: React.FC = () => { + const location = useLocation(); + return ( {/* Load the homepage */} @@ -659,97 +745,10 @@ const PageContainer: React.FC = () => { - ( - // Load the standard dataview pageContainer - - - - - - - {/* Toggle between the table and card view */} - - } - /> - ( - - )} - /> - ( - - )} - /> - - - - - - - - {/* Show loading progress if data is still being loaded */} - {loading && ( - - - - )} - - {/* Hold the view for remainder of the page */} - - - - - - )} - /> + {/* Load the standard dataview pageContainer */} + + + ); }; diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx index c33c3ff5d..111321113 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx @@ -159,11 +159,14 @@ const ISISDatasetsCardView = ( entityType="dataset" entityId={dataset.id} entityName={dataset.name} + entitySize={ + data ? sizeQueries[data.indexOf(dataset)]?.data ?? -1 : -1 + } /> ), ], - [classes.actionButtons, data] + [classes.actionButtons, data, sizeQueries] ); const moreInformation = React.useCallback( diff --git a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx index 366a8f579..4ecf497a7 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx @@ -205,11 +205,14 @@ const ISISInvestigationsCardView = ( entityType="investigation" entityId={investigation.id} entityName={investigation.name} + entitySize={ + data ? sizeQueries[data.indexOf(investigation)]?.data ?? -1 : -1 + } /> ), ], - [classes.actionButtons, data] + [classes.actionButtons, data, sizeQueries] ); const moreInformation = React.useCallback( diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx index 650ad1de0..416693d76 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx @@ -247,11 +247,14 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { allIds={[parseInt(datasetId)]} entityId={parseInt(datasetId)} /> - +
+ +
diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx index b6ff71fd2..7661bf05d 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx @@ -520,11 +520,14 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { allIds={[dataset.id]} entityId={dataset.id} /> - +
+ +
))} diff --git a/packages/datagateway-dataview/src/views/roleSelector.component.tsx b/packages/datagateway-dataview/src/views/roleSelector.component.tsx index a512207f5..eedaa2528 100644 --- a/packages/datagateway-dataview/src/views/roleSelector.component.tsx +++ b/packages/datagateway-dataview/src/views/roleSelector.component.tsx @@ -15,6 +15,7 @@ import { InvestigationUser, parseSearchToQuery, readSciGatewayToken, + retryICATErrors, StateType, usePushFilter, } from 'datagateway-common'; @@ -75,6 +76,7 @@ export const useRoles = ( onError: (error) => { handleICATError(error); }, + retry: retryICATErrors, } ); }; diff --git a/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx b/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx index c56959554..e86176e67 100644 --- a/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import ExploreIcon from '@material-ui/icons/Explore'; import SaveIcon from '@material-ui/icons/Save'; import CalendarTodayIcon from '@material-ui/icons/CalendarToday'; @@ -116,7 +116,7 @@ const DatafileTable = (props: DatafileTableProps): React.ReactElement => { const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('datafiles.name'), dataKey: 'name', filterComponent: textFilter, @@ -180,6 +180,7 @@ const DatafileTable = (props: DatafileTableProps): React.ReactElement => { entityId={rowData.id} entityName={(rowData as Datafile).location} variant="icon" + entitySize={(rowData as Datafile).fileSize ?? -1} /> ), ]} diff --git a/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx b/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx index 712ea9668..8357a5a52 100644 --- a/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import ConfirmationNumberIcon from '@material-ui/icons/ConfirmationNumber'; import CalendarTodayIcon from '@material-ui/icons/CalendarToday'; import { @@ -105,7 +105,7 @@ const DatasetTable = (props: DatasetTableProps): React.ReactElement => { const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('datasets.name'), dataKey: 'name', cellContentRenderer: (cellProps) => { diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx index 720cae7d2..53121cd12 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import ExploreIcon from '@material-ui/icons/Explore'; import SaveIcon from '@material-ui/icons/Save'; import CalendarTodayIcon from '@material-ui/icons/CalendarToday'; @@ -117,7 +117,7 @@ const DLSDatafilesTable = ( const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('datafiles.name'), dataKey: 'name', filterComponent: textFilter, diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx index 036aa8041..b42544240 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import ConfirmationNumberIcon from '@material-ui/icons/ConfirmationNumber'; import CalendarTodayIcon from '@material-ui/icons/CalendarToday'; import { @@ -106,7 +106,7 @@ const DLSDatasetsTable = (props: DLSDatasetsTableProps): React.ReactElement => { const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('datasets.name'), dataKey: 'name', cellContentRenderer: (cellProps: TableCellProps) => diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx index 58728ebc7..07deb492e 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { IndexRange, TableCellProps } from 'react-virtualized'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import FingerprintIcon from '@material-ui/icons/Fingerprint'; import ConfirmationNumberIcon from '@material-ui/icons/ConfirmationNumber'; import AssessmentIcon from '@material-ui/icons/Assessment'; @@ -81,7 +81,7 @@ const DLSMyDataTable = (): React.ReactElement => { const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('investigations.title'), dataKey: 'title', cellContentRenderer: (cellProps: TableCellProps) => { diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx index 940a98f06..5f185b508 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { IndexRange, TableCellProps } from 'react-virtualized'; import { useLocation } from 'react-router-dom'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; const DLSProposalsTable = (): React.ReactElement => { const location = useLocation(); @@ -58,7 +58,7 @@ const DLSProposalsTable = (): React.ReactElement => { const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('investigations.title'), dataKey: 'title', cellContentRenderer: (cellProps: TableCellProps) => { @@ -77,7 +77,7 @@ const DLSProposalsTable = (): React.ReactElement => { disableSort: true, }, { - icon: TitleIcon, + icon: SubjectIcon, label: t('investigations.name'), dataKey: 'name', cellContentRenderer: (cellProps: TableCellProps) => { diff --git a/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx b/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx index b51d9adf8..f97295bb4 100644 --- a/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx @@ -32,7 +32,7 @@ import { useSelector } from 'react-redux'; import { TableCellProps, IndexRange } from 'react-virtualized'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import FingerprintIcon from '@material-ui/icons/Fingerprint'; import PublicIcon from '@material-ui/icons/Public'; import SaveIcon from '@material-ui/icons/Save'; @@ -161,7 +161,7 @@ const InvestigationTable = (): React.ReactElement => { const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('investigations.title'), dataKey: 'title', cellContentRenderer: (cellProps: TableCellProps) => { diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx index 318b469bb..590a60de2 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import ExploreIcon from '@material-ui/icons/Explore'; import SaveIcon from '@material-ui/icons/Save'; import CalendarTodayIcon from '@material-ui/icons/CalendarToday'; @@ -119,7 +119,7 @@ const ISISDatafilesTable = ( const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('datafiles.name'), dataKey: 'name', filterComponent: textFilter, @@ -184,6 +184,7 @@ const ISISDatafilesTable = ( entityId={rowData.id} entityName={(rowData as Datafile).location} variant="icon" + entitySize={(rowData as Datafile).fileSize ?? -1} /> ), ]} diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx index bfc0c563e..bd5ad98c0 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import SaveIcon from '@material-ui/icons/Save'; import CalendarTodayIcon from '@material-ui/icons/CalendarToday'; import { @@ -123,7 +123,7 @@ const ISISDatasetsTable = ( const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('datasets.name'), dataKey: 'name', cellContentRenderer: (cellProps: TableCellProps) => @@ -205,6 +205,10 @@ const ISISDatasetsTable = ( entityId={rowData.id} entityName={rowData.name} variant="icon" + entitySize={ + sizeQueries[aggregatedData.indexOf(rowData as Dataset)]?.data ?? + -1 + } /> ), ]} diff --git a/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx index 974d71cf7..0f61170fd 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { IndexRange, TableCellProps } from 'react-virtualized'; import { useLocation } from 'react-router-dom'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import CalendarTodayIcon from '@material-ui/icons/CalendarToday'; interface ISISFacilityCyclesTableProps { @@ -58,7 +58,7 @@ const ISISFacilityCyclesTable = ( const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('facilitycycles.name'), dataKey: 'name', cellContentRenderer: (cellProps: TableCellProps) => diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx index 4996c538b..106b94fe7 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx @@ -13,7 +13,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { IndexRange, TableCellProps } from 'react-virtualized'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import { useLocation } from 'react-router-dom'; interface ISISInstrumentsTableProps { @@ -54,7 +54,7 @@ const ISISInstrumentsTable = ( const instrumentChild = studyHierarchy ? 'study' : 'facilityCycle'; return [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('instruments.name'), dataKey: 'fullName', cellContentRenderer: (cellProps: TableCellProps) => { @@ -70,7 +70,7 @@ const ISISInstrumentsTable = ( defaultSort: 'asc', }, { - icon: TitleIcon, + icon: SubjectIcon, label: t('instruments.type'), dataKey: 'type', filterComponent: textFilter, diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx index 332c37079..0b71993ae 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx @@ -26,7 +26,7 @@ import { useTranslation } from 'react-i18next'; import { IndexRange, TableCellProps } from 'react-virtualized'; import { StateType } from '../../../state/app.types'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import FingerprintIcon from '@material-ui/icons/Fingerprint'; import PublicIcon from '@material-ui/icons/Public'; import SaveIcon from '@material-ui/icons/Save'; @@ -131,7 +131,7 @@ const ISISInvestigationsTable = ( const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('investigations.title'), dataKey: 'title', cellContentRenderer: (cellProps: TableCellProps) => { @@ -242,6 +242,10 @@ const ISISInvestigationsTable = ( entityId={rowData.id} entityName={rowData.name} variant="icon" + entitySize={ + sizeQueries[aggregatedData.indexOf(rowData as Investigation)] + ?.data ?? -1 + } /> ), ]} diff --git a/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx index edd111e8c..a35f335ae 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx @@ -25,7 +25,7 @@ import { useTranslation } from 'react-i18next'; import { IndexRange, TableCellProps } from 'react-virtualized'; import { StateType } from '../../../state/app.types'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import FingerprintIcon from '@material-ui/icons/Fingerprint'; import PublicIcon from '@material-ui/icons/Public'; import SaveIcon from '@material-ui/icons/Save'; @@ -167,7 +167,7 @@ const ISISMyDataTable = (): React.ReactElement => { const columns: ColumnType[] = React.useMemo( () => [ { - icon: TitleIcon, + icon: SubjectIcon, label: t('investigations.title'), dataKey: 'title', cellContentRenderer: (cellProps: TableCellProps) => { @@ -211,7 +211,7 @@ const ISISMyDataTable = (): React.ReactElement => { filterComponent: textFilter, }, { - icon: TitleIcon, + icon: SubjectIcon, label: t('investigations.name'), dataKey: 'name', cellContentRenderer: (cellProps: TableCellProps) => { diff --git a/packages/datagateway-dataview/src/views/table/isis/isisStudiesTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisStudiesTable.component.tsx index 18c1a02cd..2c34c1358 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisStudiesTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisStudiesTable.component.tsx @@ -18,7 +18,7 @@ import { IndexRange, TableCellProps } from 'react-virtualized'; import PublicIcon from '@material-ui/icons/Public'; import FingerprintIcon from '@material-ui/icons/Fingerprint'; -import TitleIcon from '@material-ui/icons/Title'; +import SubjectIcon from '@material-ui/icons/Subject'; import CalendarTodayIcon from '@material-ui/icons/CalendarToday'; import { useLocation } from 'react-router'; import { format, set } from 'date-fns'; @@ -120,7 +120,7 @@ const ISISStudiesTable = (props: ISISStudiesTableProps): React.ReactElement => { filterComponent: textFilter, }, { - icon: TitleIcon, + icon: SubjectIcon, label: t('studies.title'), dataKey: 'studyInvestigations.investigation.title', cellContentRenderer: (cellProps: TableCellProps) => diff --git a/packages/datagateway-download/cypress/integration/adminDownloadStatus.spec.ts b/packages/datagateway-download/cypress/integration/adminDownloadStatus.spec.ts index 911269a4d..e1cd31789 100644 --- a/packages/datagateway-download/cypress/integration/adminDownloadStatus.spec.ts +++ b/packages/datagateway-download/cypress/integration/adminDownloadStatus.spec.ts @@ -53,6 +53,14 @@ describe('Admin Download Status', () => { describe('should be able to sort download items by', () => { it('ascending order', () => { + // Table is sorted by Requested Date by default. To keep working test, we will remove all sorts on the table beforehand + cy.get('.react-draggable') + .eq(7) + .trigger('mousedown') + .trigger('mousemove', { clientX: 800 }) + .trigger('mouseup'); + cy.contains('[role="button"]', 'Requested Date').click(); + cy.get('.react-draggable') .eq(4) .trigger('mousedown') @@ -69,6 +77,14 @@ describe('Admin Download Status', () => { }); it('descending order', () => { + // Table is sorted by Requested Date by default. To keep working test, we will remove all sorts on the table beforehand + cy.get('.react-draggable') + .eq(7) + .trigger('mousedown') + .trigger('mousemove', { clientX: 800 }) + .trigger('mouseup'); + cy.contains('[role="button"]', 'Requested Date').click(); + cy.get('.react-draggable') .eq(4) .trigger('mousedown') @@ -90,6 +106,14 @@ describe('Admin Download Status', () => { }); it('no order', () => { + // Table is sorted by Requested Date by default. To keep working test, we will remove all sorts on the table beforehand + cy.get('.react-draggable') + .eq(7) + .trigger('mousedown') + .trigger('mousemove', { clientX: 800 }) + .trigger('mouseup'); + cy.contains('[role="button"]', 'Requested Date').click(); + cy.get('.react-draggable') .eq(3) .trigger('mousedown') @@ -116,6 +140,14 @@ describe('Admin Download Status', () => { }); it('multiple columns', () => { + // Table is sorted by Requested Date by default. To keep working test, we will remove all sorts on the table beforehand + cy.get('.react-draggable') + .eq(7) + .trigger('mousedown') + .trigger('mousemove', { clientX: 800 }) + .trigger('mouseup'); + cy.contains('[role="button"]', 'Requested Date').click(); + cy.get('.react-draggable') .eq(4) .trigger('mousedown') diff --git a/packages/datagateway-download/cypress/integration/downloadCart.spec.ts b/packages/datagateway-download/cypress/integration/downloadCart.spec.ts index 22dc3ee67..6d9594417 100644 --- a/packages/datagateway-download/cypress/integration/downloadCart.spec.ts +++ b/packages/datagateway-download/cypress/integration/downloadCart.spec.ts @@ -98,7 +98,6 @@ describe('Download Cart', () => { it('should be able to remove individual items from the cart', () => { cy.intercept('DELETE', '**/topcat/user/cart/**').as('removeFromCart'); cy.contains('[role="button"]', 'Name').click(); - cy.contains('Calculating...', { timeout: 20000 }).should('not.exist'); cy.contains(/^DATASET 1$/).should('be.visible'); cy.get('[aria-label="Remove DATASET 1 from selection"]').click(); @@ -115,7 +114,6 @@ describe('Download Cart', () => { it('should be able to remove all items from the cart', () => { cy.intercept('DELETE', '**/topcat/user/cart/**').as('removeFromCart'); cy.contains('[role="button"]', 'Name').click(); - cy.contains('Calculating...', { timeout: 20000 }).should('not.exist'); cy.contains(/^DATASET 1$/).should('be.visible'); cy.contains('Remove All').click(); @@ -130,7 +128,9 @@ describe('Download Cart', () => { }); it('should be able open and close the download confirmation dialog', () => { - cy.contains('Calculating...', { timeout: 20000 }).should('not.exist'); + cy.get('[aria-label="Calculating"]', { timeout: 20000 }).should( + 'not.exist' + ); cy.contains('Download Selection').click(); cy.get('[aria-label="Download confirmation dialog"]').should('exist'); diff --git a/packages/datagateway-download/cypress/integration/downloadStatus.spec.ts b/packages/datagateway-download/cypress/integration/downloadStatus.spec.ts index b13cf2c3a..5abe1f3e4 100644 --- a/packages/datagateway-download/cypress/integration/downloadStatus.spec.ts +++ b/packages/datagateway-download/cypress/integration/downloadStatus.spec.ts @@ -1,3 +1,5 @@ +import { format } from 'date-fns-tz'; + describe('Download Status', () => { before(() => { // Ensure the downloads are cleared before running tests. @@ -44,6 +46,9 @@ describe('Download Status', () => { describe('should be able to sort download items by', () => { it('ascending order', () => { + // Table is sorted by Requested Date by default. To keep working test, we will remove all sorts on the table beforehand + cy.contains('[role="button"]', 'Requested Date').click(); + cy.contains('[role="button"]', 'Download Name').click(); cy.get('[aria-sort="ascending"]').should('exist'); @@ -56,6 +61,9 @@ describe('Download Status', () => { }); it('descending order', () => { + // Table is sorted by Requested Date by default. To keep working test, we will remove all sorts on the table beforehand + cy.contains('[role="button"]', 'Requested Date').click(); + cy.contains('[role="button"]', 'Download Name').click(); cy.contains('[role="button"]', 'Download Name').click(); @@ -77,6 +85,9 @@ describe('Download Status', () => { }); it('no order', () => { + // Table is sorted by Requested Date by default. To keep working test, we will remove all sorts on the table beforehand + cy.contains('[role="button"]', 'Requested Date').click(); + cy.contains('[role="button"]', 'Download Name').click(); cy.contains('[role="button"]', 'Download Name').click(); cy.contains('[role="button"]', 'Download Name').click(); @@ -100,6 +111,9 @@ describe('Download Status', () => { }); it('multiple columns', () => { + // Table is sorted by Requested Date by default. To keep working test, we will remove all sorts on the table beforehand + cy.contains('[role="button"]', 'Requested Date').click(); + cy.contains('[role="button"]', 'Access Method').click(); cy.contains('[role="button"]', 'Availability').click(); @@ -123,7 +137,7 @@ describe('Download Status', () => { }); it('date between', () => { - cy.get('input[id="Requested Date filter from"]').type('2020-01-31'); + cy.get('input[id="Requested Date filter from"]').type('2020-01-31 00:00'); const date = new Date(); const month = date.toLocaleString('default', { month: 'long' }); @@ -150,7 +164,7 @@ describe('Download Status', () => { cy.get('input[id="Requested Date filter to"]').should( 'have.value', - date.toISOString().slice(0, 10) + format(date, 'yyyy-MM-dd HH:mm') ); // There should not be results for this time period. @@ -163,7 +177,7 @@ describe('Download Status', () => { cy.get('[aria-rowcount="4"]').should('exist'); cy.get('input[id="Requested Date filter from"]').type( - currDate.toISOString().slice(0, 10) + format(currDate, 'yyyy-MM-dd HH:mm') ); cy.get('[aria-rowindex="1"] [aria-colindex="1"]').should( diff --git a/packages/datagateway-download/package.json b/packages/datagateway-download/package.json index 23d5526ee..65d96d60d 100644 --- a/packages/datagateway-download/package.json +++ b/packages/datagateway-download/package.json @@ -1,16 +1,17 @@ { "name": "datagateway-download", - "version": "1.0.0", + "version": "1.1.0", "private": true, "dependencies": { "@material-ui/core": "^4.11.3", "@material-ui/icons": "^4.11.2", + "@material-ui/lab": "^4.0.0-alpha.58", "@types/jest": "^27.4.0", "@types/node": "^17.0.17", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", "axios": "^0.26.0", - "datagateway-common": "1.0.0", + "datagateway-common": "^1.1.0", "date-fns": "^2.28.0", "date-fns-tz": "^1.1.6", "history": "^4.10.1", @@ -19,18 +20,21 @@ "i18next-http-backend": "^1.3.2", "lodash.chunk": "^4.2.0", "loglevel": "^1.8.0", + "p-limit": "3.1.0", "react": "^16.13.1", "react-dom": "^16.11.0", "react-i18next": "^11.15.4", + "react-query": "^3.18.1", "react-router-dom": "^5.3.0", "react-scripts": "5.0.0", "react-virtualized": "^9.22.3", "single-spa-react": "^4.3.1", - "typescript": "4.5.3", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "typescript": "4.5.3" }, "devDependencies": { "@craco/craco": "^6.4.3", + "@testing-library/react-hooks": "^7.0.1", "@types/jsrsasign": "^9.0.3", "@types/lodash.chunk": "^4.2.6", "@typescript-eslint/eslint-plugin": "^5.5.0", @@ -104,4 +108,4 @@ ], "resetMocks": false } -} \ No newline at end of file +} diff --git a/packages/datagateway-download/public/res/default.json b/packages/datagateway-download/public/res/default.json index 31ea8384a..ba4da5171 100644 --- a/packages/datagateway-download/public/res/default.json +++ b/packages/datagateway-download/public/res/default.json @@ -22,8 +22,8 @@ "size": "Size", "createdAt": "Requested Date", "deleted": "Deleted", - "download_disabled_tooltip": "Instant download not supported for {{transport}} download type", - "download_disabled_button": "Instant download not supported for {{filename}}", + "non_https_download_disabled_tooltip": "Instant download not supported for {{transport}} download type", + "https_download_disabled_tooltip": "This download is not available yet", "delete": "Delete {{filename}}", "download": "Download {{filename}}", "pause": "Pause {{filename}}", @@ -76,6 +76,7 @@ "type": "Type", "size": "Size", "fileCount": "File Count", + "calculating": "Calculating", "remove": "Remove {{name}} from selection", "remove_all": "Remove All", "download": "Download Selection", @@ -84,6 +85,8 @@ "no_selections": "No data selected. <2>Browse or <6>search for data.", "browse_link": "/browse/investigation", "search_link": "/search/data", - "empty_items_warning": "You have selected some empty items - please remove them to proceed with downloading your selection" + "empty_items_error": "You have selected some empty items - please remove them to proceed with downloading your selection", + "file_limit_error": "Too many files - you have exceeded limit of {{fileCountMax}} files - please remove some files", + "size_limit_error": "Too much data - you have exceeded limit of {{totalSizeMax}} - please remove some files" } } \ No newline at end of file diff --git a/packages/datagateway-download/server/e2e-test-server.js b/packages/datagateway-download/server/e2e-test-server.js index b233b56a1..d8b59ed6b 100644 --- a/packages/datagateway-download/server/e2e-test-server.js +++ b/packages/datagateway-download/server/e2e-test-server.js @@ -4,15 +4,15 @@ var serveStatic = require('serve-static'); var app = express(); +app.get('/datagateway-download-settings.json', function (req, res) { + res.sendFile(path.resolve('./server/e2e-settings.json')); +}); + app.use( serveStatic(path.resolve('./build'), { index: ['index.html', 'index.htm'] }) ); -app.get('/datagateway-download-settings.json', function(req, res) { - res.sendFile(path.resolve('./server/e2e-settings.json')); -}); - -app.get('/*', function(req, res) { +app.get('/*', function (req, res) { res.sendFile(path.resolve('./build/index.html')); }); diff --git a/packages/datagateway-download/src/App.tsx b/packages/datagateway-download/src/App.tsx index e852a94d0..1058ddbe3 100644 --- a/packages/datagateway-download/src/App.tsx +++ b/packages/datagateway-download/src/App.tsx @@ -16,6 +16,8 @@ import { } from 'datagateway-common'; import { DGThemeProvider } from 'datagateway-common'; import AdminDownloadStatusTable from './downloadStatus/adminDownloadStatusTable.component'; +import { QueryClientProvider, QueryClient } from 'react-query'; +import { ReactQueryDevtools } from 'react-query/devtools'; const generateClassName = createGenerateClassName({ productionPrefix: 'dgwd', @@ -26,6 +28,15 @@ const generateClassName = createGenerateClassName({ process.env.NODE_ENV === 'production' && !process.env.REACT_APP_E2E_TESTING, }); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: true, + staleTime: 300000, + }, + }, +}); + class App extends Component { public constructor(props: unknown) { super(props); @@ -84,22 +95,25 @@ class App extends Component { - Finished loading - } - > - - - - - - - - - - - + + Finished loading + } + > + + + + + + + + + + + + + diff --git a/packages/datagateway-download/src/downloadApi.test.ts b/packages/datagateway-download/src/downloadApi.test.ts index 7e70edad7..c02aee810 100644 --- a/packages/datagateway-download/src/downloadApi.test.ts +++ b/packages/datagateway-download/src/downloadApi.test.ts @@ -1,18 +1,10 @@ import axios from 'axios'; -import { DownloadCartItem, handleICATError } from 'datagateway-common'; +import { handleICATError } from 'datagateway-common'; import { downloadDeleted, downloadPreparedCart, - fetchDownloadCartItems, fetchDownloads, - getCartDatafileCount, - getCartSize, - getDatafileCount, getDownload, - getIsTwoLevel, - getSize, - removeAllDownloadCartItems, - removeDownloadCartItem, submitCart, getDataUrl, fetchAdminDownloads, @@ -57,229 +49,6 @@ describe('Download Cart API functions test', () => { (handleICATError as jest.Mock).mockClear(); }); - describe('fetchDownloadCartItems', () => { - it('returns cartItems upon successful response', async () => { - const downloadCartMockData = { - cartItems: [ - { - entityId: 1, - entityType: 'investigation', - id: 1, - name: 'INVESTIGATION 1', - parentEntities: [], - }, - { - entityId: 2, - entityType: 'dataset', - id: 2, - name: 'DATASET 2', - parentEntities: [], - }, - ], - createdAt: '2019-11-01T15:18:00Z', - facilityName: mockedSettings.facilityName, - id: 1, - updatedAt: '2019-11-01T15:18:00Z', - userName: 'test user', - }; - - axios.get = jest.fn().mockImplementation(() => - Promise.resolve({ - data: downloadCartMockData, - }) - ); - - const returnData = await fetchDownloadCartItems({ - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBe(downloadCartMockData.cartItems); - expect(axios.get).toHaveBeenCalled(); - expect( - axios.get - ).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/cart/${mockedSettings.facilityName}`, - { params: { sessionId: null } } - ); - }); - - it('returns empty array and logs error upon unsuccessful response', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.reject({ - message: 'Test error message', - }) - ); - - const returnData = await fetchDownloadCartItems({ - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toEqual([]); - expect(axios.get).toHaveBeenCalled(); - expect( - axios.get - ).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/cart/${mockedSettings.facilityName}`, - { params: { sessionId: null } } - ); - expect(handleICATError).toHaveBeenCalled(); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); - }); - }); - - describe('removeAllDownloadCartItems', () => { - it('returns nothing upon successful response', async () => { - axios.delete = jest.fn().mockImplementation(() => - Promise.resolve({ - data: { - cartItems: [], - facilityName: mockedSettings.facilityName, - userName: 'test user', - }, - }) - ); - - const returnData = await removeAllDownloadCartItems({ - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBeUndefined(); - expect(axios.delete).toHaveBeenCalled(); - expect( - axios.delete - ).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/cart/${mockedSettings.facilityName}/cartItems`, - { params: { sessionId: null, items: '*' } } - ); - }); - - it('returns empty array and logs error upon unsuccessful response', async () => { - axios.delete = jest.fn().mockImplementation(() => - Promise.reject({ - message: 'Test error message', - }) - ); - - const returnData = await removeAllDownloadCartItems({ - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBeUndefined(); - expect(axios.delete).toHaveBeenCalled(); - expect( - axios.delete - ).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/cart/${mockedSettings.facilityName}/cartItems`, - { params: { sessionId: null, items: '*' } } - ); - expect(handleICATError).toHaveBeenCalled(); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); - }); - }); - - describe('removeDownloadCartItem', () => { - it('returns nothing upon successful response', async () => { - axios.delete = jest.fn().mockImplementation(() => - Promise.resolve({ - data: { - cartItems: [], - facilityName: mockedSettings.facilityName, - userName: 'test user', - }, - }) - ); - - const returnData = await removeDownloadCartItem(1, 'datafile', { - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBeUndefined(); - expect(axios.delete).toHaveBeenCalled(); - expect( - axios.delete - ).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/cart/${mockedSettings.facilityName}/cartItems`, - { params: { sessionId: null, items: 'datafile 1' } } - ); - }); - - it('returns empty array and logs error upon unsuccessful response', async () => { - axios.delete = jest.fn().mockImplementation(() => - Promise.reject({ - message: 'Test error message', - }) - ); - - const returnData = await removeDownloadCartItem(1, 'investigation', { - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBeUndefined(); - expect(axios.delete).toHaveBeenCalled(); - expect( - axios.delete - ).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/cart/${mockedSettings.facilityName}/cartItems`, - { params: { sessionId: null, items: 'investigation 1' } } - ); - expect(handleICATError).toHaveBeenCalled(); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); - }); - }); - - describe('getIsTwoLevel', () => { - it('returns true if IDS is two-level', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.resolve({ - data: true, - }) - ); - - const isTwoLevel = await getIsTwoLevel({ idsUrl: mockedSettings.idsUrl }); - - expect(isTwoLevel).toBe(true); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.idsUrl}/isTwoLevel` - ); - }); - - it('returns false in the event of an error and logs error upon unsuccessful response', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.reject({ - message: 'Test error message', - }) - ); - - const isTwoLevel = await getIsTwoLevel({ idsUrl: mockedSettings.idsUrl }); - - expect(isTwoLevel).toBe(false); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.idsUrl}/isTwoLevel` - ); - expect(handleICATError).toHaveBeenCalled(); - expect(handleICATError).toHaveBeenCalledWith( - { - message: 'Test error message', - }, - false - ); - }); - }); - describe('submitCart', () => { it('returns the downloadId after the submitting cart', async () => { axios.post = jest.fn().mockImplementation(() => { @@ -459,404 +228,6 @@ describe('Download Cart API functions test', () => { expect(document.body.appendChild).toHaveBeenCalledWith(link); }); }); - - describe('getSize', () => { - it('returns a number upon successful response for datafile entityType', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.resolve({ - data: { - id: 1, - name: 'test datafile', - fileSize: 1, - }, - }) - ); - - const returnData = await getSize(1, 'datafile', { - facilityName: mockedSettings.facilityName, - apiUrl: mockedSettings.apiUrl, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBe(1); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.apiUrl}/datafiles/1`, - { - headers: { Authorization: 'Bearer null' }, - } - ); - }); - - it('returns -1 and logs error upon unsuccessful response for datafile entityType', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.reject({ - message: 'Test error message', - }) - ); - - const returnData = await getSize(1, 'datafile', { - facilityName: mockedSettings.facilityName, - apiUrl: mockedSettings.apiUrl, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBe(-1); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.apiUrl}/datafiles/1`, - { - headers: { Authorization: 'Bearer null' }, - } - ); - expect(handleICATError).toHaveBeenCalled(); - expect(handleICATError).toHaveBeenCalledWith( - { - message: 'Test error message', - }, - false - ); - }); - - it('returns a number upon successful response for non-datafile entityType', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.resolve({ - data: 2, - }) - ); - - const returnData = await getSize(1, 'dataset', { - facilityName: mockedSettings.facilityName, - apiUrl: mockedSettings.apiUrl, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBe(2); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/getSize`, - { - params: { - sessionId: null, - facilityName: mockedSettings.facilityName, - entityType: 'dataset', - entityId: 1, - }, - } - ); - }); - - it('returns -1 and logs error upon unsuccessful response for non-datafile entityType', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.reject({ - message: 'Test error message', - }) - ); - - const returnData = await getSize(1, 'investigation', { - facilityName: mockedSettings.facilityName, - apiUrl: mockedSettings.apiUrl, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBe(-1); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/getSize`, - { - params: { - sessionId: null, - facilityName: mockedSettings.facilityName, - entityType: 'investigation', - entityId: 1, - }, - } - ); - expect(handleICATError).toHaveBeenCalled(); - expect(handleICATError).toHaveBeenCalledWith( - { - message: 'Test error message', - }, - false - ); - }); - }); - - describe('getDatafileCount', () => { - it('returns 1 upon request for datafile entityType', async () => { - const returnData = await getDatafileCount(1, 'datafile', { - apiUrl: mockedSettings.apiUrl, - }); - - expect(returnData).toBe(1); - }); - - it('returns a number upon successful response for dataset entityType', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.resolve({ - data: 2, - }) - ); - - const returnData = await getDatafileCount(1, 'dataset', { - apiUrl: mockedSettings.apiUrl, - }); - - expect(returnData).toBe(2); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.apiUrl}/datafiles/count`, - { - params: { - where: { - 'dataset.id': { - eq: 1, - }, - }, - include: '"dataset"', - }, - headers: { Authorization: 'Bearer null' }, - } - ); - }); - - it('returns -1 and logs error upon unsuccessful response for dataset entityType', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.reject({ - message: 'Test error message', - }) - ); - - const returnData = await getDatafileCount(1, 'dataset', { - apiUrl: mockedSettings.apiUrl, - }); - - expect(returnData).toBe(-1); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.apiUrl}/datafiles/count`, - { - params: { - where: { - 'dataset.id': { - eq: 1, - }, - }, - include: '"dataset"', - }, - headers: { Authorization: 'Bearer null' }, - } - ); - expect(handleICATError).toHaveBeenCalled(); - expect(handleICATError).toHaveBeenCalledWith( - { - message: 'Test error message', - }, - false - ); - }); - - it('returns a number upon successful response for investigation entityType', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.resolve({ - data: 5, - }) - ); - - const returnData = await getDatafileCount(2, 'investigation', { - apiUrl: mockedSettings.apiUrl, - }); - - expect(returnData).toBe(5); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.apiUrl}/datafiles/count`, - { - params: { - include: '{"dataset": "investigation"}', - where: { - 'dataset.investigation.id': { - eq: 2, - }, - }, - }, - headers: { Authorization: 'Bearer null' }, - } - ); - }); - - it('returns -1 and logs error upon unsuccessful response for investigation entityType', async () => { - axios.get = jest.fn().mockImplementation(() => - Promise.reject({ - message: 'Test error message', - }) - ); - - const returnData = await getDatafileCount(2, 'investigation', { - apiUrl: mockedSettings.apiUrl, - }); - - expect(returnData).toBe(-1); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.apiUrl}/datafiles/count`, - { - params: { - include: '{"dataset": "investigation"}', - where: { - 'dataset.investigation.id': { - eq: 2, - }, - }, - }, - headers: { Authorization: 'Bearer null' }, - } - ); - expect(handleICATError).toHaveBeenCalled(); - expect(handleICATError).toHaveBeenCalledWith( - { - message: 'Test error message', - }, - false - ); - }); - }); - - describe('getCartDatafileCount', () => { - it('returns an accurate count of a given cart', async () => { - axios.get = jest - .fn() - .mockImplementation(() => - Promise.resolve({ - data: 1, - }) - ) - .mockImplementationOnce(() => - Promise.reject({ - message: 'simulating a failed response', - }) - ); - - const cartItems: DownloadCartItem[] = [ - { - entityId: 1, - entityType: 'investigation', - id: 1, - name: 'INVESTIGATION 1', - parentEntities: [], - }, - { - entityId: 2, - entityType: 'investigation', - id: 2, - name: 'INVESTIGATION 2', - parentEntities: [], - }, - { - entityId: 3, - entityType: 'dataset', - id: 3, - name: 'DATASET 1', - parentEntities: [], - }, - { - entityId: 4, - entityType: 'datafile', - id: 4, - name: 'DATAFILE 1', - parentEntities: [], - }, - ]; - - const returnData = await getCartDatafileCount(cartItems, { - apiUrl: mockedSettings.apiUrl, - }); - - expect(returnData).toBe(3); - expect(axios.get).toHaveBeenCalledTimes(3); - expect(handleICATError).toHaveBeenCalled(); - expect(handleICATError).toHaveBeenCalledWith( - { - message: 'simulating a failed response', - }, - false - ); - }); - }); - - describe('getCartSize', () => { - it('returns an accurate size of a given cart', async () => { - axios.get = jest - .fn() - .mockImplementation((path) => { - if (path.includes('datafiles/')) { - return Promise.resolve({ - data: { - id: 1, - name: 'test datafile', - fileSize: 1, - }, - }); - } else { - return Promise.resolve({ - data: 1, - }); - } - }) - .mockImplementationOnce(() => - Promise.reject({ - message: 'simulating a failed response', - }) - ); - - const cartItems: DownloadCartItem[] = [ - { - entityId: 1, - entityType: 'investigation', - id: 1, - name: 'INVESTIGATION 1', - parentEntities: [], - }, - { - entityId: 2, - entityType: 'investigation', - id: 2, - name: 'INVESTIGATION 2', - parentEntities: [], - }, - { - entityId: 3, - entityType: 'dataset', - id: 3, - name: 'DATASET 1', - parentEntities: [], - }, - { - entityId: 4, - entityType: 'datafile', - id: 4, - name: 'DATAFILE 1', - parentEntities: [], - }, - ]; - - const returnData = await getCartSize(cartItems, { - facilityName: mockedSettings.facilityName, - apiUrl: mockedSettings.apiUrl, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBe(3); - expect(axios.get).toHaveBeenCalledTimes(4); - expect(handleICATError).toHaveBeenCalled(); - expect(handleICATError).toHaveBeenCalledWith( - { - message: 'simulating a failed response', - }, - false - ); - }); - }); }); describe('Download Status API functions test', () => { diff --git a/packages/datagateway-download/src/downloadApi.ts b/packages/datagateway-download/src/downloadApi.ts index 6a110b13b..d089388aa 100644 --- a/packages/datagateway-download/src/downloadApi.ts +++ b/packages/datagateway-download/src/downloadApi.ts @@ -1,40 +1,15 @@ import axios, { AxiosResponse } from 'axios'; import * as log from 'loglevel'; import { - DownloadCart, SubmitCart, - DownloadCartItem, Datafile, Download, readSciGatewayToken, handleICATError, + DownloadCart, + DownloadCartItem, } from 'datagateway-common'; -export const fetchDownloadCartItems: (settings: { - facilityName: string; - downloadApiUrl: string; -}) => Promise = (settings: { - facilityName: string; - downloadApiUrl: string; -}) => { - return axios - .get( - `${settings.downloadApiUrl}/user/cart/${settings.facilityName}`, - { - params: { - sessionId: readSciGatewayToken().sessionId, - }, - } - ) - .then((response) => { - return response.data.cartItems; - }) - .catch((error) => { - handleICATError(error); - return []; - }); -}; - export const removeAllDownloadCartItems: (settings: { facilityName: string; downloadApiUrl: string; @@ -54,39 +29,27 @@ export const removeAllDownloadCartItems: (settings: { ) .then(() => { // do nothing - }) - .catch(handleICATError); + }); }; -export const removeDownloadCartItem: ( - entityId: number, - entityType: string, - settings: { - facilityName: string; - downloadApiUrl: string; - } -) => Promise = ( - entityId: number, - entityType: string, - settings: { - facilityName: string; - downloadApiUrl: string; - } -) => { +export const removeFromCart = ( + entityType: 'investigation' | 'dataset' | 'datafile', + entityIds: number[], + config: { facilityName: string; downloadApiUrl: string } +): Promise => { + const { facilityName, downloadApiUrl } = config; + return axios - .delete( - `${settings.downloadApiUrl}/user/cart/${settings.facilityName}/cartItems`, + .delete( + `${downloadApiUrl}/user/cart/${facilityName}/cartItems`, { params: { sessionId: readSciGatewayToken().sessionId, - items: `${entityType} ${entityId}`, + items: `${entityType} ${entityIds.join(`, ${entityType} `)}`, }, } ) - .then(() => { - // do nothing - }) - .catch(handleICATError); + .then((response) => response.data.cartItems); }; export const getIsTwoLevel: (settings: { @@ -96,10 +59,6 @@ export const getIsTwoLevel: (settings: { .get(`${settings.idsUrl}/isTwoLevel`) .then((response) => { return response.data; - }) - .catch((error) => { - handleICATError(error, false); - return false; }); }; @@ -412,10 +371,6 @@ export const getSize: ( .then((response) => { const size = response.data['fileSize'] as number; return size; - }) - .catch((error) => { - handleICATError(error, false); - return -1; }); } else { return axios @@ -429,10 +384,6 @@ export const getSize: ( }) .then((response) => { return response.data; - }) - .catch((error) => { - handleICATError(error, false); - return -1; }); } }; @@ -447,7 +398,12 @@ export const getDatafileCount: ( settings: { apiUrl: string } ) => { if (entityType === 'datafile') { - return Promise.resolve(1); + // need to do this in a setTimeout to ensure it doesn't block the main thread + return new Promise((resolve) => + window.setTimeout(() => { + resolve(1); + }, 0) + ); } else if (entityType === 'dataset') { return axios .get(`${settings.apiUrl}/datafiles/count`, { @@ -457,7 +413,6 @@ export const getDatafileCount: ( eq: entityId, }, }, - include: '"dataset"', }, headers: { Authorization: `Bearer ${readSciGatewayToken().sessionId}`, @@ -465,16 +420,11 @@ export const getDatafileCount: ( }) .then((response) => { return response.data; - }) - .catch((error) => { - handleICATError(error, false); - return -1; }); } else { return axios .get(`${settings.apiUrl}/datafiles/count`, { params: { - include: '{"dataset": "investigation"}', where: { 'dataset.investigation.id': { eq: entityId, @@ -487,74 +437,10 @@ export const getDatafileCount: ( }) .then((response) => { return response.data; - }) - .catch((error) => { - handleICATError(error, false); - return -1; }); } }; -export const getCartDatafileCount: ( - cartItems: DownloadCartItem[], - settings: { apiUrl: string } -) => Promise = ( - cartItems: DownloadCartItem[], - settings: { apiUrl: string } -) => { - const getDatafileCountPromises: Promise[] = []; - cartItems.forEach((cartItem) => - getDatafileCountPromises.push( - getDatafileCount(cartItem.entityId, cartItem.entityType, { - apiUrl: settings.apiUrl, - }) - ) - ); - - return Promise.all(getDatafileCountPromises).then((counts) => - counts.reduce( - (accumulator, nextCount) => - nextCount > -1 ? accumulator + nextCount : accumulator, - 0 - ) - ); -}; - -export const getCartSize: ( - cartItems: DownloadCartItem[], - settings: { - facilityName: string; - apiUrl: string; - downloadApiUrl: string; - } -) => Promise = ( - cartItems: DownloadCartItem[], - settings: { - facilityName: string; - apiUrl: string; - downloadApiUrl: string; - } -) => { - const getSizePromises: Promise[] = []; - cartItems.forEach((cartItem) => - getSizePromises.push( - getSize(cartItem.entityId, cartItem.entityType, { - facilityName: settings.facilityName, - apiUrl: settings.apiUrl, - downloadApiUrl: settings.downloadApiUrl, - }) - ) - ); - - return Promise.all(getSizePromises).then((sizes) => - sizes.reduce( - (accumulator, nextSize) => - nextSize > -1 ? accumulator + nextSize : accumulator, - 0 - ) - ); -}; - export const getDataUrl = ( preparedId: string, fileName: string, diff --git a/packages/datagateway-download/src/downloadApiHooks.test.tsx b/packages/datagateway-download/src/downloadApiHooks.test.tsx new file mode 100644 index 000000000..3a52cd0df --- /dev/null +++ b/packages/datagateway-download/src/downloadApiHooks.test.tsx @@ -0,0 +1,545 @@ +import axios from 'axios'; +import { DownloadCartItem, handleICATError } from 'datagateway-common'; +import { + useCart, + useRemoveAllFromCart, + useRemoveEntityFromCart, + useIsTwoLevel, + useSizes, + useDatafileCounts, +} from './downloadApiHooks'; +import { renderHook, WrapperComponent } from '@testing-library/react-hooks'; +import React from 'react'; +import { createMemoryHistory } from 'history'; +import { QueryClient, QueryClientProvider, setLogger } from 'react-query'; +import { Router } from 'react-router-dom'; +import { DownloadSettingsContext } from './ConfigProvider'; + +jest.mock('datagateway-common', () => { + const originalModule = jest.requireActual('datagateway-common'); + + return { + __esModule: true, + ...originalModule, + handleICATError: jest.fn(), + retryICATErrors: jest.fn().mockReturnValue(false), + }; +}); + +// Create our mocked datagateway-download mockedSettings file. +const mockedSettings = { + facilityName: 'LILS', + apiUrl: 'https://example.com/api', + downloadApiUrl: 'https://example.com/downloadApi', + idsUrl: 'https://example.com/ids', + fileCountMax: 5000, + totalSizeMax: 1000000000000, + accessMethods: { + https: { + idsUrl: 'https://example.com/ids', + displayName: 'HTTPS', + description: 'Example description for HTTPS access method.', + }, + globus: { + idsUrl: 'https://example.com/ids', + displayName: 'Globus', + description: 'Example description for Globus access method.', + }, + }, +}; + +// silence react-query errors +setLogger({ + log: console.log, + warn: console.warn, + error: jest.fn(), +}); + +const createTestQueryClient = (): QueryClient => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + +const createReactQueryWrapper = (): WrapperComponent => { + const testQueryClient = createTestQueryClient(); + const history = createMemoryHistory(); + + const wrapper: WrapperComponent = ({ children }) => ( + + + + {children} + + + + ); + return wrapper; +}; + +describe('Download Cart API react-query hooks test', () => { + afterEach(() => { + (handleICATError as jest.Mock).mockClear(); + }); + + describe('useCart', () => { + it('sends axios request to fetch cart and returns successful response', async () => { + const downloadCartMockData = { + cartItems: [ + { + entityId: 1, + entityType: 'investigation', + id: 1, + name: 'INVESTIGATION 1', + parentEntities: [], + }, + { + entityId: 2, + entityType: 'dataset', + id: 2, + name: 'DATASET 2', + parentEntities: [], + }, + ], + createdAt: '2019-11-01T15:18:00Z', + facilityName: mockedSettings.facilityName, + id: 1, + updatedAt: '2019-11-01T15:18:00Z', + userName: 'test user', + }; + + axios.get = jest.fn().mockResolvedValue({ + data: downloadCartMockData, + }); + + const { result, waitFor } = renderHook(() => useCart(), { + wrapper: createReactQueryWrapper(), + }); + + await waitFor(() => result.current.isSuccess); + + expect(axios.get).toHaveBeenCalledWith( + 'https://example.com/downloadApi/user/cart/LILS', + { + params: { + sessionId: null, + }, + } + ); + expect(result.current.data).toEqual(downloadCartMockData.cartItems); + }); + + it('sends axios request to fetch cart and calls handleICATError on failure', async () => { + axios.get = jest.fn().mockRejectedValue({ + message: 'Test error message', + }); + + const { result, waitFor } = renderHook(() => useCart(), { + wrapper: createReactQueryWrapper(), + }); + + await waitFor(() => result.current.isError); + + expect(handleICATError).toHaveBeenCalledWith({ + message: 'Test error message', + }); + }); + }); + + describe('useRemoveAllFromCart', () => { + it('returns nothing upon successful response', async () => { + axios.delete = jest.fn().mockImplementation(() => + Promise.resolve({ + data: { + cartItems: [], + facilityName: mockedSettings.facilityName, + userName: 'test user', + }, + }) + ); + + const { result, waitFor } = renderHook(() => useRemoveAllFromCart(), { + wrapper: createReactQueryWrapper(), + }); + + expect(axios.delete).not.toHaveBeenCalled(); + expect(result.current.isIdle).toBe(true); + + result.current.mutate(); + + await waitFor(() => result.current.isSuccess); + + expect(result.current.data).toBeUndefined(); + expect(axios.delete).toHaveBeenCalled(); + expect( + axios.delete + ).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/user/cart/${mockedSettings.facilityName}/cartItems`, + { params: { sessionId: null, items: '*' } } + ); + }); + + it('logs error upon unsuccessful response, with a retry on code 431', async () => { + axios.delete = jest + .fn() + .mockImplementationOnce(() => + Promise.reject({ + code: '431', + message: 'Test 431 error message', + }) + ) + .mockImplementation(() => + Promise.reject({ + message: 'Test error message', + }) + ); + + const { result, waitFor } = renderHook(() => useRemoveAllFromCart(), { + wrapper: createReactQueryWrapper(), + }); + + result.current.mutate(); + + await waitFor(() => result.current.isError, { timeout: 2000 }); + + expect( + axios.delete + ).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/user/cart/${mockedSettings.facilityName}/cartItems`, + { params: { sessionId: null, items: '*' } } + ); + expect(result.current.failureCount).toBe(2); + expect(handleICATError).toHaveBeenCalledTimes(1); + expect(handleICATError).toHaveBeenCalledWith({ + message: 'Test error message', + }); + }); + }); + + describe('useRemoveEntityFromCart', () => { + it('returns empty array upon successful response', async () => { + axios.delete = jest.fn().mockImplementation(() => + Promise.resolve({ + data: { + cartItems: [], + facilityName: mockedSettings.facilityName, + userName: 'test user', + }, + }) + ); + + const { result, waitFor } = renderHook(() => useRemoveEntityFromCart(), { + wrapper: createReactQueryWrapper(), + }); + + expect(axios.delete).not.toHaveBeenCalled(); + expect(result.current.isIdle).toBe(true); + + result.current.mutate({ entityId: 1, entityType: 'datafile' }); + + await waitFor(() => result.current.isSuccess); + + expect(result.current.data).toEqual([]); + expect(axios.delete).toHaveBeenCalled(); + expect( + axios.delete + ).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/user/cart/${mockedSettings.facilityName}/cartItems`, + { params: { sessionId: null, items: 'datafile 1' } } + ); + }); + + it('logs error upon unsuccessful response', async () => { + axios.delete = jest + .fn() + .mockImplementationOnce(() => + Promise.reject({ + code: '431', + message: 'Test 431 error message', + }) + ) + .mockImplementation(() => + Promise.reject({ + message: 'Test error message', + }) + ); + + const { result, waitFor } = renderHook(() => useRemoveEntityFromCart(), { + wrapper: createReactQueryWrapper(), + }); + + result.current.mutate({ entityId: 1, entityType: 'investigation' }); + + await waitFor(() => result.current.isError, { timeout: 2000 }); + + expect( + axios.delete + ).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/user/cart/${mockedSettings.facilityName}/cartItems`, + { params: { sessionId: null, items: 'investigation 1' } } + ); + expect(result.current.failureCount).toBe(2); + expect(handleICATError).toHaveBeenCalledTimes(1); + expect(handleICATError).toHaveBeenCalledWith({ + message: 'Test error message', + }); + }); + }); + + describe('useIsTwoLevel', () => { + it('returns true if IDS is two-level', async () => { + axios.get = jest.fn().mockImplementation(() => + Promise.resolve({ + data: true, + }) + ); + + const { result, waitFor } = renderHook(() => useIsTwoLevel(), { + wrapper: createReactQueryWrapper(), + }); + + await waitFor(() => result.current.isSuccess); + + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.idsUrl}/isTwoLevel` + ); + expect(result.current.data).toEqual(true); + }); + + it('returns false in the event of an error and logs error upon unsuccessful response', async () => { + axios.get = jest.fn().mockImplementation(() => + Promise.reject({ + message: 'Test error message', + }) + ); + + const { result, waitFor } = renderHook(() => useIsTwoLevel(), { + wrapper: createReactQueryWrapper(), + }); + + await waitFor(() => result.current.isError); + + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.idsUrl}/isTwoLevel` + ); + expect(handleICATError).toHaveBeenCalledWith({ + message: 'Test error message', + }); + }); + }); + + describe('useSizes', () => { + it('returns the sizes of all the items in a cart', async () => { + axios.get = jest + .fn() + .mockImplementation((path) => { + if (path.includes('datafiles/')) { + return Promise.resolve({ + data: { + id: 1, + name: 'test datafile', + fileSize: 1, + }, + }); + } else { + return Promise.resolve({ + data: 1, + }); + } + }) + .mockImplementationOnce(() => + Promise.reject({ + message: 'simulating a failed response', + }) + ); + + const cartItems: DownloadCartItem[] = [ + { + entityId: 1, + entityType: 'investigation', + id: 1, + name: 'INVESTIGATION 1', + parentEntities: [], + }, + { + entityId: 2, + entityType: 'dataset', + id: 2, + name: 'DATASET 2', + parentEntities: [], + }, + { + entityId: 3, + entityType: 'datafile', + id: 3, + name: 'DATAFILE 1', + parentEntities: [], + }, + { + entityId: 4, + entityType: 'investigation', + id: 4, + name: 'INVESTIGATION 1', + parentEntities: [], + }, + ]; + + const { result, waitFor } = renderHook(() => useSizes(cartItems), { + wrapper: createReactQueryWrapper(), + }); + + await waitFor(() => + result.current.every((query) => query.isSuccess || query.isError) + ); + + expect(result.current.map((query) => query.data)).toEqual([ + undefined, + 1, + 1, + 1, + ]); + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/user/getSize`, + { + params: { + sessionId: null, + facilityName: mockedSettings.facilityName, + entityType: 'investigation', + entityId: 1, + }, + } + ); + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/user/getSize`, + { + params: { + sessionId: null, + facilityName: mockedSettings.facilityName, + entityType: 'dataset', + entityId: 2, + }, + } + ); + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.apiUrl}/datafiles/${3}`, + { + headers: { + Authorization: 'Bearer null', + }, + } + ); + expect(handleICATError).toHaveBeenCalledWith( + { + message: 'simulating a failed response', + }, + false + ); + }); + }); + + describe('useDatafileCounts', () => { + it('returns the counts of all the items in a cart', async () => { + axios.get = jest + .fn() + .mockImplementation(() => + Promise.resolve({ + data: 1, + }) + ) + .mockImplementationOnce(() => + Promise.reject({ + message: 'simulating a failed response', + }) + ); + + const cartItems: DownloadCartItem[] = [ + { + entityId: 1, + entityType: 'investigation', + id: 1, + name: 'INVESTIGATION 1', + parentEntities: [], + }, + { + entityId: 2, + entityType: 'investigation', + id: 2, + name: 'INVESTIGATION 2', + parentEntities: [], + }, + { + entityId: 3, + entityType: 'dataset', + id: 3, + name: 'DATASET 1', + parentEntities: [], + }, + { + entityId: 4, + entityType: 'datafile', + id: 4, + name: 'DATAFILE 1', + parentEntities: [], + }, + ]; + + const { result, waitFor } = renderHook( + () => useDatafileCounts(cartItems), + { + wrapper: createReactQueryWrapper(), + } + ); + + await waitFor(() => + result.current.every((query) => query.isSuccess || query.isError) + ); + + expect(result.current.map((query) => query.data)).toEqual([ + undefined, + 1, + 1, + 1, + ]); + expect(axios.get).toHaveBeenCalledTimes(3); + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.apiUrl}/datafiles/count`, + { + params: { + where: { + 'dataset.investigation.id': { + eq: 2, + }, + }, + }, + headers: { + Authorization: 'Bearer null', + }, + } + ); + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.apiUrl}/datafiles/count`, + { + params: { + where: { + 'dataset.id': { + eq: 3, + }, + }, + }, + headers: { + Authorization: 'Bearer null', + }, + } + ); + expect(handleICATError).toHaveBeenCalledWith( + { + message: 'simulating a failed response', + }, + false + ); + }); + }); +}); diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts new file mode 100644 index 000000000..a25f68e84 --- /dev/null +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -0,0 +1,252 @@ +import React from 'react'; +import { AxiosError } from 'axios'; +import { + DownloadCartItem, + handleICATError, + fetchDownloadCart, + retryICATErrors, +} from 'datagateway-common'; +import { DownloadSettingsContext } from './ConfigProvider'; +import { + UseQueryResult, + useQuery, + useMutation, + UseMutationResult, + useQueryClient, + UseQueryOptions, + useQueries, +} from 'react-query'; +import pLimit from 'p-limit'; +import { + removeAllDownloadCartItems, + removeFromCart, + getSize, + getDatafileCount, + getIsTwoLevel, +} from './downloadApi'; + +export const useCart = (): UseQueryResult => { + const settings = React.useContext(DownloadSettingsContext); + const { facilityName, downloadApiUrl } = settings; + return useQuery( + 'cart', + () => + fetchDownloadCart({ + facilityName, + downloadApiUrl, + }), + { + onError: (error) => { + handleICATError(error); + }, + retry: retryICATErrors, + staleTime: 0, + } + ); +}; + +export const useRemoveAllFromCart = (): UseMutationResult< + void, + AxiosError, + void +> => { + const queryClient = useQueryClient(); + const settings = React.useContext(DownloadSettingsContext); + const { facilityName, downloadApiUrl } = settings; + + return useMutation( + () => removeAllDownloadCartItems({ facilityName, downloadApiUrl }), + { + onSuccess: (data) => { + queryClient.setQueryData('cart', []); + }, + retry: (failureCount, error) => { + // if we get 431 we know this is an intermittent error so retry + if (error.code === '431' && failureCount < 3) { + return true; + } else { + return false; + } + }, + onError: (error) => { + handleICATError(error); + }, + } + ); +}; + +export const useRemoveEntityFromCart = (): UseMutationResult< + DownloadCartItem[], + AxiosError, + { entityId: number; entityType: 'investigation' | 'dataset' | 'datafile' } +> => { + const queryClient = useQueryClient(); + const settings = React.useContext(DownloadSettingsContext); + const { facilityName, downloadApiUrl } = settings; + + return useMutation( + ({ entityId, entityType }) => + removeFromCart(entityType, [entityId], { + facilityName, + downloadApiUrl, + }), + { + onSuccess: (data) => { + queryClient.setQueryData('cart', data); + }, + retry: (failureCount, error) => { + // if we get 431 we know this is an intermittent error so retry + if (error.code === '431' && failureCount < 3) { + return true; + } else { + return false; + } + }, + onError: (error) => { + handleICATError(error); + }, + } + ); +}; + +export const useIsTwoLevel = (): UseQueryResult => { + const settings = React.useContext(DownloadSettingsContext); + const { idsUrl } = settings; + return useQuery('isTwoLevel', () => getIsTwoLevel({ idsUrl }), { + onError: (error) => { + handleICATError(error); + }, + retry: retryICATErrors, + staleTime: Infinity, + }); +}; + +// TODO: refactor rest of dg-download to use react-query +// export const useSubmitCart = (): UseMutationResult< +// number, +// AxiosError, +// { +// transport: string; +// emailAddress: string; +// fileName: string; +// zipType?: 'ZIP' | 'ZIP_AND_COMPRESS'; +// } +// > => { +// const queryClient = useQueryClient(); +// const settings = React.useContext(DownloadSettingsContext); +// const { facilityName, downloadApiUrl } = settings; + +// return useMutation( +// ({ transport, emailAddress, fileName, zipType }) => +// submitCart( +// transport, +// emailAddress, +// fileName, +// { +// facilityName, +// downloadApiUrl, +// }, +// zipType +// ), +// { +// onSuccess: (data) => { +// queryClient.setQueryData('cart', data); +// }, +// onError: (error) => { +// handleICATError(error); +// }, +// } +// ); +// }; + +const sizesLimit = pLimit(20); + +export const useSizes = ( + data: DownloadCartItem[] | undefined +): UseQueryResult[] => { + const settings = React.useContext(DownloadSettingsContext); + const { facilityName, apiUrl, downloadApiUrl } = settings; + + const queryConfigs: UseQueryOptions< + number, + AxiosError, + number, + ['size', number] + >[] = React.useMemo(() => { + return data + ? data.map((cartItem) => { + const { entityId, entityType } = cartItem; + return { + queryKey: ['size', entityId], + queryFn: () => + sizesLimit(getSize, entityId, entityType, { + facilityName, + apiUrl, + downloadApiUrl, + }), + onError: (error) => { + handleICATError(error, false); + }, + retry: retryICATErrors, + staleTime: Infinity, + }; + }) + : []; + }, [data, facilityName, apiUrl, downloadApiUrl]); + + // useQueries doesn't allow us to specify type info, so ignore this line + // since we strongly type the queries object anyway + // we also need to prettier-ignore to make sure we don't wrap onto next line + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // prettier-ignore + const queries: UseQueryResult[] = useQueries(queryConfigs); + + return queries; +}; + +const datafileCountslimit = pLimit(20); + +export const useDatafileCounts = ( + data: DownloadCartItem[] | undefined +): UseQueryResult[] => { + const settings = React.useContext(DownloadSettingsContext); + const { apiUrl } = settings; + + const queryConfigs: UseQueryOptions< + number, + AxiosError, + number, + ['datafileCount', number] + >[] = React.useMemo(() => { + return data + ? data.map((cartItem) => { + const { entityId, entityType } = cartItem; + return { + queryKey: ['datafileCount', entityId], + queryFn: () => + datafileCountslimit(getDatafileCount, entityId, entityType, { + apiUrl, + }), + onError: (error) => { + handleICATError(error, false); + }, + retry: retryICATErrors, + staleTime: Infinity, + enabled: entityType !== 'datafile', + initialData: entityType === 'datafile' ? 1 : undefined, + }; + }) + : []; + }, [data, apiUrl]); + + // useQueries doesn't allow us to specify type info, so ignore this line + // since we strongly type the queries object anyway + // we also need to prettier-ignore to make sure we don't wrap onto next line + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // prettier-ignore + const queries: UseQueryResult[] = useQueries(queryConfigs); + + return queries; +}; diff --git a/packages/datagateway-download/src/downloadCart/__snapshots__/downloadCartTable.component.test.tsx.snap b/packages/datagateway-download/src/downloadCart/__snapshots__/downloadCartTable.component.test.tsx.snap deleted file mode 100644 index ade849f68..000000000 --- a/packages/datagateway-download/src/downloadCart/__snapshots__/downloadCartTable.component.test.tsx.snap +++ /dev/null @@ -1,92 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Download cart table component renders correctly 1`] = ` -
- - - - - - No data selected. - - - Browse - - - or - - - search - - - for data. - - - - - -
-`; diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx index 601d58660..6edd387be 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx @@ -1,61 +1,53 @@ import React from 'react'; -import { createShallow, createMount } from '@material-ui/core/test-utils'; +import { createMount } from '@material-ui/core/test-utils'; import DownloadCartTable from './downloadCartTable.component'; -import { DownloadCartItem } from 'datagateway-common'; +import { DownloadCartItem, fetchDownloadCart } from 'datagateway-common'; import { flushPromises } from '../setupTests'; -import { - fetchDownloadCartItems, - removeAllDownloadCartItems, - removeDownloadCartItem, - getSize, - getDatafileCount, -} from '../downloadApi'; import { act } from 'react-dom/test-utils'; import { DownloadSettingsContext } from '../ConfigProvider'; import { Router } from 'react-router-dom'; import { ReactWrapper } from 'enzyme'; import { createMemoryHistory } from 'history'; +import { QueryClientProvider, QueryClient } from 'react-query'; +import { + getDatafileCount, + getSize, + removeAllDownloadCartItems, + removeFromCart, +} from '../downloadApi'; + +jest.mock('datagateway-common', () => { + const originalModule = jest.requireActual('datagateway-common'); + + return { + __esModule: true, + ...originalModule, + fetchDownloadCart: jest.fn(), + }; +}); -jest.mock('../downloadApi'); +jest.mock('../downloadApi', () => { + const originalModule = jest.requireActual('../downloadApi'); + + return { + ...originalModule, + removeAllDownloadCartItems: jest.fn(), + getSize: jest.fn(), + getDatafileCount: jest.fn(), + getIsTwoLevel: jest.fn().mockResolvedValue(true), + removeFromCart: jest.fn(), + }; +}); describe('Download cart table component', () => { - let shallow; let mount; let history; + let queryClient; - const cartItems: DownloadCartItem[] = [ - { - entityId: 1, - entityType: 'investigation', - id: 1, - name: 'INVESTIGATION 1', - parentEntities: [], - }, - { - entityId: 2, - entityType: 'investigation', - id: 2, - name: 'INVESTIGATION 2', - parentEntities: [], - }, - { - entityId: 3, - entityType: 'dataset', - id: 3, - name: 'DATASET 1', - parentEntities: [], - }, - { - entityId: 4, - entityType: 'datafile', - id: 4, - name: 'DATAFILE 1', - parentEntities: [], - }, - ]; + let cartItems: DownloadCartItem[] = []; // Create our mocked datagateway-download settings file. - const mockedSettings = { + let mockedSettings = { facilityName: 'LILS', apiUrl: 'https://example.com/api', downloadApiUrl: 'https://example.com/downloadApi', @@ -77,11 +69,14 @@ describe('Download cart table component', () => { }; const createWrapper = (): ReactWrapper => { + queryClient = new QueryClient(); return mount(
- + + +
@@ -89,44 +84,72 @@ describe('Download cart table component', () => { }; beforeEach(() => { - shallow = createShallow({ untilSelector: 'div' }); mount = createMount(); history = createMemoryHistory(); - (fetchDownloadCartItems as jest.Mock).mockImplementation(() => - Promise.resolve(cartItems) - ); - (removeAllDownloadCartItems as jest.Mock).mockImplementation(() => - Promise.resolve() - ); - (removeDownloadCartItem as jest.Mock).mockImplementation(() => - Promise.resolve() - ); - (getSize as jest.Mock).mockImplementation(() => Promise.resolve(1)); - (getDatafileCount as jest.Mock).mockImplementation(() => - Promise.resolve(7) - ); + cartItems = [ + { + entityId: 1, + entityType: 'investigation', + id: 1, + name: 'INVESTIGATION 1', + parentEntities: [], + }, + { + entityId: 2, + entityType: 'investigation', + id: 2, + name: 'INVESTIGATION 2', + parentEntities: [], + }, + { + entityId: 3, + entityType: 'dataset', + id: 3, + name: 'DATASET 1', + parentEntities: [], + }, + { + entityId: 4, + entityType: 'datafile', + id: 4, + name: 'DATAFILE 1', + parentEntities: [], + }, + ]; + + (fetchDownloadCart as jest.MockedFunction< + typeof fetchDownloadCart + >).mockResolvedValue(cartItems); + (removeAllDownloadCartItems as jest.MockedFunction< + typeof removeAllDownloadCartItems + >).mockResolvedValue(null); + (removeFromCart as jest.MockedFunction< + typeof removeFromCart + >).mockImplementation((entityType, entityIds) => { + cartItems = cartItems.filter( + (item) => !entityIds.includes(item.entityId) + ); + return Promise.resolve(cartItems); + }); + + (getSize as jest.MockedFunction).mockResolvedValue(1); + (getDatafileCount as jest.MockedFunction< + typeof getDatafileCount + >).mockResolvedValue(7); }); afterEach(() => { mount.cleanUp(); - (fetchDownloadCartItems as jest.Mock).mockClear(); - (getSize as jest.Mock).mockClear(); - (getDatafileCount as jest.Mock).mockClear(); - (removeAllDownloadCartItems as jest.Mock).mockClear(); - (removeDownloadCartItem as jest.Mock).mockClear(); + jest.clearAllMocks(); jest.clearAllTimers(); jest.useRealTimers(); }); - it('renders correctly', () => { - const wrapper = shallow( - - ); - - expect(wrapper).toMatchSnapshot(); - }); + it('renders no cart message correctly', async () => { + (fetchDownloadCart as jest.MockedFunction< + typeof fetchDownloadCart + >).mockResolvedValue([]); - it('fetches the download cart on load', async () => { const wrapper = createWrapper(); await act(async () => { @@ -134,24 +157,21 @@ describe('Download cart table component', () => { wrapper.update(); }); - expect(fetchDownloadCartItems).toHaveBeenCalled(); + expect(wrapper.exists('[data-testid="no-selections-message"]')).toBe(true); }); - it('does not fetch the download cart on load if no dg-download element exists', async () => { - const wrapper = mount( - - - - - - ); + it('fetches the download cart on load', async () => { + const wrapper = createWrapper(); await act(async () => { await flushPromises(); wrapper.update(); }); - expect(fetchDownloadCartItems).not.toHaveBeenCalled(); + expect(fetchDownloadCart).toHaveBeenCalled(); + expect(wrapper.find('[aria-colindex=1]').find('p').first().text()).toEqual( + 'INVESTIGATION 1' + ); }); it('calculates sizes once cart items have been fetched', async () => { @@ -179,9 +199,17 @@ describe('Download cart table component', () => { wrapper.update(); }); - expect(getDatafileCount).toHaveBeenCalled(); + expect(getDatafileCount).toHaveBeenCalledTimes(3); + expect(wrapper.find('[aria-colindex=4]').find('p').first().text()).toEqual( + '7' + ); + // datafiles should always be 1 and shouldn't call getDatafileCount + expect(wrapper.find('[aria-colindex=4]').find('p').last().text()).toEqual( + '1' + ); + expect(wrapper.find('p#fileCountDisplay').text()).toEqual( - expect.stringContaining('downloadCart.number_of_files: 28') + expect.stringContaining('downloadCart.number_of_files: 22 / 5000') ); }); @@ -246,9 +274,17 @@ describe('Download cart table component', () => { }); it('disables remove all button while request is processing', async () => { - (removeAllDownloadCartItems as jest.Mock).mockImplementation(() => { - return new Promise((resolve) => setTimeout(resolve, 2000)); - }); + // use this to manually resolve promise + let promiseResolve; + + (removeAllDownloadCartItems as jest.MockedFunction< + typeof removeAllDownloadCartItems + >).mockImplementation( + () => + new Promise((resolve) => { + promiseResolve = resolve; + }) + ); const wrapper = createWrapper(); @@ -269,13 +305,63 @@ describe('Download cart table component', () => { ).toBeTruthy(); await act(async () => { - await new Promise((r) => setTimeout(r, 2001)); + promiseResolve(); + await flushPromises(); wrapper.update(); }); expect(wrapper.exists('[data-testid="no-selections-message"]')).toBe(true); }); + it('disables download button when there are empty items in the cart ', async () => { + (getSize as jest.MockedFunction) + .mockResolvedValueOnce(1) + .mockResolvedValueOnce(0); + (getDatafileCount as jest.MockedFunction< + typeof getDatafileCount + >).mockResolvedValueOnce(0); + + const wrapper = createWrapper(); + + await act(async () => { + await flushPromises(); + wrapper.update(); + }); + + expect(wrapper.exists('div#emptyFilesAlert')).toBeTruthy(); + expect( + wrapper.find('button#downloadCartButton').prop('disabled') + ).toBeTruthy(); + + wrapper + .find(`button[aria-label="downloadCart.remove {name:INVESTIGATION 2}"]`) + .simulate('click'); + + await act(async () => { + await flushPromises(); + wrapper.update(); + }); + + expect(wrapper.exists('div#emptyFilesAlert')).toBeTruthy(); + expect( + wrapper.find('button#downloadCartButton').prop('disabled') + ).toBeTruthy(); + + wrapper + .find(`button[aria-label="downloadCart.remove {name:INVESTIGATION 1}"]`) + .simulate('click'); + + await act(async () => { + await flushPromises(); + wrapper.update(); + }); + + expect(wrapper.exists('div#emptyFilesAlert')).toBeFalsy(); + expect( + wrapper.find('button#downloadCartButton').prop('disabled') + ).toBeFalsy(); + }); + it("removes an item when said item's remove button is clicked", async () => { const wrapper = createWrapper(); @@ -307,8 +393,8 @@ describe('Download cart table component', () => { wrapper.update(); }); - expect(removeDownloadCartItem).toHaveBeenCalled(); - expect(removeDownloadCartItem).toHaveBeenCalledWith(2, 'investigation', { + expect(removeFromCart).toHaveBeenCalled(); + expect(removeFromCart).toHaveBeenCalledWith('investigation', [2], { facilityName: mockedSettings.facilityName, downloadApiUrl: mockedSettings.downloadApiUrl, }); @@ -423,4 +509,50 @@ describe('Download cart table component', () => { ) ).toBe(true); }); + + it('displays error alert if file/size limit exceeded', async () => { + let wrapper = createWrapper(); + + await act(async () => { + await flushPromises(); + wrapper.update(); + }); + + // Make sure alerts are not displayed if under the limits + expect(wrapper.exists('div#fileLimitAlert')).toBeFalsy(); + expect(wrapper.exists('div#sizeLimitAlert')).toBeFalsy(); + + const oldSettings = mockedSettings; + mockedSettings = { + ...oldSettings, + totalSizeMax: 1, + }; + + wrapper = createWrapper(); + + await act(async () => { + await flushPromises(); + wrapper.update(); + }); + + // Make sure size limit alert is displayed if over the limit + expect(wrapper.exists('div#sizeLimitAlert')).toBeTruthy(); + + mockedSettings = { + ...oldSettings, + fileCountMax: 1, + }; + + wrapper = createWrapper(); + + await act(async () => { + await flushPromises(); + wrapper.update(); + }); + + // Make sure file limit alert is displayed if over the limit + expect(wrapper.exists('div#fileLimitAlert')).toBeTruthy(); + + mockedSettings = oldSettings; + }); }); diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index 63e1549f8..0e259b815 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -8,6 +8,7 @@ import { DownloadCartItem, DownloadCartTableItem, TextFilter, + ColumnType, } from 'datagateway-common'; import { IconButton, @@ -23,20 +24,21 @@ import { CircularProgress, } from '@material-ui/core'; import { RemoveCircle } from '@material-ui/icons'; +import { Alert } from '@material-ui/lab'; import { - fetchDownloadCartItems, - removeAllDownloadCartItems, - removeDownloadCartItem, - getSize, - getIsTwoLevel, - getDatafileCount, -} from '../downloadApi'; -import chunk from 'lodash.chunk'; + useCart, + useRemoveEntityFromCart, + useIsTwoLevel, + useRemoveAllFromCart, + useSizes, + useDatafileCounts, +} from '../downloadApiHooks'; import DownloadConfirmDialog from '../downloadConfirmation/downloadConfirmDialog.component'; import { DownloadSettingsContext } from '../ConfigProvider'; import { Trans, useTranslation } from 'react-i18next'; import { Link as RouterLink } from 'react-router-dom'; +import { useQueryClient } from 'react-query'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -63,151 +65,88 @@ const DownloadCartTable: React.FC = ( const [filters, setFilters] = React.useState<{ [column: string]: { value?: string | number; type: string }; }>({}); - const [data, setData] = React.useState([]); - const [dataLoaded, setDataLoaded] = React.useState(false); - const [sizesLoaded, setSizesLoaded] = React.useState(true); - const [sizesFinished, setSizesFinished] = React.useState(true); - const [removingAll, setRemovingAll] = React.useState(false); const fileCountMax = settings.fileCountMax; const totalSizeMax = settings.totalSizeMax; const [showConfirmation, setShowConfirmation] = React.useState(false); - const [isTwoLevel, setIsTwoLevel] = React.useState(false); - const [t] = useTranslation(); - const dgDownloadElement = document.getElementById('datagateway-download'); + const { data: isTwoLevel } = useIsTwoLevel(); + const { mutate: removeDownloadCartItem } = useRemoveEntityFromCart(); + const { + mutate: removeAllDownloadCartItems, + isLoading: removingAll, + } = useRemoveAllFromCart(); + const { data, isFetching: dataLoading } = useCart(); - const totalSize = React.useMemo(() => { - if (sizesFinished) { - return data.reduce((accumulator, nextItem) => { - if (nextItem.size > -1) { - return accumulator + nextItem.size; + const queryClient = useQueryClient(); + const setData = React.useCallback( + (newData: DownloadCartTableItem[]) => { + queryClient.setQueryData('cart', newData); + }, + [queryClient] + ); + + const fileCountQueries = useDatafileCounts(data); + const sizeQueries = useSizes(data); + + const fileCount = React.useMemo(() => { + return ( + fileCountQueries?.reduce((accumulator, nextItem) => { + if (nextItem.data && nextItem.data > -1) { + return accumulator + nextItem.data; } else { return accumulator; } - }, 0); - } else { - return -1; - } - }, [data, sizesFinished]); + }, 0) ?? -1 + ); + }, [fileCountQueries]); - const fileCount = React.useMemo(() => { - if (sizesFinished) { - return data.reduce((accumulator, nextItem) => { - if (nextItem.fileCount > -1) { - return accumulator + nextItem.fileCount; + const totalSize = React.useMemo(() => { + return ( + sizeQueries?.reduce((accumulator, nextItem) => { + if (nextItem.data && nextItem.data > -1) { + return accumulator + nextItem.data; } else { return accumulator; } - }, 0); - } else { - return -1; - } - }, [data, sizesFinished]); - - React.useEffect(() => { - const checkTwoLevel = async (): Promise => - setIsTwoLevel(await getIsTwoLevel({ idsUrl: settings.idsUrl })); - - if (settings.idsUrl) checkTwoLevel(); - }, [settings.idsUrl]); - React.useEffect(() => { - if ( - settings.facilityName && - settings.apiUrl && - settings.downloadApiUrl && - dgDownloadElement - ) - fetchDownloadCartItems({ - facilityName: settings.facilityName, - downloadApiUrl: settings.downloadApiUrl, - }).then((cartItems) => { - setData( - cartItems.map((cartItem) => ({ - ...cartItem, - size: -1, - fileCount: -1, - })) - ); - setDataLoaded(true); - setSizesLoaded(false); - setSizesFinished(false); - }); - }, [ - settings.facilityName, - settings.apiUrl, - settings.downloadApiUrl, - dgDownloadElement, - ]); - - React.useEffect(() => { - if (!sizesLoaded) { - const chunkSize = 10; - const chunkedData = chunk(data, chunkSize); - const allPromises: Promise[] = []; - chunkedData.forEach((chunk, chunkIndex) => { - const updatedData = [...data]; - const chunkPromises: Promise[] = []; + }, 0) ?? -1 + ); + }, [sizeQueries]); - const chunkIndexOffset = chunkIndex * chunkSize; - chunk.forEach((cartItem, index) => { - const promiseSize = getSize(cartItem.entityId, cartItem.entityType, { - facilityName: settings.facilityName, - apiUrl: settings.apiUrl, - downloadApiUrl: settings.downloadApiUrl, - }).then((size) => { - updatedData[chunkIndexOffset + index].size = size; - }); - const promiseFileCount = getDatafileCount( - cartItem.entityId, - cartItem.entityType, - { - apiUrl: settings.apiUrl, - } - ).then((fileCount) => { - updatedData[chunkIndexOffset + index].fileCount = fileCount; - }); - chunkPromises.push(promiseSize); - allPromises.push(promiseSize); - chunkPromises.push(promiseFileCount); - allPromises.push(promiseFileCount); - }); + const sizesLoading = sizeQueries.some((query) => query.isLoading); + const fileCountsLoading = fileCountQueries.some((query) => query.isLoading); - Promise.all(chunkPromises).then(() => { - setData(updatedData); - }); - }); - Promise.all(allPromises).then(() => { - setSizesFinished(true); - }); - setSizesLoaded(true); - } - }, [ - data, - sizesLoaded, - settings.facilityName, - settings.apiUrl, - settings.downloadApiUrl, - ]); + const [t] = useTranslation(); - const textFilter = (label: string, dataKey: string): React.ReactElement => ( - { - if (value) { - setFilters({ ...filters, [dataKey]: value }); - } else { - const { [dataKey]: value, ...restOfFilters } = filters; - setFilters(restOfFilters); - } - }} - value={filters[dataKey] as TextFilter} - /> + const textFilter = React.useCallback( + (label: string, dataKey: string): React.ReactElement => ( + { + if (value) { + setFilters({ ...filters, [dataKey]: value }); + } else { + const { [dataKey]: value, ...restOfFilters } = filters; + setFilters(restOfFilters); + } + }} + value={filters[dataKey] as TextFilter} + /> + ), + [filters] ); const sortedAndFilteredData = React.useMemo(() => { - const filteredData = data.filter((item) => { + const sizeAndCountAddedData = data?.map( + (item, index) => + ({ + ...item, + size: sizeQueries?.[index]?.data ?? -1, + fileCount: fileCountQueries?.[index]?.data ?? -1, + } as DownloadCartTableItem) + ); + const filteredData = sizeAndCountAddedData?.filter((item) => { for (const [key, value] of Object.entries(filters)) { const tableValue = item[key]; if ( @@ -246,15 +185,92 @@ const DownloadCartTable: React.FC = ( return 0; } - return filteredData.sort(sortCartItems); - }, [data, sort, filters]); + return filteredData?.sort(sortCartItems); + }, [data, sort, filters, sizeQueries, fileCountQueries]); + + const columns: ColumnType[] = React.useMemo( + () => [ + { + label: t('downloadCart.name'), + dataKey: 'name', + filterComponent: textFilter, + }, + { + label: t('downloadCart.type'), + dataKey: 'entityType', + filterComponent: textFilter, + }, + { + label: t('downloadCart.size'), + dataKey: 'size', + cellContentRenderer: (props) => { + return formatBytes(props.cellData); + }, + }, + { + label: t('downloadCart.fileCount'), + dataKey: 'fileCount', + cellContentRenderer: (props) => { + if (props.cellData === -1) return 'Loading...'; + return props.cellData; + }, + }, + ], + [t, textFilter] + ); + const onSort = React.useCallback( + (column: string, order: 'desc' | 'asc' | null) => { + if (order) { + setSort({ ...sort, [column]: order }); + } else { + const { [column]: order, ...restOfSort } = sort; + setSort(restOfSort); + } + }, + [sort] + ); + const actions = React.useMemo( + () => [ + function RemoveButton({ rowData }: TableActionProps) { + const cartItem = rowData as DownloadCartItem; + const { entityId, entityType } = cartItem; + const [isDeleting, setIsDeleting] = React.useState(false); + return ( + { + setIsDeleting(true); + removeDownloadCartItem({ + entityId, + entityType, + }); + }} + > + + + ); + }, + ], + [removeDownloadCartItem, t] + ); const emptyItems = React.useMemo( - () => data.some((item) => item.size === 0 || item.fileCount === 0), - [data] + () => + sizeQueries.some((query) => query.data === 0) || + fileCountQueries.some((query) => query.data === 0), + [sizeQueries, fileCountQueries] ); - return data.length === 0 ? ( + return !dataLoading && data?.length === 0 ? (
= ( > search {' '} - for data. + for data?. @@ -298,7 +314,7 @@ const DownloadCartTable: React.FC = (
{/* Show loading progress if data is still being loaded */} - {!dataLoaded && ( + {dataLoading && ( @@ -311,89 +327,23 @@ const DownloadCartTable: React.FC = ( className="tour-download-results" style={{ height: `calc(100vh - 64px - 48px - 48px - 48px - 3rem${ - emptyItems ? ' - 1rem' : '' + emptyItems || + fileCount > fileCountMax || + totalSize > totalSizeMax + ? ' - 2rem' + : '' } - (1.75 * 0.875rem + 12px)`, minHeight: 230, overflowX: 'auto', }} > { - return formatBytes(props.cellData); - }, - }, - { - label: t('downloadCart.fileCount'), - dataKey: 'fileCount', - cellContentRenderer: (props) => { - if (props.cellData === -1) return 'Loading...'; - return props.cellData; - }, - }, - ]} + columns={columns} sort={sort} - onSort={(column: string, order: 'desc' | 'asc' | null) => { - if (order) { - setSort({ ...sort, [column]: order }); - } else { - const { [column]: order, ...restOfSort } = sort; - setSort(restOfSort); - } - }} - data={sortedAndFilteredData} - loading={!dataLoaded} - actions={[ - function RemoveButton({ rowData }: TableActionProps) { - const cartItem = rowData as DownloadCartItem; - const [isDeleting, setIsDeleting] = React.useState(false); - return ( - { - setIsDeleting(true); - removeDownloadCartItem( - cartItem.entityId, - cartItem.entityType, - { - facilityName: settings.facilityName, - downloadApiUrl: settings.downloadApiUrl, - } - ).then(() => { - setData( - data.filter( - (item) => item.entityId !== cartItem.entityId - ) - ); - }); - }} - > - - - ); - }, - ]} + onSort={onSort} + data={sortedAndFilteredData ?? []} + loading={dataLoading} + actions={actions} /> @@ -407,24 +357,114 @@ const DownloadCartTable: React.FC = ( + + {fileCountsLoading && ( + + )} + + {t('downloadCart.number_of_files')}:{' '} + {fileCount !== -1 + ? fileCount + : `${t('downloadCart.calculating')}...`} + {fileCountMax !== -1 && ` / ${fileCountMax}`} + + + + {fileCount > fileCountMax && ( + + {t('downloadCart.file_limit_error', { + fileCountMax: fileCountMax, + })} + + )} + + + + + {sizesLoading && ( + + )} + + {t('downloadCart.total_size')}:{' '} + {totalSize !== -1 + ? formatBytes(totalSize) + : `${t('downloadCart.calculating')}...`} + {totalSizeMax !== -1 && ` / ${formatBytes(totalSizeMax)}`} + + + + {totalSize > totalSizeMax && ( + + {t('downloadCart.size_limit_error', { + totalSizeMax: formatBytes(totalSizeMax), + })} + + )} + + + - - {t('downloadCart.number_of_files')}:{' '} - {fileCount !== -1 ? fileCount : 'Calculating...'} - {fileCountMax !== -1 && ` / ${fileCountMax}`} - - - {t('downloadCart.total_size')}:{' '} - {totalSize !== -1 ? formatBytes(totalSize) : 'Calculating...'} - {totalSizeMax !== -1 && ` / ${formatBytes(totalSizeMax)}`} - {emptyItems && ( - {t('downloadCart.empty_items_warning')} + + {t('downloadCart.empty_items_error')} + )} = ( color="primary" disabled={removingAll} startIcon={removingAll && } - onClick={() => { - setRemovingAll(true); - removeAllDownloadCartItems({ - facilityName: settings.facilityName, - downloadApiUrl: settings.downloadApiUrl, - }).then(() => { - setData([]); - setRemovingAll(false); - }); - }} + onClick={() => removeAllDownloadCartItems()} > {t('downloadCart.remove_all')} @@ -468,6 +499,8 @@ const DownloadCartTable: React.FC = ( disabled={ fileCount <= 0 || totalSize <= 0 || + fileCountsLoading || + sizesLoading || emptyItems || (fileCountMax !== -1 && fileCount > fileCountMax) || (totalSizeMax !== -1 && totalSize > totalSizeMax) @@ -484,7 +517,7 @@ const DownloadCartTable: React.FC = ( setShowConfirmation(false)} diff --git a/packages/datagateway-download/src/downloadStatus/__snapshots__/adminDownloadStatusTable.component.test.tsx.snap b/packages/datagateway-download/src/downloadStatus/__snapshots__/adminDownloadStatusTable.component.test.tsx.snap index 8c0a615ef..5a40a327e 100644 --- a/packages/datagateway-download/src/downloadStatus/__snapshots__/adminDownloadStatusTable.component.test.tsx.snap +++ b/packages/datagateway-download/src/downloadStatus/__snapshots__/adminDownloadStatusTable.component.test.tsx.snap @@ -144,7 +144,11 @@ exports[`Admin Download Status Table renders correctly 1`] = ` loadMoreRows={[Function]} loading={true} onSort={[Function]} - sort={Object {}} + sort={ + Object { + "createdAt": "desc", + } + } totalRowCount={9007199254740991} /> diff --git a/packages/datagateway-download/src/downloadStatus/__snapshots__/downloadStatusTable.component.test.tsx.snap b/packages/datagateway-download/src/downloadStatus/__snapshots__/downloadStatusTable.component.test.tsx.snap index 3668ec9bc..bb2d5ba8d 100644 --- a/packages/datagateway-download/src/downloadStatus/__snapshots__/downloadStatusTable.component.test.tsx.snap +++ b/packages/datagateway-download/src/downloadStatus/__snapshots__/downloadStatusTable.component.test.tsx.snap @@ -60,7 +60,11 @@ exports[`Download Status Table renders correctly 1`] = ` data={Array []} loading={true} onSort={[Function]} - sort={Object {}} + sort={ + Object { + "createdAt": "desc", + } + } /> diff --git a/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.test.tsx b/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.test.tsx index 6c65cd724..544a5c26b 100644 --- a/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.test.tsx +++ b/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.test.tsx @@ -148,7 +148,7 @@ describe('Admin Download Status Table', () => { expect(wrapper).toMatchSnapshot(); }); - it('fetches the download items on load', async () => { + it('fetches the download items and sorts by download requested time desc on load ', async () => { const wrapper = mount(
@@ -164,7 +164,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenNthCalledWith( 1, { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' ORDER BY UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' ORDER BY download.createdAt desc, download.id ASC LIMIT 0, 50" ); expect(wrapper.exists('[aria-rowcount=5]')).toBe(true); }); @@ -194,7 +194,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenCalledTimes(3); expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' ORDER BY UPPER(download.id) ASC LIMIT 5, 5" + "WHERE download.facilityName = '' ORDER BY download.createdAt desc, download.id ASC LIMIT 5, 5" ); expect(wrapper.exists('[aria-rowcount=5]')).toBe(true); }); @@ -254,7 +254,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenNthCalledWith( 3, { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' ORDER BY UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' ORDER BY download.createdAt desc, download.id ASC LIMIT 0, 50" ); expect(wrapper.exists('[aria-rowcount=5]')).toBe(true); }); @@ -271,6 +271,17 @@ describe('Admin Download Status Table', () => { wrapper.update(); }); + // Table is sorted by createdAt desc by default + // To keep working test, we will remove all sorts on the table beforehand + const createdAtSortLabel = wrapper + .find('[role="columnheader"] span[role="button"]') + .at(6); + await act(async () => { + createdAtSortLabel.simulate('click'); + await flushPromises(); + wrapper.update(); + }); + // Get the Username sort header const usernameSortLabel = wrapper .find('[role="columnheader"] span[role="button"]') @@ -283,7 +294,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' ORDER BY UPPER(download.userName) asc, UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' ORDER BY download.userName asc, download.id ASC LIMIT 0, 50" ); // Get the Access Method sort header. @@ -298,7 +309,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' ORDER BY UPPER(download.userName) asc, UPPER(download.transport) asc, UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' ORDER BY download.userName asc, download.transport asc, download.id ASC LIMIT 0, 50" ); await act(async () => { @@ -309,7 +320,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' ORDER BY UPPER(download.userName) asc, UPPER(download.transport) desc, UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' ORDER BY download.userName asc, download.transport desc, download.id ASC LIMIT 0, 50" ); await act(async () => { @@ -320,7 +331,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' ORDER BY UPPER(download.userName) asc, UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' ORDER BY download.userName asc, download.id ASC LIMIT 0, 50" ); }, 10000); @@ -336,6 +347,17 @@ describe('Admin Download Status Table', () => { wrapper.update(); }); + // Table is sorted by createdAt desc by default + // To keep working test, we will remove all sorts on the table beforehand + const createdAtSortLabel = wrapper + .find('[role="columnheader"] span[role="button"]') + .at(6); + await act(async () => { + createdAtSortLabel.simulate('click'); + await flushPromises(); + wrapper.update(); + }); + // Get the Username filter input const usernameFilterInput = wrapper .find('[aria-label="Filter by downloadStatus.username"]') @@ -349,7 +371,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' AND UPPER(download.userName) LIKE CONCAT('%', 'TEST USER', '%') ORDER BY UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' AND UPPER(download.userName) LIKE CONCAT('%', 'TEST USER', '%') ORDER BY download.id ASC LIMIT 0, 50" ); usernameFilterInput.instance().value = ''; usernameFilterInput.simulate('change'); @@ -367,7 +389,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' AND UPPER(download.status) LIKE CONCAT('%', 'COMPLETE', '%') ORDER BY UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' AND UPPER(download.status) LIKE CONCAT('%', 'COMPLETE', '%') ORDER BY download.id ASC LIMIT 0, 50" ); // We simulate a change in the select from 'include' to 'exclude'. @@ -382,7 +404,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' AND UPPER(download.status) NOT LIKE CONCAT('%', 'COMPLETE', '%') ORDER BY UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' AND UPPER(download.status) NOT LIKE CONCAT('%', 'COMPLETE', '%') ORDER BY download.id ASC LIMIT 0, 50" ); await act(async () => { @@ -394,7 +416,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' ORDER BY UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' ORDER BY download.id ASC LIMIT 0, 50" ); }, 10000); @@ -410,12 +432,23 @@ describe('Admin Download Status Table', () => { wrapper.update(); }); + // Table is sorted by createdAt desc by default + // To keep working test, we will remove all sorts on the table beforehand + const createdAtSortLabel = wrapper + .find('[role="columnheader"] span[role="button"]') + .at(6); + await act(async () => { + createdAtSortLabel.simulate('click'); + await flushPromises(); + wrapper.update(); + }); + // Get the Requested Data From filter input const dateFromFilterInput = wrapper.find( 'input[id="downloadStatus.createdAt filter from"]' ); await act(async () => { - dateFromFilterInput.instance().value = '2020-01-01'; + dateFromFilterInput.instance().value = '2020-01-01 00:00'; dateFromFilterInput.simulate('change'); await flushPromises(); wrapper.update(); @@ -423,7 +456,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' AND UPPER(download.createdAt) BETWEEN {ts '2020-01-01 00:00:00'} AND {ts '9999-12-31 23:59:59'} ORDER BY UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' AND download.createdAt BETWEEN {ts '2020-01-01 00:00'} AND {ts '9999-12-31 23:59'} ORDER BY download.id ASC LIMIT 0, 50" ); // Get the Requested Data To filter input @@ -431,7 +464,7 @@ describe('Admin Download Status Table', () => { 'input[id="downloadStatus.createdAt filter to"]' ); await act(async () => { - dateToFilterInput.instance().value = '2020-01-02'; + dateToFilterInput.instance().value = '2020-01-02 23:59'; dateToFilterInput.simulate('change'); await flushPromises(); wrapper.update(); @@ -439,7 +472,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' AND UPPER(download.createdAt) BETWEEN {ts '2020-01-01 00:00:00'} AND {ts '2020-01-02 23:59:59'} ORDER BY UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' AND download.createdAt BETWEEN {ts '2020-01-01 00:00'} AND {ts '2020-01-02 23:59'} ORDER BY download.id ASC LIMIT 0, 50" ); dateFromFilterInput.instance().value = ''; @@ -453,7 +486,7 @@ describe('Admin Download Status Table', () => { expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - "WHERE UPPER(download.facilityName) = '' ORDER BY UPPER(download.id) ASC LIMIT 0, 50" + "WHERE download.facilityName = '' ORDER BY download.id ASC LIMIT 0, 50" ); }, 10000); @@ -491,7 +524,7 @@ describe('Admin Download Status Table', () => { }); expect(fetchAdminDownloads).toHaveBeenLastCalledWith( { downloadApiUrl: '', facilityName: '' }, - 'WHERE UPPER(download.id) = 4' + 'WHERE download.id = 4' ); expect( wrapper.exists( diff --git a/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.tsx b/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.tsx index 59c4cc430..031e9aab0 100644 --- a/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.tsx +++ b/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.tsx @@ -59,7 +59,9 @@ const AdminDownloadStatusTable: React.FC = () => { // Load the settings for use const settings = React.useContext(DownloadSettingsContext); // Sorting columns - const [sort, setSort] = React.useState<{ [column: string]: Order }>({}); + const [sort, setSort] = React.useState<{ [column: string]: Order }>({ + createdAt: 'desc', + }); const [filters, setFilters] = React.useState<{ [column: string]: | { value?: string | number; type: string } @@ -84,19 +86,15 @@ const AdminDownloadStatusTable: React.FC = () => { }, [t]); const buildQueryOffset = useCallback(() => { - let queryOffset = `WHERE UPPER(download.facilityName) = '${settings.facilityName}'`; + let queryOffset = `WHERE download.facilityName = '${settings.facilityName}'`; for (const [column, filter] of Object.entries(filters)) { if (typeof filter === 'object') { if (!Array.isArray(filter)) { if ('startDate' in filter || 'endDate' in filter) { - const startDate = filter.startDate - ? `${filter.startDate} 00:00:00` - : '0000-01-01 00:00:00'; - const endDate = filter.endDate - ? `${filter.endDate} 23:59:59` - : '9999-12-31 23:59:59'; - - queryOffset += ` AND UPPER(download.${column}) BETWEEN {ts '${startDate}'} AND {ts '${endDate}'}`; + const startDate = filter.startDate ?? '0001-01-01 00:00'; + const endDate = filter.endDate ?? '9999-12-31 23:59'; + + queryOffset += ` AND download.${column} BETWEEN {ts '${startDate}'} AND {ts '${endDate}'}`; } if ('type' in filter && filter.type) { @@ -118,9 +116,9 @@ const AdminDownloadStatusTable: React.FC = () => { queryOffset += ' ORDER BY'; for (const [column, order] of Object.entries(sort)) { - queryOffset += ` UPPER(download.${column}) ${order},`; + queryOffset += ` download.${column} ${order},`; } - queryOffset += ' UPPER(download.id) ASC'; + queryOffset += ' download.id ASC'; return queryOffset; }, [filters, settings.facilityName, sort]); @@ -259,6 +257,7 @@ const AdminDownloadStatusTable: React.FC = () => { } }} value={filters[dataKey] as DateFilter} + filterByTime /> ); @@ -428,7 +427,7 @@ const AdminDownloadStatusTable: React.FC = () => { facilityName: settings.facilityName, downloadApiUrl: settings.downloadApiUrl, }, - `WHERE UPPER(download.id) = ${downloadItem.id}` + `WHERE download.id = ${downloadItem.id}` ).then((downloads) => { const formattedDownload = formatDownloads( downloads diff --git a/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.test.tsx b/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.test.tsx index 57b35e2b9..59c76b4be 100644 --- a/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.test.tsx +++ b/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.test.tsx @@ -250,22 +250,27 @@ describe('Download Status Table', () => { // Expect globus download items to have been disabled. expect( - wrapper.exists( - '[aria-label="downloadStatus.download_disabled_button {filename:test-file-2}"]' - ) + wrapper + .find( + 'button[aria-label="downloadStatus.download {filename:test-file-2}"]' + ) + .prop('disabled') ).toBe(true); + + // Expect HTTPS download items with non-COMPLETE status to have been disabled. expect( wrapper .find( - 'button[aria-label="downloadStatus.download_disabled_button {filename:test-file-2}"]' + 'button[aria-label="downloadStatus.download {filename:test-file-3}"]' ) .prop('disabled') ).toBe(true); + // Expect complete HTTPS download items to be downloadable // Check to see if the href contains the correct call. expect( wrapper - .find('a[aria-label="downloadStatus.download {filename:test-file-3}"]') + .find('a[aria-label="downloadStatus.download {filename:test-file-1}"]') .at(0) .props().href ).toContain('/getData'); @@ -334,6 +339,13 @@ describe('Download Status Table', () => { wrapper.update(); }); + // Table is sorted by createdAt desc by default + // To keep working test, we will remove all sorts on the table beforehand + const createdAtSortLabel = wrapper + .find('[role="columnheader"] span[role="button"]') + .at(3); + createdAtSortLabel.simulate('click'); + const firstNameCell = wrapper.find('[aria-colindex=1]').find('p').first(); // Get the access method sort header. @@ -461,7 +473,7 @@ describe('Download Status Table', () => { 'input[id="downloadStatus.createdAt filter from"]' ); - dateFromFilterInput.instance().value = '2020-01-01'; + dateFromFilterInput.instance().value = '2020-01-01 00:00'; dateFromFilterInput.simulate('change'); expect(wrapper.exists('[aria-rowcount=5]')).toBe(true); @@ -470,14 +482,14 @@ describe('Download Status Table', () => { 'input[id="downloadStatus.createdAt filter to"]' ); - dateToFilterInput.instance().value = '2020-01-02'; + dateToFilterInput.instance().value = '2020-01-02 23:59'; dateToFilterInput.simulate('change'); expect(wrapper.exists('[aria-rowcount=0]')).toBe(true); - dateFromFilterInput.instance().value = '2020-02-26'; + dateFromFilterInput.instance().value = '2020-02-26 00:00'; dateFromFilterInput.simulate('change'); - dateToFilterInput.instance().value = '2020-02-27'; + dateToFilterInput.instance().value = '2020-02-27 23:59'; dateToFilterInput.simulate('change'); expect(wrapper.exists('[aria-rowcount=2]')).toBe(true); diff --git a/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.tsx b/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.tsx index cf15be54c..2358809ee 100644 --- a/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.tsx +++ b/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.tsx @@ -33,7 +33,9 @@ const DownloadStatusTable: React.FC = ( const settings = React.useContext(DownloadSettingsContext); // Sorting columns - const [sort, setSort] = React.useState<{ [column: string]: Order }>({}); + const [sort, setSort] = React.useState<{ [column: string]: Order }>({ + createdAt: 'desc', + }); const [filters, setFilters] = React.useState<{ [column: string]: | { value?: string | number; type: string } @@ -158,6 +160,7 @@ const DownloadStatusTable: React.FC = ( } }} value={filters[dataKey] as DateFilter} + filterByTime /> ); @@ -186,7 +189,7 @@ const DownloadStatusTable: React.FC = ( const tableTimestamp = toDate(tableValue).getTime(); const startTimestamp = toDate(value.startDate).getTime(); const endTimestamp = value.endDate - ? new Date(`${value.endDate} 23:59:59`).getTime() + ? new Date(value.endDate).getTime() : Date.now(); if ( @@ -297,59 +300,56 @@ const DownloadStatusTable: React.FC = ( actions={[ function DownloadButton({ rowData }: TableActionProps) { const downloadItem = rowData as FormattedDownload; - const isDownloadable = (downloadItem.transport as string).match( - /https|http/ - ) + const isHTTP = downloadItem.transport.match(/https|http/) ? true : false; + const isComplete = + downloadItem.status === t('downloadStatus.complete') + ? true + : false; + + const isDownloadable = isHTTP && isComplete; + return ( -
- {/* Provide a download button and set disabled if instant download is not supported. */} - {isDownloadable ? ( - - - - ) : ( - - - - )} -
+ {/* Provide a download button and set disabled if instant download is not supported. */} + + +
); }, diff --git a/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx b/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx index 087f79543..9d8e16c2a 100644 --- a/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx +++ b/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx @@ -6,6 +6,8 @@ import { flushPromises } from '../setupTests'; import { DownloadSettingsContext } from '../ConfigProvider'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; +import { ReactWrapper } from 'enzyme'; +import { QueryClient, QueryClientProvider } from 'react-query'; // Create our mocked datagateway-download settings file. const mockedSettings = { @@ -42,19 +44,26 @@ describe('DownloadTab', () => { mount.cleanUp(); }); + const createWrapper = (): ReactWrapper => { + const queryClient = new QueryClient(); + return mount( + + + + + + + + ); + }; + it('renders correctly', () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); it('shows the appropriate table when clicking between tabs', async () => { - const wrapper = mount( - - - - - - ); + const wrapper = createWrapper(); await act(async () => { await flushPromises(); @@ -129,13 +138,7 @@ describe('DownloadTab', () => { }); it('renders the selections tab on each mount', async () => { - let wrapper = mount( - - - - - - ); + let wrapper = createWrapper(); await act(async () => { await flushPromises(); @@ -153,13 +156,7 @@ describe('DownloadTab', () => { }); // Recreate the wrapper and expect it to show the selections tab. - wrapper = mount( - - - - - - ); + wrapper = createWrapper(); await act(async () => { await flushPromises(); diff --git a/packages/datagateway-search/cypress/integration/search/datafileSearch.spec.ts b/packages/datagateway-search/cypress/integration/search/datafileSearch.spec.ts index f13bfd60d..a47245ea3 100644 --- a/packages/datagateway-search/cypress/integration/search/datafileSearch.spec.ts +++ b/packages/datagateway-search/cypress/integration/search/datafileSearch.spec.ts @@ -81,15 +81,14 @@ describe('Datafile search tab', () => { cy.get('[aria-label="Start date input"]').type('2012-02-02'); cy.get('[aria-label="End date input"]').type('2012-02-03'); - cy.get('[aria-label="Submit search"]').click(); + cy.get('[aria-label="Submit search"]').click().wait('@datafilesCount', { + timeout: 10000, + }); cy.get('[aria-label="Search table"]') .contains('Datafile') .contains('9') - .click() - .wait(['@datafiles', '@datafiles', '@datafilesCount'], { - timeout: 10000, - }); + .click(); cy.get('[aria-rowcount="9"]').should('exist'); diff --git a/packages/datagateway-search/cypress/integration/searchPageContainer.spec.ts b/packages/datagateway-search/cypress/integration/searchPageContainer.spec.ts index 38b7de97d..a061ea37d 100644 --- a/packages/datagateway-search/cypress/integration/searchPageContainer.spec.ts +++ b/packages/datagateway-search/cypress/integration/searchPageContainer.spec.ts @@ -214,7 +214,11 @@ describe('SearchPageContainer Component', () => { cy.get('select[id="select-max-results"]', { timeout: 10000, - }).select('20'); + }) + .select('20') + .wait(['@investigations', '@investigations', '@investigationsCount'], { + timeout: 10000, + }); cy.get('[aria-label="card-buttons"]', { timeout: 10000 }).should( 'have.length', 20 @@ -222,7 +226,11 @@ describe('SearchPageContainer Component', () => { cy.get('select[id="select-max-results"]', { timeout: 10000, - }).select('30'); + }) + .select('30') + .wait(['@investigations', '@investigations', '@investigationsCount'], { + timeout: 10000, + }); cy.get('[aria-label="card-buttons"]', { timeout: 10000 }).should( 'have.length', 30 diff --git a/packages/datagateway-search/package.json b/packages/datagateway-search/package.json index 2122a30eb..d4f672865 100644 --- a/packages/datagateway-search/package.json +++ b/packages/datagateway-search/package.json @@ -1,6 +1,6 @@ { "name": "datagateway-search", - "version": "1.0.0", + "version": "1.1.0", "private": true, "dependencies": { "@date-io/date-fns": "^1.3.13", @@ -14,7 +14,7 @@ "axios": "^0.26.0", "connected-react-router": "^6.9.1", "custom-event-polyfill": "^1.0.7", - "datagateway-common": "1.0.0", + "datagateway-common": "^1.1.0", "date-fns": "^2.28.0", "history": "^4.10.1", "i18next": "^21.6.13", @@ -122,4 +122,4 @@ "serve": "^13.0.2", "start-server-and-test": "^1.14.0" } -} \ No newline at end of file +} diff --git a/packages/datagateway-search/src/__snapshots__/searchPageCardView.component.test.tsx.snap b/packages/datagateway-search/src/__snapshots__/searchPageCardView.component.test.tsx.snap index 52c3ff7d2..c6098f6e7 100644 --- a/packages/datagateway-search/src/__snapshots__/searchPageCardView.component.test.tsx.snap +++ b/packages/datagateway-search/src/__snapshots__/searchPageCardView.component.test.tsx.snap @@ -148,6 +148,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -183,6 +184,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -248,6 +250,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -283,6 +286,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -310,6 +314,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Investigation\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -413,6 +418,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -448,6 +454,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -475,6 +482,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Dataset\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -578,6 +586,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -613,6 +622,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -640,6 +650,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Datafile\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -745,6 +756,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -782,6 +794,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -849,6 +862,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -886,6 +900,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -915,6 +930,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"investigation\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -1048,6 +1064,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1085,6 +1102,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1114,6 +1132,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"dataset\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -1247,6 +1266,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1284,6 +1304,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1313,6 +1334,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"datafile\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -1435,6 +1457,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1461,6 +1484,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1479,6 +1503,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", @@ -1582,6 +1607,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1626,6 +1652,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1662,6 +1689,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"investigation\\",{\\"filters\\":{},\\"page\\":1,\\"results\\":10,\\"sort\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"},{\\"filterType\\":\\"include\\",\\"filterValue\\":\\"{\\\\\\"investigationInstruments\\\\\\":\\\\\\"instrument\\\\\\"}\\"}],null]", @@ -1804,6 +1832,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1841,6 +1870,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1870,6 +1900,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"datafile\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -2003,6 +2034,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2040,6 +2072,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -2069,6 +2102,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"dataset\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -2202,6 +2236,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2239,6 +2274,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -2306,6 +2342,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2343,6 +2380,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -2372,6 +2410,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"investigation\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -2494,6 +2533,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2520,6 +2560,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -2538,6 +2579,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", @@ -2641,6 +2683,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2685,6 +2728,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -2721,6 +2765,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"investigation\\",{\\"filters\\":{},\\"page\\":1,\\"results\\":10,\\"sort\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"},{\\"filterType\\":\\"include\\",\\"filterValue\\":\\"{\\\\\\"investigationInstruments\\\\\\":\\\\\\"instrument\\\\\\"}\\"}],null]", @@ -2859,6 +2904,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2894,6 +2940,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -2921,6 +2968,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Datafile\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -3024,6 +3072,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -3059,6 +3108,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -3086,6 +3136,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Dataset\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -3189,6 +3240,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -3224,6 +3276,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -3289,6 +3342,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -3324,6 +3378,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -3351,6 +3406,7 @@ exports[`SearchPageCardView renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Investigation\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ diff --git a/packages/datagateway-search/src/__snapshots__/searchPageContainer.component.test.tsx.snap b/packages/datagateway-search/src/__snapshots__/searchPageContainer.component.test.tsx.snap index 3d0abc2b0..279e77345 100644 --- a/packages/datagateway-search/src/__snapshots__/searchPageContainer.component.test.tsx.snap +++ b/packages/datagateway-search/src/__snapshots__/searchPageContainer.component.test.tsx.snap @@ -146,6 +146,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -181,6 +182,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -208,6 +210,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Investigation\\",{\\"endDate\\":null,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -311,6 +314,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -346,6 +350,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -373,6 +378,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Dataset\\",{\\"endDate\\":null,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -476,6 +482,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -511,6 +518,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -538,6 +546,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Datafile\\",{\\"endDate\\":null,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -632,6 +641,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "currentResultState": Object { @@ -659,6 +669,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "previousQueryResult": undefined, @@ -678,6 +689,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "queryHash": "[\\"cart\\"]", @@ -766,6 +778,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "currentResultState": Object { @@ -793,6 +806,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "previousQueryResult": undefined, @@ -812,6 +826,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "queryHash": "[\\"cart\\"]", @@ -907,6 +922,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -942,6 +958,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -969,6 +986,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Datafile\\",{\\"endDate\\":null,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -1072,6 +1090,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1107,6 +1126,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1134,6 +1154,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Dataset\\",{\\"endDate\\":null,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -1237,6 +1258,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1272,6 +1294,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1299,6 +1322,7 @@ exports[`SearchPageContainer - Tests renders searchPageContainer correctly 1`] = "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Investigation\\",{\\"endDate\\":null,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ diff --git a/packages/datagateway-search/src/__snapshots__/searchPageTable.component.test.tsx.snap b/packages/datagateway-search/src/__snapshots__/searchPageTable.component.test.tsx.snap index 85ec751c1..2eac199de 100644 --- a/packages/datagateway-search/src/__snapshots__/searchPageTable.component.test.tsx.snap +++ b/packages/datagateway-search/src/__snapshots__/searchPageTable.component.test.tsx.snap @@ -148,6 +148,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -183,6 +184,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -248,6 +250,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -283,6 +286,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -310,6 +314,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Investigation\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -413,6 +418,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -448,6 +454,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -475,6 +482,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Dataset\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -578,6 +586,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -613,6 +622,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -640,6 +650,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Datafile\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -745,6 +756,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -782,6 +794,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -849,6 +862,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -886,6 +900,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -915,6 +930,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"investigation\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -1048,6 +1064,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1085,6 +1102,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1114,6 +1132,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"dataset\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -1247,6 +1266,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1284,6 +1304,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1313,6 +1334,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"datafile\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -1435,6 +1457,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1461,6 +1484,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1479,6 +1503,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", @@ -1589,6 +1614,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1636,6 +1662,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1673,6 +1700,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"investigation\\",{\\"filters\\":{},\\"sort\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"},{\\"filterType\\":\\"include\\",\\"filterValue\\":\\"{\\\\\\"investigationInstruments\\\\\\":\\\\\\"instrument\\\\\\"}\\"}],null]", @@ -1811,6 +1839,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -1848,6 +1877,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -1877,6 +1907,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"investigationIds\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -1998,6 +2029,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "currentResultState": Object { @@ -2025,6 +2057,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "previousQueryResult": undefined, @@ -2044,6 +2077,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "queryHash": "[\\"cart\\"]", @@ -2132,6 +2166,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "currentResultState": Object { @@ -2159,6 +2194,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "previousQueryResult": undefined, @@ -2178,6 +2214,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"cart\\"]", "queryKey": "cart", + "retry": [Function], "staleTime": 0, }, "queryHash": "[\\"cart\\"]", @@ -2275,6 +2312,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2312,6 +2350,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -2341,6 +2380,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"datafile\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -2474,6 +2514,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2511,6 +2552,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -2540,6 +2582,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"dataset\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -2673,6 +2716,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2710,6 +2754,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -2777,6 +2822,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2814,6 +2860,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -2843,6 +2890,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"count\\",\\"investigation\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -2965,6 +3013,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -2991,6 +3040,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -3009,6 +3059,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "queryFn": [Function], "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", + "retry": [Function], }, "queryHash": "[\\"facilityCycle\\"]", "queryKey": "facilityCycle", @@ -3119,6 +3170,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -3166,6 +3218,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -3203,6 +3256,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` ], undefined, ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"investigation\\",{\\"filters\\":{},\\"sort\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"},{\\"filterType\\":\\"include\\",\\"filterValue\\":\\"{\\\\\\"investigationInstruments\\\\\\":\\\\\\"instrument\\\\\\"}\\"}],null]", @@ -3341,6 +3395,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -3378,6 +3433,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -3407,6 +3463,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` }, ], ], + "retry": [Function], }, "promise": Promise {}, "queryHash": "[\\"investigationIds\\",{\\"filters\\":{}},[{\\"filterType\\":\\"where\\",\\"filterValue\\":\\"{\\\\\\"id\\\\\\":{\\\\\\"in\\\\\\":[]}}\\"}]]", @@ -3537,6 +3594,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -3572,6 +3630,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -3599,6 +3658,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Datafile\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -3702,6 +3762,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -3737,6 +3798,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -3764,6 +3826,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Dataset\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ @@ -3867,6 +3930,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -3902,6 +3966,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -3967,6 +4032,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "currentResultState": Object { "data": undefined, @@ -4002,6 +4068,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "previousQueryResult": undefined, "previousSelectError": null, @@ -4029,6 +4096,7 @@ exports[`SearchPageTable renders correctly when request received 1`] = ` "startDate": null, }, ], + "retry": [Function], }, "queryHash": "[\\"search\\",\\"Investigation\\",{\\"endDate\\":null,\\"maxCount\\":300,\\"searchText\\":\\"\\",\\"startDate\\":null}]", "queryKey": Array [ diff --git a/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx b/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx index 6c39da6b9..0248f353e 100644 --- a/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx +++ b/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx @@ -364,6 +364,9 @@ const DatasetCardView = (props: DatasetCardViewProps): React.ReactElement => { entityType="dataset" entityId={dataset.id} entityName={dataset.name} + entitySize={ + data ? sizeQueries[data.indexOf(dataset)]?.data ?? -1 : -1 + } />
), @@ -378,7 +381,7 @@ const DatasetCardView = (props: DatasetCardViewProps): React.ReactElement => { ), ], - [classes.actionButtons, data, hierarchy] + [classes.actionButtons, data, hierarchy, sizeQueries] ); return ( diff --git a/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx b/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx index b2b2845c1..affb10241 100644 --- a/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx +++ b/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx @@ -365,12 +365,17 @@ const InvestigationCardView = ( entityType="investigation" entityId={investigation.id} entityName={investigation.name} + entitySize={ + data + ? sizeQueries[data.indexOf(investigation)]?.data ?? -1 + : -1 + } /> ), ] : [], - [classes.actionButtons, data, hierarchy] + [classes.actionButtons, data, hierarchy, sizeQueries] ); return ( diff --git a/yarn.lock b/yarn.lock index f2ed1428b..2107ee5ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4559,11 +4559,10 @@ ansi-styles@^4.0.0: color-convert "^2.0.1" ansi-styles@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.0.tgz#5681f0dcf7ae5880a7841d8831c4724ed9cc0172" - integrity sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg== + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "@types/color-name" "^1.1.1" color-convert "^2.0.1" ansi-styles@^5.0.0: @@ -4749,22 +4748,17 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async@0.9.x: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= - async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" -async@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== +async@^3.2.0, async@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== asynckit@^0.4.0: version "0.4.0" @@ -4978,9 +4972,9 @@ babel-preset-react-app@^10.0.1: babel-plugin-transform-react-remove-prop-types "^0.4.24" balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.3.1: version "1.5.1" @@ -5094,6 +5088,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -5327,7 +5328,7 @@ chalk@2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -5344,7 +5345,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -6759,11 +6760,11 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= ejs@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" - integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== + version "3.1.7" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.7.tgz#c544d9c7f715783dd92f0bddcf73a59e6962d006" + integrity sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw== dependencies: - jake "^10.6.1" + jake "^10.8.5" electron-to-chromium@^1.4.17: version "1.4.68" @@ -7682,11 +7683,11 @@ file-loader@^6.2.0: schema-utils "^3.0.0" filelist@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" - integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== + version "1.0.3" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.3.tgz#448607750376484932f67ef1b9ff07386b036c83" + integrity sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q== dependencies: - minimatch "^3.0.4" + minimatch "^5.0.1" filesize@^8.0.6: version "8.0.7" @@ -9202,13 +9203,13 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jake@^10.6.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" - integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== dependencies: - async "0.9.x" - chalk "^2.4.2" + async "^3.2.3" + chalk "^4.0.2" filelist "^1.0.1" minimatch "^3.0.4" @@ -10583,13 +10584,27 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -10600,9 +10615,9 @@ minimist-options@4.1.0: kind-of "^6.0.3" minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== minipass-collect@^1.0.2: version "1.0.2" @@ -10829,9 +10844,9 @@ node-fetch@2.6.7, node-fetch@^2.6.1: whatwg-url "^5.0.0" node-forge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" - integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== + version "1.3.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.0.tgz#37a874ea723855f37db091e6c186e5b67a01d4b2" + integrity sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA== node-gyp@^5.0.2: version "5.0.5" @@ -11292,6 +11307,13 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= +p-limit@3.1.0, p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -11306,20 +11328,6 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-limit@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" - integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== - dependencies: - yocto-queue "^1.0.0" - p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -15735,8 +15743,3 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -yocto-queue@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==