From 7827bbbefa33e59957211dccfb5cae07ea3c48d5 Mon Sep 17 00:00:00 2001 From: sambokar Date: Tue, 24 Sep 2024 09:53:11 -0400 Subject: [PATCH] 1. cleaning tests -- tests need to be reimplemented in full to account for new table changes. 2. postvalidation API endpoint refined into dynamic routing collection. 3. dedicated page to show post-validation statistics added to measurements hub 4. postvalidationqueries table created in schema. schema structure updated to reflect this 5. layout.tsx file updated to increase debounce (race conditions seem to be occurring). useEffects loop were restructured to ensure that on login, sites will correctly load. 6. error fallback pages received minor edits/changes --- .../__tests__/api/cmprevalidation.test.tsx | 100 ---- frontend/__tests__/api/fetchall.test.tsx | 94 ---- .../api/filehandlers/deletefile.test.tsx | 85 --- .../filehandlers/downloadallfiles.test.tsx | 108 ---- .../api/filehandlers/downloadfile.test.tsx | 108 ---- .../api/filehandlers/storageload.test.tsx | 131 ----- frontend/__tests__/dashboard.test.tsx | 84 +++ frontend/__tests__/login.test.tsx | 59 ++ frontend/__tests__/loginpage.test.tsx | 49 -- frontend/__tests__/rollovermodal.test.tsx | 159 ------ frontend/__tests__/sidebar.test.tsx | 167 ++++++ frontend/app/(hub)/dashboard/error.tsx | 10 +- frontend/app/(hub)/error.tsx | 10 +- .../fixeddatainput/alltaxonomies/error.tsx | 10 +- .../(hub)/fixeddatainput/attributes/error.tsx | 10 +- .../(hub)/fixeddatainput/personnel/error.tsx | 10 +- .../fixeddatainput/quadratpersonnel/error.tsx | 10 +- .../(hub)/fixeddatainput/quadrats/error.tsx | 10 +- .../fixeddatainput/stemtaxonomies/error.tsx | 10 +- frontend/app/(hub)/layout.tsx | 50 +- .../measurementshub/postvalidation/error.tsx | 16 + .../measurementshub/postvalidation/page.tsx | 93 +++ .../(hub)/measurementshub/summary/error.tsx | 10 +- .../measurementshub/uploadedfiles/error.tsx | 10 +- .../measurementshub/validations/error.tsx | 116 +++- .../measurementshub/validations/page.tsx | 10 +- .../measurementshub/viewfulltable/error.tsx | 10 +- frontend/app/access-denied.tsx | 46 -- .../app/api/fetchall/[[...slugs]]/route.ts | 2 - frontend/app/api/postvalidation/route.ts | 530 +++++++++--------- .../[plotID]/[censusID]/[queryID]/route.ts | 36 ++ frontend/app/global-error.tsx | 31 +- frontend/components/client/rollovermodal.tsx | 2 +- .../components/client/rolloverstemsmodal.tsx | 2 +- frontend/components/loginlogout.tsx | 4 +- frontend/components/sidebar.tsx | 85 +-- frontend/config/datagridhelpers.ts | 1 - frontend/config/macros/siteconfigs.ts | 6 + frontend/middleware.ts | 8 - frontend/playwright.config.ts | 80 --- frontend/tests/attributes-datagrid.spec.ts | 75 --- 41 files changed, 939 insertions(+), 1508 deletions(-) delete mode 100644 frontend/__tests__/api/cmprevalidation.test.tsx delete mode 100644 frontend/__tests__/api/fetchall.test.tsx delete mode 100644 frontend/__tests__/api/filehandlers/deletefile.test.tsx delete mode 100644 frontend/__tests__/api/filehandlers/downloadallfiles.test.tsx delete mode 100644 frontend/__tests__/api/filehandlers/downloadfile.test.tsx delete mode 100644 frontend/__tests__/api/filehandlers/storageload.test.tsx create mode 100644 frontend/__tests__/dashboard.test.tsx create mode 100644 frontend/__tests__/login.test.tsx delete mode 100644 frontend/__tests__/loginpage.test.tsx delete mode 100644 frontend/__tests__/rollovermodal.test.tsx create mode 100644 frontend/__tests__/sidebar.test.tsx create mode 100644 frontend/app/(hub)/measurementshub/postvalidation/error.tsx create mode 100644 frontend/app/(hub)/measurementshub/postvalidation/page.tsx delete mode 100644 frontend/app/access-denied.tsx create mode 100644 frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts delete mode 100644 frontend/playwright.config.ts delete mode 100644 frontend/tests/attributes-datagrid.spec.ts diff --git a/frontend/__tests__/api/cmprevalidation.test.tsx b/frontend/__tests__/api/cmprevalidation.test.tsx deleted file mode 100644 index 20293687..00000000 --- a/frontend/__tests__/api/cmprevalidation.test.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { GET } from '@/app/api/cmprevalidation/[dataType]/[[...slugs]]/route'; -import { createMocks } from 'node-mocks-http'; -import { getConn, runQuery } from '@/components/processors/processormacros'; -import { HTTPResponses } from '@/config/macros'; -import { NextRequest } from 'next/server'; - -vi.mock('@/components/processors/processormacros', () => ({ - getConn: vi.fn(), - runQuery: vi.fn() -})); - -describe('GET /api/cmprevalidation/[dataType]/[[...slugs]]', () => { - it('should return 412 if required tables are empty', async () => { - const conn = { - query: vi.fn().mockResolvedValue([[]]), - release: vi.fn() - }; - - (getConn as jest.Mock).mockResolvedValue(conn); - (runQuery as jest.Mock).mockResolvedValue([]); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/cmprevalidation/attributes/schema/1/1' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq, { params: { dataType: 'attributes', slugs: ['schema', '1', '1'] } }); - - expect(response.status).toBe(HTTPResponses.PRECONDITION_VALIDATION_FAILURE); - }); - - it('should return 200 if required tables are populated', async () => { - const conn = { - query: vi.fn().mockResolvedValue([[1]]), - release: vi.fn() - }; - - (getConn as jest.Mock).mockResolvedValue(conn); - (runQuery as jest.Mock).mockResolvedValue([[1]]); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/cmprevalidation/attributes/schema/1/1' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq, { params: { dataType: 'attributes', slugs: ['schema', '1', '1'] } }); - - expect(response.status).toBe(HTTPResponses.OK); - }); - - it('should return 412 if there is a database error', async () => { - (getConn as jest.Mock).mockRejectedValue(new Error('Database error')); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/cmprevalidation/attributes/schema/1/1' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq, { params: { dataType: 'attributes', slugs: ['schema', '1', '1'] } }); - - expect(response.status).toBe(HTTPResponses.PRECONDITION_VALIDATION_FAILURE); - }); - - it('should return 400 if slugs are missing', async () => { - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/cmprevalidation/attributes' - }); - - const mockReq = new NextRequest(req.url); - - try { - await GET(mockReq, { params: { dataType: 'attributes', slugs: [] } }); - } catch (e) { - expect((e as Error).message).toBe('incorrect slugs provided'); - } - }); - - it('should return 400 if slugs are incorrect', async () => { - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/cmprevalidation/attributes/schema' - }); - - const mockReq = new NextRequest(req.url); - - try { - await GET(mockReq, { params: { dataType: 'attributes', slugs: ['schema'] } }); - } catch (e) { - expect((e as Error).message).toBe('incorrect slugs provided'); - } - }); -}); diff --git a/frontend/__tests__/api/fetchall.test.tsx b/frontend/__tests__/api/fetchall.test.tsx deleted file mode 100644 index 5ea7ff9f..00000000 --- a/frontend/__tests__/api/fetchall.test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { GET } from '@/app/api/fetchall/[[...slugs]]/route'; -import { getConn, runQuery } from '@/components/processors/processormacros'; -import MapperFactory, { IDataMapper } from '@/config/datamapper'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; - -// Mocking getConn and runQuery functions -vi.mock('@/components/processors/processormacros', () => ({ - getConn: vi.fn(), - runQuery: vi.fn() -})); - -// Mocking MapperFactory -vi.mock('@/config/datamapper', () => ({ - default: { - getMapper: vi.fn() - } -})); - -describe('GET /api/fetchall/[[...slugs]]', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should return 500 if schema is not provided', async () => { - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/fetchall/plots' - }); - - const mockReq = new NextRequest(req.url); - - await expect(GET(mockReq, { params: { slugs: ['plots'] } })).rejects.toThrow('Schema selection was not provided to API endpoint'); - }); - - it('should return 500 if fetchType is not provided', async () => { - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/fetchall?schema=test_schema' - }); - - const mockReq = new NextRequest(req.url); - - await expect(GET(mockReq, { params: { slugs: [] } })).rejects.toThrow('fetchType was not correctly provided'); - }); - - it('should return 200 and data if query is successful', async () => { - const mockConn = { release: vi.fn() }; - (getConn as ReturnType).mockResolvedValue(mockConn); - const mockResults = [{ PlotID: 1, PlotName: 'Plot 1' }]; - (runQuery as ReturnType).mockResolvedValue(mockResults); - - const mockMapper: IDataMapper = { - mapData: vi.fn().mockReturnValue([{ plotID: 1, plotName: 'Plot 1' }]), - demapData: vi.fn() - }; - (MapperFactory.getMapper as ReturnType).mockReturnValue(mockMapper); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/fetchall/plots?schema=test_schema' - }); - - const mockReq = new NextRequest(req.url); - const response = await GET(mockReq, { params: { slugs: ['plots'] } }); - - expect(response.status).toBe(200); - const data = await response.json(); - expect(data).toEqual([{ plotID: 1, plotName: 'Plot 1' }]); - expect(getConn).toHaveBeenCalled(); - expect(runQuery).toHaveBeenCalledWith(mockConn, expect.stringContaining('SELECT')); - expect(mockMapper.mapData).toHaveBeenCalledWith(mockResults); - expect(mockConn.release).toHaveBeenCalled(); - }); - - it('should return 500 if there is a database error', async () => { - const mockConn = { release: vi.fn() }; - (getConn as ReturnType).mockResolvedValue(mockConn); - (runQuery as ReturnType).mockRejectedValue(new Error('Database error')); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/fetchall/plots?schema=test_schema' - }); - - const mockReq = new NextRequest(req.url); - - await expect(GET(mockReq, { params: { slugs: ['plots'] } })).rejects.toThrow('Call failed'); - expect(getConn).toHaveBeenCalled(); - expect(runQuery).toHaveBeenCalledWith(mockConn, expect.stringContaining('SELECT')); - expect(mockConn.release).toHaveBeenCalled(); - }); -}); diff --git a/frontend/__tests__/api/filehandlers/deletefile.test.tsx b/frontend/__tests__/api/filehandlers/deletefile.test.tsx deleted file mode 100644 index 1bb7a42a..00000000 --- a/frontend/__tests__/api/filehandlers/deletefile.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { DELETE } from '@/app/api/filehandlers/deletefile/route'; -import { getContainerClient } from '@/config/macros/azurestorage'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; - -vi.mock('@/config/macros/azurestorage', () => ({ - getContainerClient: vi.fn() -})); - -describe('DELETE /api/filehandlers/deletefile', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should return 400 if container name or filename is missing', async () => { - const { req } = createMocks({ - method: 'DELETE', - url: 'http://localhost/api/filehandlers/deletefile' - }); - - const mockReq = new NextRequest(req.url); - - const response = await DELETE(mockReq); - expect(response.status).toBe(400); - const data = await response.text(); - expect(data).toBe('Container name and filename are required'); - }); - - it('should return 400 if container client creation fails', async () => { - (getContainerClient as ReturnType).mockResolvedValue(null); - - const { req } = createMocks({ - method: 'DELETE', - url: 'http://localhost/api/filehandlers/deletefile?container=testContainer&filename=testFile' - }); - - const mockReq = new NextRequest(req.url); - - const response = await DELETE(mockReq); - expect(response.status).toBe(400); - const data = await response.text(); - expect(data).toBe('Container name and filename are required'); - }); - - it('should return 200 and delete the file if successful', async () => { - const mockBlobClient = { - delete: vi.fn().mockResolvedValue({}) - }; - const mockContainerClient = { - getBlobClient: vi.fn().mockReturnValue(mockBlobClient) - }; - - (getContainerClient as ReturnType).mockResolvedValue(mockContainerClient); - - const { req } = createMocks({ - method: 'DELETE', - url: 'http://localhost/api/filehandlers/deletefile?container=testContainer&filename=testFile' - }); - - const mockReq = new NextRequest(req.url); - - const response = await DELETE(mockReq); - expect(response.status).toBe(200); - const data = await response.text(); - expect(data).toBe('File deleted successfully'); - expect(mockBlobClient.delete).toHaveBeenCalled(); - }); - - it('should return 500 if there is an error', async () => { - (getContainerClient as ReturnType).mockRejectedValue(new Error('Test error')); - - const { req } = createMocks({ - method: 'DELETE', - url: 'http://localhost/api/filehandlers/deletefile?container=testContainer&filename=testFile' - }); - - const mockReq = new NextRequest(req.url); - - const response = await DELETE(mockReq); - expect(response.status).toBe(500); - const data = await response.text(); - expect(data).toBe('Test error'); - }); -}); diff --git a/frontend/__tests__/api/filehandlers/downloadallfiles.test.tsx b/frontend/__tests__/api/filehandlers/downloadallfiles.test.tsx deleted file mode 100644 index 93d09db0..00000000 --- a/frontend/__tests__/api/filehandlers/downloadallfiles.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { GET } from '@/app/api/filehandlers/downloadallfiles/route'; -import { getContainerClient } from '@/config/macros/azurestorage'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; - -vi.mock('@/config/macros/azurestorage', () => ({ - getContainerClient: vi.fn() -})); - -describe('GET /api/filehandlers/downloadallfiles', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should return 400 if plot or census is not provided', async () => { - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/filehandlers/downloadallfiles' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq); - expect(response.status).toBe(400); - const data = await response.text(); - expect(data).toBe('Both plot and census parameters are required'); - }); - - it('should return 400 if container client creation fails', async () => { - (getContainerClient as ReturnType).mockResolvedValue(null); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/filehandlers/downloadallfiles?plot=testPlot&census=testCensus' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq); - expect(response.status).toBe(400); - const data = await response.json(); - expect(data.statusText).toBe('Container client creation error'); - }); - - it('should return 200 and list of blobs if successful', async () => { - const mockContainerClient = { - listBlobsFlat: vi.fn().mockImplementation(function* () { - yield { - name: 'testBlob', - metadata: { - user: 'testUser', - FormType: 'testFormType', - FileErrorState: JSON.stringify([{ stemtag: 'testStemtag', tag: 'testTag', validationErrorID: 1 }]) - }, - properties: { - lastModified: new Date() - } - }; - }) - }; - - (getContainerClient as ReturnType).mockResolvedValue(mockContainerClient); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/filehandlers/downloadallfiles?plot=testPlot&census=testCensus' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq); - expect(response.status).toBe(200); - const data = await response.json(); - expect(data.responseMessage).toBe('List of files'); - expect(data.blobData).toHaveLength(1); - expect(data.blobData[0]).toEqual({ - key: 1, - name: 'testBlob', - user: 'testUser', - formType: 'testFormType', - fileErrors: [{ stemtag: 'testStemtag', tag: 'testTag', validationErrorID: 1 }], - date: expect.any(String) // Date will be serialized to a string - }); - }); - - it('should return 400 if there is an error in blob listing', async () => { - const mockContainerClient = { - listBlobsFlat: vi.fn().mockImplementation(() => { - throw new Error('Blob listing error'); - }) - }; - - (getContainerClient as ReturnType).mockResolvedValue(mockContainerClient); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/filehandlers/downloadallfiles?plot=testPlot&census=testCensus' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq); - expect(response.status).toBe(400); - const data = await response.json(); - expect(data.message).toBe('Blob listing error'); - }); -}); diff --git a/frontend/__tests__/api/filehandlers/downloadfile.test.tsx b/frontend/__tests__/api/filehandlers/downloadfile.test.tsx deleted file mode 100644 index 6ac52b51..00000000 --- a/frontend/__tests__/api/filehandlers/downloadfile.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { GET } from '@/app/api/filehandlers/downloadfile/route'; -import { getContainerClient } from '@/config/macros/azurestorage'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; -import { BlobServiceClient, generateBlobSASQueryParameters, StorageSharedKeyCredential } from '@azure/storage-blob'; - -vi.mock('@azure/storage-blob', async () => { - const actual = await vi.importActual('@azure/storage-blob'); - return { - ...actual, - BlobServiceClient: { - fromConnectionString: vi.fn() - }, - generateBlobSASQueryParameters: vi.fn() - }; -}); - -vi.mock('@/config/macros/azurestorage', () => ({ - getContainerClient: vi.fn() -})); - -describe('GET /api/filehandlers/downloadfile', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should return 400 if container name, filename, or storage connection string is missing', async () => { - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/filehandlers/downloadfile' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq); - expect(response.status).toBe(400); - const data = await response.text(); - expect(data).toBe('Container name, filename, and storage connection string are required'); - }); - - it('should return 400 if container client creation fails', async () => { - process.env.AZURE_STORAGE_CONNECTION_STRING = 'test-connection-string'; - (getContainerClient as ReturnType).mockResolvedValue(null); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/filehandlers/downloadfile?container=testContainer&filename=testFile' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq); - expect(response.status).toBe(400); - const data = await response.text(); - expect(data).toBe('Failed to get container client'); - }); - - it('should return 200 and SAS token URL if successful', async () => { - process.env.AZURE_STORAGE_CONNECTION_STRING = 'test-connection-string'; - - const mockContainerClient = { - getBlobClient: vi.fn().mockReturnValue({ - url: 'https://testaccount.blob.core.windows.net/testcontainer/testblob' - }) - }; - - (getContainerClient as ReturnType).mockResolvedValue(mockContainerClient); - - (BlobServiceClient.fromConnectionString as ReturnType).mockReturnValue({ - credential: new StorageSharedKeyCredential('testaccount', 'testkey') - }); - - (generateBlobSASQueryParameters as ReturnType).mockReturnValue({ - toString: () => 'sastoken' - }); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/filehandlers/downloadfile?container=testContainer&filename=testFile' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq); - expect(response.status).toBe(200); - const data = await response.json(); - expect(data.url).toBe('https://testaccount.blob.core.windows.net/testcontainer/testblob?sastoken'); - }); - - it('should return 500 if there is an error', async () => { - process.env.AZURE_STORAGE_CONNECTION_STRING = 'test-connection-string'; - - (getContainerClient as ReturnType).mockRejectedValue(new Error('Test error')); - - const { req } = createMocks({ - method: 'GET', - url: 'http://localhost/api/filehandlers/downloadfile?container=testContainer&filename=testFile' - }); - - const mockReq = new NextRequest(req.url); - - const response = await GET(mockReq); - expect(response.status).toBe(500); - const data = await response.text(); - expect(data).toBe('Test error'); - }); -}); diff --git a/frontend/__tests__/api/filehandlers/storageload.test.tsx b/frontend/__tests__/api/filehandlers/storageload.test.tsx deleted file mode 100644 index ad11453f..00000000 --- a/frontend/__tests__/api/filehandlers/storageload.test.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { POST } from '@/app/api/filehandlers/storageload/route'; -import { getContainerClient, uploadValidFileAsBuffer } from '@/config/macros/azurestorage'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; - -vi.mock('@/config/macros/azurestorage', () => ({ - getContainerClient: vi.fn(), - uploadValidFileAsBuffer: vi.fn() -})); - -describe.skip('POST /api/filehandlers/storageload', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - const createMockRequest = (url: string, formData: FormData) => { - const { req } = createMocks({ - method: 'POST', - url: url, - headers: { - 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' - } - }); - - if (formData.get('file') === null) { - console.log('createMockRequest: received empty formData: ', formData); - return new NextRequest(req.url!, { method: 'POST' }); - } - req.formData = async () => formData; - - const headers = new Headers(); - for (const [key, value] of Object.entries(req.headers)) { - headers.append(key, value as string); - } - - const body = `------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="file"; filename="testfile.txt"\r\nContent-Type: text/plain\r\n\r\ntest content\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--`; - - return new NextRequest(req.url!, { method: 'POST', headers, body }); - }; - - it('should return 500 if container client creation fails', async () => { - (getContainerClient as ReturnType).mockRejectedValue(new Error('Test error')); - - const formData = new FormData(); - formData.append('file', new File(['test content'], 'testfile.txt')); - - const mockReq = createMockRequest( - 'http://localhost/api/filehandlers/storageload?fileName=testfile.txt&plot=testplot&census=testcensus&user=testuser&formType=testform', - formData - ); - - const response = await POST(mockReq); - expect(response.status).toBe(500); - const data = await response.json(); - expect(data.responseMessage).toBe('Error getting container client.'); - expect(data.error).toBe('Test error'); - }); - - it('should return 500 if file upload fails', async () => { - (getContainerClient as ReturnType).mockResolvedValue({}); - (uploadValidFileAsBuffer as ReturnType).mockRejectedValue(new Error('Upload error')); - - const formData = new FormData(); - formData.append('file', new File(['test content'], 'testfile.txt')); - - const mockReq = createMockRequest( - 'http://localhost/api/filehandlers/storageload?fileName=testfile.txt&plot=testplot&census=testcensus&user=testuser&formType=testform', - formData - ); - - const response = await POST(mockReq); - expect(response.status).toBe(500); - const data = await response.json(); - expect(data.responseMessage).toBe('File Processing error'); - expect(data.error).toBe('Upload error'); - }); - - it('should return 200 if file upload is successful', async () => { - const mockUploadResponse = { requestId: '12345', _response: { status: 200 } }; - (getContainerClient as ReturnType).mockResolvedValue({}); - (uploadValidFileAsBuffer as ReturnType).mockResolvedValue(mockUploadResponse); - - const formData = new FormData(); - formData.append('file', new File(['test content'], 'testfile.txt')); - - const mockReq = createMockRequest( - 'http://localhost/api/filehandlers/storageload?fileName=testfile.txt&plot=testplot&census=testcensus&user=testuser&formType=testform', - formData - ); - - const response = await POST(mockReq); - expect(response.status).toBe(200); - const data = await response.json(); - expect(data.message).toBe('Insert to Azure Storage successful'); - }); - - it('should return 400 if file is missing', async () => { - const formData = new FormData(); - console.log('test formData: ', formData); - - const mockReq = createMockRequest( - 'http://localhost/api/filehandlers/storageload?fileName=testfile.txt&plot=testplot&census=testcensus&user=testuser&formType=testform', - formData - ); - - const response = await POST(mockReq); - expect(response.status).toBe(400); - const data = await response.text(); - expect(data).toBe('File is required'); - }); - - it('should return 500 for unknown errors', async () => { - (getContainerClient as ReturnType).mockResolvedValue({}); - (uploadValidFileAsBuffer as ReturnType).mockRejectedValue('Unknown error'); - - const formData = new FormData(); - formData.append('file', new File(['test content'], 'testfile.txt')); - - const mockReq = createMockRequest( - 'http://localhost/api/filehandlers/storageload?fileName=testfile.txt&plot=testplot&census=testcensus&user=testuser&formType=testform', - formData - ); - - const response = await POST(mockReq); - expect(response.status).toBe(500); - const data = await response.json(); - expect(data.responseMessage).toBe('File Processing error'); - expect(data.error).toBe('Unknown error'); - }); -}); diff --git a/frontend/__tests__/dashboard.test.tsx b/frontend/__tests__/dashboard.test.tsx new file mode 100644 index 00000000..9e682f10 --- /dev/null +++ b/frontend/__tests__/dashboard.test.tsx @@ -0,0 +1,84 @@ +import { render, screen } from '@testing-library/react'; +import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import DashboardPage from '@/app/(hub)/dashboard/page'; +import { useSession } from 'next-auth/react'; +import '@testing-library/jest-dom/vitest'; +import { LockAnimationProvider } from '@/app/contexts/lockanimationcontext'; + +// Mock the useSession hook +vi.mock('next-auth/react', () => ({ + useSession: vi.fn() +})); + +// Define a mock session object +const mockSession = { + user: { + name: 'John Doe', + email: 'john.doe@example.com', + userStatus: 'admin', + sites: [ + { schemaName: 'site1', siteName: 'Site 1' }, + { schemaName: 'site2', siteName: 'Site 2' } + ] + } +}; + +describe.skip('DashboardPage Component', () => { + // Mock the authenticated session before all tests + beforeAll(() => { + (useSession as Mock).mockReturnValue({ data: mockSession, status: 'authenticated' }); + }); + + beforeEach(() => { + // Reset mocks before each test + vi.clearAllMocks(); + }); + + const renderWithProvider = () => { + return render( + + + + ); + }; + + it("displays the user's name", () => { + renderWithProvider(); + + // Assert that the user's name is displayed + expect(screen.getByText(/Welcome, John Doe!/i)).toBeInTheDocument(); + }); + + it("displays the user's email", () => { + renderWithProvider(); + + // Assert that the user's email is displayed + // To handle multiple instances of "Registered Email:" + const emails = screen.getAllByText(/Registered Email:/i); + expect(emails).length.greaterThanOrEqual(1); // Expect only one occurrence or handle all + expect(emails[0]).toBeInTheDocument(); + expect(screen.getByText(/john.doe@example.com/i)).toBeInTheDocument(); + }); + + it("displays the user's permission status", () => { + renderWithProvider(); + + // Same for "Assigned Role:" + const roles = screen.getAllByText(/Assigned Role:/i); + expect(roles).length.greaterThanOrEqual(1); // Handle according to your use case + expect(roles[0]).toBeInTheDocument(); + + expect(screen.getByText(/global/i)).toBeInTheDocument(); + }); + + it('displays the list of allowed sites', () => { + renderWithProvider(); + + // Assert that the allowed sites are displayed + const sites = screen.getAllByText(/You have access to the following sites:/i); + expect(sites).length.greaterThanOrEqual(1); // Handle according to your use case + expect(sites[0]).toBeInTheDocument(); + expect(screen.getByText(/Site 1/i)).toBeInTheDocument(); + expect(screen.getByText(/Site 2/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/__tests__/login.test.tsx b/frontend/__tests__/login.test.tsx new file mode 100644 index 00000000..33135b1d --- /dev/null +++ b/frontend/__tests__/login.test.tsx @@ -0,0 +1,59 @@ +import { render, screen } from '@testing-library/react'; +import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import LoginPage from '@/app/(login)/login/page'; +import { useSession } from 'next-auth/react'; +import { redirect } from 'next/navigation'; +import '@testing-library/jest-dom/vitest'; + +// Mock the useSession hook and next/navigation functions +vi.mock('next-auth/react', () => ({ + useSession: vi.fn() +})); + +vi.mock('next/navigation', () => ({ + redirect: vi.fn() +})); + +// Mock the UnauthenticatedSidebar component +vi.mock('@/components/unauthenticatedsidebar', () => ({ + default: () =>
Unauthenticated Sidebar
+})); + +// Define a mock session object to use across tests +const mockSession = { + user: { + email: 'user@example.com', + userStatus: 'admin', + sites: [{ name: 'Site 1' }, { name: 'Site 2' }], + allsites: [{ name: 'Site 1' }, { name: 'Site 2' }] + } +}; + +describe('LoginPage Component with authenticated session', () => { + // Set up the mock authenticated session once for all tests + beforeAll(() => { + (useSession as Mock).mockReturnValue({ data: mockSession, status: 'authenticated' }); + }); + + beforeEach(() => { + // Reset mocks before each test + vi.clearAllMocks(); + }); + + it('redirects to dashboard when the user is authenticated', () => { + render(); + + // Assert that redirect was called to navigate to the dashboard + expect(redirect).toHaveBeenCalledWith('/dashboard'); + }); + + // Add more tests here that assume the user is authenticated + it('does not render the unauthenticated sidebar when the user is authenticated', () => { + render(); + + // Assert that the unauthenticated sidebar is not present + expect(screen.queryByTestId('unauthenticated-sidebar')).not.toBeInTheDocument(); + }); + + // Additional tests can go here, all assuming the user is already logged in... +}); diff --git a/frontend/__tests__/loginpage.test.tsx b/frontend/__tests__/loginpage.test.tsx deleted file mode 100644 index 8c3b418e..00000000 --- a/frontend/__tests__/loginpage.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// loginPage.test.tsx - -import { render, screen } from '@testing-library/react'; -import { describe, it, vi, beforeEach, Mock, expect } from 'vitest'; -import LoginPage from '@/app/(login)/login/page'; -import { useSession } from 'next-auth/react'; -import { redirect } from 'next/navigation'; -import '@testing-library/jest-dom/vitest'; - -// Mock the useSession hook and next/navigation functions -vi.mock('next-auth/react', () => ({ - useSession: vi.fn() -})); - -vi.mock('next/navigation', () => ({ - redirect: vi.fn() -})); - -// Mock the UnauthenticatedSidebar component -vi.mock('@/components/unauthenticatedsidebar', () => ({ - default: () =>
Unauthenticated Sidebar
-})); - -describe('LoginPage Component', () => { - beforeEach(() => { - // Reset mocks before each test - vi.clearAllMocks(); - }); - - it('renders the unauthenticated sidebar if the user is unauthenticated', () => { - // Mock unauthenticated status - (useSession as Mock).mockReturnValue({ data: null, status: 'unauthenticated' }); - - render(); - - // Assert that the sidebar is present and visible - expect(screen.getByTestId('unauthenticated-sidebar')).toBeInTheDocument(); - }); - - it('redirects to dashboard if the user is authenticated', () => { - // Mock authenticated status - (useSession as Mock).mockReturnValue({ data: { user: {} }, status: 'authenticated' }); - - render(); - - // Assert that redirect was called to navigate to the dashboard - expect(redirect).toHaveBeenCalledWith('/dashboard'); - }); -}); diff --git a/frontend/__tests__/rollovermodal.test.tsx b/frontend/__tests__/rollovermodal.test.tsx deleted file mode 100644 index e176658e..00000000 --- a/frontend/__tests__/rollovermodal.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import RolloverModal from '@/components/client/rollovermodal'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -// Mock the fetch API -global.fetch = vi.fn(); - -// Mock contexts -vi.mock('@/app/contexts/userselectionprovider', () => ({ - useSiteContext: () => ({ schemaName: 'testSchema' }), - usePlotContext: () => ({ plotID: 1 }) -})); -vi.mock('@/app/contexts/listselectionprovider', () => ({ - useOrgCensusListContext: () => [ - { plotCensusNumber: 1, dateRanges: [{ censusID: 1 }] }, - { plotCensusNumber: 2, dateRanges: [{ censusID: 2 }] } - ] -})); - -// Mock Data -const previousPersonnel = [ - { personnelID: 1, name: 'Person 1' }, - { personnelID: 2, name: 'Person 2' } -]; -const previousQuadrats = [ - { quadratID: 1, name: 'Quadrat 1' }, - { quadratID: 2, name: 'Quadrat 2' } -]; - -describe.skip('RolloverModal Component', () => { - const setup = (props = {}) => render(); - - beforeEach(() => { - (global.fetch as jest.Mock).mockClear(); - - (global.fetch as jest.Mock).mockImplementation((url: string) => { - if (url.includes('/fetchall/personnel/')) { - return Promise.resolve({ - status: 200, - json: () => Promise.resolve(previousPersonnel) - }); - } - if (url.includes('/fetchall/quadrats/')) { - return Promise.resolve({ - status: 200, - json: () => Promise.resolve(previousQuadrats) - }); - } - if (url.includes('/cmprevalidation/personnel/')) { - return Promise.resolve({ - status: 200 - }); - } - if (url.includes('/cmprevalidation/quadrats/')) { - return Promise.resolve({ - status: 200 - }); - } - return Promise.reject(new Error('Unknown API call')); - }); - }); - - it('should open modal and display title', async () => { - setup(); - await waitFor(() => { - expect(screen.getByText(/Rollover Census Data/i)).toBeInTheDocument(); - }); - }); - - it('should show error if no checkbox is selected and confirm is pressed', async () => { - setup(); - fireEvent.click(screen.getByText(/Confirm/i)); - await waitFor(() => { - expect(screen.getByText(/You must select at least one option to roll over or confirm no rollover/i)).toBeInTheDocument(); - }); - }); - - it('should allow selecting and confirming personnel rollover', async () => { - setup(); - await waitFor(() => { - fireEvent.click(screen.getByLabelText(/Roll over personnel data/i)); - }); - fireEvent.click(screen.getByText(/Confirm/i)); - await waitFor(() => { - expect(screen.queryByText(/You must select at least one option to roll over or confirm no rollover/i)).toBeNull(); - }); - }); - - it('should allow selecting and confirming quadrats rollover', async () => { - setup(); - await waitFor(() => { - fireEvent.click(screen.getByLabelText(/Roll over quadrats data/i)); - }); - fireEvent.click(screen.getByText(/Confirm/i)); - await waitFor(() => { - expect(screen.queryByText(/You must select at least one option to roll over or confirm no rollover/i)).toBeNull(); - }); - }); - - it('should allow selecting and confirming both personnel and quadrats rollover', async () => { - setup(); - await waitFor(() => { - fireEvent.click(screen.getByLabelText(/Roll over personnel data/i)); - fireEvent.click(screen.getByLabelText(/Roll over quadrats data/i)); - }); - fireEvent.click(screen.getByText(/Confirm/i)); - await waitFor(() => { - expect(screen.queryByText(/You must select at least one option to roll over or confirm no rollover/i)).toBeNull(); - }); - }); - - it('should allow customizing personnel selection', async () => { - setup(); - await waitFor(() => { - fireEvent.click(screen.getByLabelText(/Roll over personnel data/i)); - fireEvent.click(screen.getByText(/Customize personnel selection/i)); - }); - expect(screen.getByText(/Person 1/i)).toBeInTheDocument(); - expect(screen.getByText(/Person 2/i)).toBeInTheDocument(); - }); - - it('should allow customizing quadrats selection', async () => { - setup(); - await waitFor(() => { - fireEvent.click(screen.getByLabelText(/Roll over quadrats data/i)); - fireEvent.click(screen.getByText(/Customize quadrats selection/i)); - }); - expect(screen.getByText(/Quadrat 1/i)).toBeInTheDocument(); - expect(screen.getByText(/Quadrat 2/i)).toBeInTheDocument(); - }); - - it('should confirm no rollover for personnel', async () => { - setup(); - fireEvent.mouseDown(screen.getByLabelText(/Do not roll over any Personnel data/i)); - fireEvent.click(screen.getByText(/Confirm No Rollover/i)); - fireEvent.click(screen.getByText(/Confirm/i)); - await waitFor(() => { - expect(screen.queryByText(/You must select at least one option to roll over or confirm no rollover/i)).toBeNull(); - }); - }); - - it('should confirm no rollover for quadrats', async () => { - setup(); - fireEvent.mouseDown(screen.getByLabelText(/Do not roll over any Quadrats data/i)); - fireEvent.click(screen.getByText(/Confirm No Rollover/i)); - fireEvent.click(screen.getByText(/Confirm/i)); - await waitFor(() => { - expect(screen.queryByText(/You must select at least one option to roll over or confirm no rollover/i)).toBeNull(); - }); - }); - - it('should handle error during fetch data', async () => { - (global.fetch as jest.Mock).mockImplementationOnce(() => Promise.reject(new Error('Failed to fetch'))); - setup(); - await waitFor(() => { - expect(screen.getByText(/Failed to fetch previous data. Please try again/i)).toBeInTheDocument(); - }); - }); -}); diff --git a/frontend/__tests__/sidebar.test.tsx b/frontend/__tests__/sidebar.test.tsx new file mode 100644 index 00000000..a330c7f6 --- /dev/null +++ b/frontend/__tests__/sidebar.test.tsx @@ -0,0 +1,167 @@ +// Mock ResizeObserver +class ResizeObserver { + observe() {} + + unobserve() {} + + disconnect() {} +} + +global.ResizeObserver = ResizeObserver; + +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import Sidebar from '@/components/sidebar'; +import { useSession } from 'next-auth/react'; +import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/contexts/userselectionprovider'; +import { useOrgCensusListContext, usePlotListContext, useSiteListContext } from '@/app/contexts/listselectionprovider'; +import '@testing-library/jest-dom/vitest'; +import { UserAuthRoles } from '@/config/macros'; +import { Session } from 'next-auth/core/types'; +import { CensusDateRange } from '@/config/sqlrdsdefinitions/timekeeping'; + +// Mock the necessary hooks +vi.mock('next-auth/react', () => ({ + useSession: vi.fn() +})); + +vi.mock('@/app/contexts/userselectionprovider', () => ({ + useSiteContext: vi.fn(), + usePlotContext: vi.fn(), + useOrgCensusContext: vi.fn(), + useSiteDispatch: vi.fn(), + usePlotDispatch: vi.fn(), + useOrgCensusDispatch: vi.fn() +})); + +vi.mock('@/app/contexts/listselectionprovider', () => ({ + useSiteListContext: vi.fn(), + usePlotListContext: vi.fn(), + useOrgCensusListContext: vi.fn(), + useSiteListDispatch: vi.fn(), + usePlotListDispatch: vi.fn(), + useOrgCensusListDispatch: vi.fn() +})); + +vi.mock('next/navigation', async () => { + const actual = await vi.importActual('next/navigation'); + return { + ...actual, + useRouter: vi.fn().mockReturnValue({ + route: '/', + pathname: '/', + query: {}, + asPath: '/', + push: vi.fn(), + replace: vi.fn(), + back: vi.fn() + }), + usePathname: vi.fn().mockReturnValue('/mock-path'), + useSearchParams: vi.fn().mockReturnValue({ + get: vi.fn() + }) + }; +}); + +describe.skip('Sidebar Component', () => { + // Mock session data + const mockSession = { + user: { + name: 'John Doe', + email: 'john.doe@example.com', + userStatus: 'global' as UserAuthRoles, + sites: [ + { siteID: 1, siteName: 'Site 1', schemaName: 'schema1' }, + { siteID: 2, siteName: 'Site 2', schemaName: 'schema2' } + ], + allsites: [ + { siteID: 1, siteName: 'Site 1', schemaName: 'schema1' }, + { siteID: 2, siteName: 'Site 2', schemaName: 'schema2' } + ] + }, + expires: '9999-12-31T23:59:59.999Z' // Add this line to satisfy the 'Session' type + }; + + // Mock site, plot, and census contexts + const mockSite = { siteID: 1, siteName: 'Site 1', schemaName: 'schema1' }; + const mockPlot = { plotID: 1, plotName: 'Plot 1', numQuadrats: 5 }; + const mockCensus = { + plotCensusNumber: 1, + dateRanges: [{ censusID: 1, startDate: new Date('2023-01-01'), endDate: new Date('2023-01-31') } as CensusDateRange], + plotID: 1, + censusIDs: [1, 2], + description: 'Test Census' + }; + const mockCensusList = [ + { + plotCensusNumber: 1, + dateRanges: [{ censusID: 1, startDate: new Date('2023-01-01'), endDate: new Date('2023-01-31') } as CensusDateRange], + plotID: 1, + censusIDs: [1, 2], + description: 'Test Census' + } + ]; + + beforeEach(() => { + vi.clearAllMocks(); + + // Mock session + vi.mocked(useSession).mockReturnValue({ + data: mockSession, + status: 'authenticated', + update: function (_data?: any): Promise { + throw new Error('Function not implemented.'); + } + }); + + // Mock contexts + vi.mocked(useSiteContext).mockReturnValue(mockSite); + vi.mocked(usePlotContext).mockReturnValue(mockPlot); + vi.mocked(useOrgCensusContext).mockReturnValue(mockCensus); + + // Mock list contexts + vi.mocked(useSiteListContext).mockReturnValue([mockSite]); + vi.mocked(usePlotListContext).mockReturnValue([mockPlot]); + vi.mocked(useOrgCensusListContext).mockReturnValue(mockCensusList); + }); + + it('renders the sidebar', async () => { + render(); + + // Check if the sidebar renders the user name and admin status + expect(screen.getByTestId('login-logout-component')).toBeInTheDocument(); + + // Check if the site, plot, and census dropdowns are rendered using data-testid + expect(screen.getByTestId('site-select-component')).toBeInTheDocument(); + expect(screen.getByTestId('plot-select-component')).toBeInTheDocument(); + expect(screen.getByTestId('census-select-component')).toBeInTheDocument(); + }); + + it('displays the selected site, plot, and census', async () => { + render(); + + // Check that the selected site, plot, and census are displayed correctly + expect(screen.getByTestId('selected-site-name')).toHaveTextContent('Site: Site 1'); + expect(screen.getByTestId('selected-plot-name')).toHaveTextContent('Plot: Plot 1'); + expect(screen.getByTestId('selected-census-plotcensusnumber')).toHaveTextContent('Census: 1'); + + // Check dates + expect(screen.getByTestId('selected-census-dates')).toHaveTextContent('First Record: Sun Jan 01 2023'); + expect(screen.getByTestId('selected-census-dates')).toHaveTextContent('Last Record: Tue Jan 31 2023'); + }); + + // it('opens the "Add New Census" modal when clicked', async () => { + // render(); + // + // // Find and click the "Add New Census" button + // const addCensusButton = screen.getByTestId('add-new-census-button'); + // expect(addCensusButton).toBeInTheDocument(); + // + // await act(async () => { + // fireEvent.click(addCensusButton); + // }); + // + // // Verify that the modal opens successfully using its test ID + // expect(screen.getByTestId('rollover-modal')).toBeInTheDocument(); + // }); +}); diff --git a/frontend/app/(hub)/dashboard/error.tsx b/frontend/app/(hub)/dashboard/error.tsx index b46fe9a0..5ca1d123 100644 --- a/frontend/app/(hub)/dashboard/error.tsx +++ b/frontend/app/(hub)/dashboard/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - Dashboard {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/(hub)/error.tsx b/frontend/app/(hub)/error.tsx index ac1f69fa..11c4c276 100644 --- a/frontend/app/(hub)/error.tsx +++ b/frontend/app/(hub)/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - Core Hub Layout Page {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/(hub)/fixeddatainput/alltaxonomies/error.tsx b/frontend/app/(hub)/fixeddatainput/alltaxonomies/error.tsx index d6cbebd7..4ea21499 100644 --- a/frontend/app/(hub)/fixeddatainput/alltaxonomies/error.tsx +++ b/frontend/app/(hub)/fixeddatainput/alltaxonomies/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - Species List Page {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/(hub)/fixeddatainput/attributes/error.tsx b/frontend/app/(hub)/fixeddatainput/attributes/error.tsx index 83821003..f5becb34 100644 --- a/frontend/app/(hub)/fixeddatainput/attributes/error.tsx +++ b/frontend/app/(hub)/fixeddatainput/attributes/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - Stem Codes Page {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/(hub)/fixeddatainput/personnel/error.tsx b/frontend/app/(hub)/fixeddatainput/personnel/error.tsx index 28baf540..415d1b09 100644 --- a/frontend/app/(hub)/fixeddatainput/personnel/error.tsx +++ b/frontend/app/(hub)/fixeddatainput/personnel/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - Personnel Page {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/(hub)/fixeddatainput/quadratpersonnel/error.tsx b/frontend/app/(hub)/fixeddatainput/quadratpersonnel/error.tsx index fbe6b55e..26a71a70 100644 --- a/frontend/app/(hub)/fixeddatainput/quadratpersonnel/error.tsx +++ b/frontend/app/(hub)/fixeddatainput/quadratpersonnel/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - QuadratPersonnel Page {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/(hub)/fixeddatainput/quadrats/error.tsx b/frontend/app/(hub)/fixeddatainput/quadrats/error.tsx index b8828d5b..e78eac6a 100644 --- a/frontend/app/(hub)/fixeddatainput/quadrats/error.tsx +++ b/frontend/app/(hub)/fixeddatainput/quadrats/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - Quadrats Page {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/(hub)/fixeddatainput/stemtaxonomies/error.tsx b/frontend/app/(hub)/fixeddatainput/stemtaxonomies/error.tsx index b8fb3d48..eaaea954 100644 --- a/frontend/app/(hub)/fixeddatainput/stemtaxonomies/error.tsx +++ b/frontend/app/(hub)/fixeddatainput/stemtaxonomies/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - Plot-Species List {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/(hub)/layout.tsx b/frontend/app/(hub)/layout.tsx index e67abde5..5e917153 100644 --- a/frontend/app/(hub)/layout.tsx +++ b/frontend/app/(hub)/layout.tsx @@ -74,7 +74,7 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { const quadratLastExecutedRef = useRef(null); // Debounce delay - const debounceDelay = 100; + const debounceDelay = 300; const fetchSiteList = useCallback(async () => { const now = Date.now(); @@ -175,52 +175,41 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { } }, [currentSite, currentPlot, currentCensus, quadratListLoaded, quadratListDispatch, setLoading]); + // Fetch site list if session exists and site list has not been loaded useEffect(() => { - if (currentSite && siteListLoaded) { - loadPlotData().catch(console.error); - } - if (currentSite && siteListLoaded && currentPlot && plotListLoaded) { - loadCensusData().catch(console.error); - } - if (currentSite && siteListLoaded && currentPlot && plotListLoaded && currentCensus && censusListLoaded) { - loadQuadratData().catch(console.error); - } - }, [currentSite, currentPlot, currentCensus, loadPlotData, loadCensusData, loadQuadratData]); - - // Fetch site list when siteListLoaded is false - useEffect(() => { - if (!siteListLoaded) { + // Ensure session is ready before attempting to fetch site list + if (session && !siteListLoaded) { fetchSiteList().catch(console.error); } - }, [siteListLoaded, fetchSiteList]); + }, [session, siteListLoaded, fetchSiteList]); - // Fetch plot data when plotListLoaded is false and currentSite is defined + // Fetch plot data when currentSite is defined and plotList has not been loaded useEffect(() => { if (currentSite && !plotListLoaded) { loadPlotData().catch(console.error); } - }, [plotListLoaded, currentSite, loadPlotData]); + }, [currentSite, plotListLoaded, loadPlotData]); - // Fetch census data when censusListLoaded is false and currentSite and currentPlot are defined + // Fetch census data when currentSite, currentPlot are defined and censusList has not been loaded useEffect(() => { if (currentSite && currentPlot && !censusListLoaded) { loadCensusData().catch(console.error); } - }, [censusListLoaded, currentSite, currentPlot, loadCensusData]); + }, [currentSite, currentPlot, censusListLoaded, loadCensusData]); - // Fetch quadrat data when quadratListLoaded is false and currentSite, currentPlot, and currentCensus are defined + // Fetch quadrat data when currentSite, currentPlot, currentCensus are defined and quadratList has not been loaded useEffect(() => { if (currentSite && currentPlot && currentCensus && !quadratListLoaded) { loadQuadratData().catch(console.error); } - }, [quadratListLoaded, currentSite, currentPlot, currentCensus, loadQuadratData]); + }, [currentSite, currentPlot, currentCensus, quadratListLoaded, loadQuadratData]); - // Manual reset logic + // Handle manual reset logic useEffect(() => { if (manualReset) { setLoading(true, 'Manual refresh beginning...'); - // Set all loaded states to false to trigger the re-fetching + // Reset all loading states setSiteListLoaded(false); setPlotListLoaded(false); setCensusListLoaded(false); @@ -230,13 +219,7 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { } }, [manualReset]); - // Fetch site list if session exists and site list has not been loaded - useEffect(() => { - if (session && !siteListLoaded) { - fetchSiteList().catch(console.error); - } - }, [fetchSiteList, session, siteListLoaded]); - + // Clear lists and reload data when site, plot, or census changes useEffect(() => { const hasSiteChanged = previousSiteRef.current !== currentSite?.siteName; const hasPlotChanged = previousPlotRef.current !== currentPlot?.plotID; @@ -288,18 +271,19 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { } }, [currentSite, currentPlot, currentCensus, plotListDispatch, censusListDispatch, quadratListDispatch, loadPlotData, loadCensusData, loadQuadratData]); + // Handle redirection if contexts are reset (i.e., no site, plot, or census) and user is not on the dashboard useEffect(() => { - // if contexts are reset due to website refresh, system needs to redirect user back to dashboard if (currentSite === undefined && currentPlot === undefined && currentCensus === undefined && pathname !== '/dashboard') { redirect('/dashboard'); } }, [pathname, currentSite, currentPlot, currentCensus]); + // Handle sidebar visibility based on session presence useEffect(() => { if (session) { const timer = setTimeout(() => { setSidebarVisible(true); - }, 300); // Debounce the sidebar visibility with a delay + }, 300); // Debounce sidebar visibility with a delay return () => clearTimeout(timer); } }, [session]); diff --git a/frontend/app/(hub)/measurementshub/postvalidation/error.tsx b/frontend/app/(hub)/measurementshub/postvalidation/error.tsx new file mode 100644 index 00000000..eaaea954 --- /dev/null +++ b/frontend/app/(hub)/measurementshub/postvalidation/error.tsx @@ -0,0 +1,16 @@ +'use client'; + +import React from 'react'; +import { Box, Button, Typography } from '@mui/joy'; + +const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { + return ( + + Something went wrong - Plot-Species List + {error.message} + + + ); +}; + +export default ErrorPage; diff --git a/frontend/app/(hub)/measurementshub/postvalidation/page.tsx b/frontend/app/(hub)/measurementshub/postvalidation/page.tsx new file mode 100644 index 00000000..86ecfb66 --- /dev/null +++ b/frontend/app/(hub)/measurementshub/postvalidation/page.tsx @@ -0,0 +1,93 @@ +'use client'; + +import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/contexts/userselectionprovider'; +import { useEffect, useState } from 'react'; +import { Box, LinearProgress } from '@mui/joy'; + +interface PostValidations { + queryID: number; + queryName: string; + queryDescription: string; +} + +interface PostValidationResults { + count: number; + data: any; +} + +export default function PostValidationPage() { + const currentSite = useSiteContext(); + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); + const [postValidations, setPostValidations] = useState([]); + const [validationResults, setValidationResults] = useState>({}); + const [loadingQueries, setLoadingQueries] = useState(false); + + // Fetch post-validation queries on first render + useEffect(() => { + async function loadQueries() { + try { + setLoadingQueries(true); + const response = await fetch(`/api/postvalidation?schema=${currentSite?.schemaName}`, { method: 'GET' }); + const data = await response.json(); + setPostValidations(data); + } catch (error) { + console.error('Error loading queries:', error); + } finally { + setLoadingQueries(false); + } + } + + if (currentSite?.schemaName) { + loadQueries(); + } + }, [currentSite?.schemaName]); + + // Fetch validation results for each query + useEffect(() => { + async function fetchValidationResults(postValidation: PostValidations) { + try { + const response = await fetch( + `/api/postvalidationbyquery/${currentSite?.schemaName}/${currentPlot?.plotID}/${currentCensus?.dateRanges[0].censusID}/${postValidation.queryID}`, + { method: 'GET' } + ); + const data = await response.json(); + setValidationResults(prev => ({ + ...prev, + [postValidation.queryID]: data + })); + } catch (error) { + console.error(`Error fetching validation results for query ${postValidation.queryID}:`, error); + setValidationResults(prev => ({ + ...prev, + [postValidation.queryID]: null // Mark as failed if there was an error + })); + } + } + + if (postValidations.length > 0 && currentPlot?.plotID && currentCensus?.dateRanges) { + postValidations.forEach(postValidation => { + fetchValidationResults(postValidation).then(r => console.log(r)); + }); + } + }, [postValidations, currentPlot?.plotID, currentCensus?.dateRanges, currentSite?.schemaName]); + + return ( + + {loadingQueries ? ( + + ) : postValidations.length > 0 ? ( + + {postValidations.map(postValidation => ( + +
{postValidation.queryName}
+ {validationResults[postValidation.queryID] ? : } +
+ ))} +
+ ) : ( +
No validations available.
+ )} +
+ ); +} diff --git a/frontend/app/(hub)/measurementshub/summary/error.tsx b/frontend/app/(hub)/measurementshub/summary/error.tsx index a7162a7d..c45668bf 100644 --- a/frontend/app/(hub)/measurementshub/summary/error.tsx +++ b/frontend/app/(hub)/measurementshub/summary/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - View Data Page {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/(hub)/measurementshub/uploadedfiles/error.tsx b/frontend/app/(hub)/measurementshub/uploadedfiles/error.tsx index da252b9e..1ed10f37 100644 --- a/frontend/app/(hub)/measurementshub/uploadedfiles/error.tsx +++ b/frontend/app/(hub)/measurementshub/uploadedfiles/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - Uploaded Files Page {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/(hub)/measurementshub/validations/error.tsx b/frontend/app/(hub)/measurementshub/validations/error.tsx index b0480ffa..51e41ab7 100644 --- a/frontend/app/(hub)/measurementshub/validations/error.tsx +++ b/frontend/app/(hub)/measurementshub/validations/error.tsx @@ -1,24 +1,104 @@ -'use client'; +'use client'; // Error boundaries must be Client Components -import React, { useEffect } from 'react'; -import { Box, Button, Typography } from '@mui/joy'; +import { useEffect } from 'react'; +import { Alert, Box, Button, Card, CardContent, Divider, Stack, Typography } from '@mui/joy'; +import CircularProgress from '@mui/joy/CircularProgress'; +import { Warning } from '@mui/icons-material'; +import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; -const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { +export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + const { data: session } = useSession(); useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - + // Log the error to an error reporting service + console.error(error); + }, [error]); + const router = useRouter(); + if (error.message === 'access-denied') { + return ( + + + + + } + sx={{ alignItems: 'flex-start', gap: '1rem' }} + > + + Access Denied + Unfortunately, you do not have access to this webpage. + + Your assigned role is + + {session?.user?.userStatus} + + + Please submit a GitHub issue if this is incorrect and you should have access to this page. + + + + + + + + ); + } return ( - - Something went wrong - Validation History Page - {error.message} - Retrying in 5 seconds... - + + + + + } + sx={{ alignItems: 'flex-start', gap: '1rem' }} + > + + Oh no! + Something unexpected seems to have went wrong. + Please provide the following metadata to an administrator so they can diagnose the problem further! + + + + Metadata + + + + + Error Message:{' '} + + {error.message} + + + + + Your assigned role is + + {session?.user?.userStatus} + + + Please submit a GitHub issue if this is incorrect and you should have access to this page. + + + + + + ); -}; - -export default ErrorPage; +} diff --git a/frontend/app/(hub)/measurementshub/validations/page.tsx b/frontend/app/(hub)/measurementshub/validations/page.tsx index 0218190c..119a76b7 100644 --- a/frontend/app/(hub)/measurementshub/validations/page.tsx +++ b/frontend/app/(hub)/measurementshub/validations/page.tsx @@ -5,14 +5,22 @@ import React, { useEffect, useState } from 'react'; import ValidationCard from '@/components/validationcard'; import { ValidationProceduresRDS } from '@/config/sqlrdsdefinitions/validations'; import { useSiteContext } from '@/app/contexts/userselectionprovider'; +import { useSession } from 'next-auth/react'; export default function ValidationsPage() { const [globalValidations, setGlobalValidations] = React.useState([]); const [loading, setLoading] = useState(true); // Use a loading state instead of refresh const [schemaDetails, setSchemaDetails] = useState<{ table_name: string; column_name: string }[]>([]); + const { data: session } = useSession(); const currentSite = useSiteContext(); + useEffect(() => { + if (session !== null && !['db admin', 'global'].includes(session.user.userStatus)) { + throw new Error('access-denied'); + } + }, []); + const handleSaveChanges = async (updatedValidation: ValidationProceduresRDS) => { try { // Make the API call to toggle the validation @@ -81,7 +89,7 @@ export default function ValidationsPage() { }; if (currentSite?.schemaName) { - fetchSchema(); + fetchSchema().then(r => console.log(r)); } }, [currentSite?.schemaName]); diff --git a/frontend/app/(hub)/measurementshub/viewfulltable/error.tsx b/frontend/app/(hub)/measurementshub/viewfulltable/error.tsx index 6612167f..568ba7e2 100644 --- a/frontend/app/(hub)/measurementshub/viewfulltable/error.tsx +++ b/frontend/app/(hub)/measurementshub/viewfulltable/error.tsx @@ -1,21 +1,13 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Button, Typography } from '@mui/joy'; const ErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); - return ( Something went wrong - View Full Table Page {error.message} - Retrying in 5 seconds... ); diff --git a/frontend/app/access-denied.tsx b/frontend/app/access-denied.tsx deleted file mode 100644 index ad2d1d6f..00000000 --- a/frontend/app/access-denied.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client'; - -import { Alert, Box, Button, Typography } from '@mui/joy'; -import { Warning } from '@mui/icons-material'; -import CircularProgress from '@mui/joy/CircularProgress'; -import { useSession } from 'next-auth/react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; - -export default function AccessDenied() { - const { data: session } = useSession(); - const router = useRouter(); - return ( - - - - - } - sx={{ alignItems: 'flex-start', gap: '1rem' }} - > - Access Denied - Unfortunately, you do not have access to this webpage. - Your assigned role is {session?.user?.userStatus} - Please submit a GitHub issue if this is incorrect and you should have access to this page. - - - - - n - - - - - ); -} diff --git a/frontend/app/api/fetchall/[[...slugs]]/route.ts b/frontend/app/api/fetchall/[[...slugs]]/route.ts index 14f39918..9cd93c6f 100644 --- a/frontend/app/api/fetchall/[[...slugs]]/route.ts +++ b/frontend/app/api/fetchall/[[...slugs]]/route.ts @@ -53,13 +53,11 @@ export async function GET(request: NextRequest, { params }: { params: { slugs?: console.log('fetchall --> slugs provided: fetchType: ', dataType, 'plotID: ', plotID, 'plotcensusnumber: ', plotCensusNumber, 'quadratID: ', quadratID); const query = buildQuery(schema, dataType, plotID, plotCensusNumber, quadratID); - console.log(query); let conn: PoolConnection | null = null; try { conn = await getConn(); const results = await runQuery(conn, query); - console.log(results); return new NextResponse(JSON.stringify(MapperFactory.getMapper(dataType).mapData(results)), { status: HTTPResponses.OK }); } catch (error) { console.error('Error:', error); diff --git a/frontend/app/api/postvalidation/route.ts b/frontend/app/api/postvalidation/route.ts index fa4f259f..0df42b31 100644 --- a/frontend/app/api/postvalidation/route.ts +++ b/frontend/app/api/postvalidation/route.ts @@ -1,265 +1,287 @@ import { NextRequest, NextResponse } from 'next/server'; import { getConn, runQuery } from '@/components/processors/processormacros'; -import { PoolConnection } from 'mysql2/promise'; import { HTTPResponses } from '@/config/macros'; export async function GET(request: NextRequest) { const schema = request.nextUrl.searchParams.get('schema'); if (!schema) throw new Error('no schema variable provided!'); - const currentPlotParam = request.nextUrl.searchParams.get('currentPlotParam'); - if (!currentPlotParam) throw new Error('no current PlotParam'); - const currentCensusParam = request.nextUrl.searchParams.get('currentCensusParam'); - if (!currentCensusParam) throw new Error('no current CensusParam'); - const currentPlotID = parseInt(currentPlotParam); - const currentCensusID = parseInt(currentCensusParam); - const queries = { - numRecordsByQuadrat: `SELECT q.QuadratID, COUNT(cm.CoreMeasurementID) AS MeasurementCount - FROM ${schema}.coremeasurements cm - JOIN ${schema}.quadrats q ON q.CensusID = cm.CensusID - WHERE cm.CensusID = ${currentCensusID} AND q.PlotID = ${currentPlotID} - GROUP BY QuadratID;`, - allStemRecords: `SELECT COUNT(s.StemID) AS TotalStems - FROM ${schema}.stems s - JOIN ${schema}.cmattributes cma ON s.StemID = cma.CoreMeasurementID - JOIN ${schema}.attributes a ON cma.Code = a.Code - JOIN ${schema}.quadrats q ON q.QuadratID = s.QuadratID - WHERE q.CensusID = ${currentCensusID} AND q.PlotID = ${currentPlotID};`, - liveStemRecords: `SELECT COUNT(s.StemID) AS LiveStems - FROM ${schema}.stems s - JOIN ${schema}.cmattributes cma ON s.StemID = cma.CoreMeasurementID - JOIN ${schema}.attributes a ON cma.Code = a.Code - JOIN ${schema}.quadrats q ON q.QuadratID = s.QuadratID - WHERE a.Status = 'alive' - AND q.CensusID = ${currentCensusID} AND q.PlotID = ${currentPlotID};`, - treeCount: `SELECT COUNT(t.TreeID) AS TotalTrees - FROM ${schema}.trees t - JOIN ${schema}.stems s ON s.TreeID = t.TreeID - JOIN ${schema}.quadrats q ON q.QuadratID = s.QuadratID - WHERE q.CensusID = ${currentCensusID} AND q.PlotID = ${currentPlotID};`, - countNumDeadMissingByCensus: `SELECT cm.CensusID, COUNT(s.StemID) AS DeadOrMissingStems - FROM ${schema}.stems s - JOIN ${schema}.cmattributes cma ON s.StemID = cma.CoreMeasurementID - JOIN ${schema}.attributes a ON cma.Code = a.Code - JOIN ${schema}.coremeasurements cm ON s.StemID = cm.StemID - WHERE a.Status IN ('dead', 'missing') - GROUP BY cm.CensusID;`, - treesOutsideLimits: `SELECT t.TreeID, s.LocalX, s.LocalY, p.DimensionX, p.DimensionY - FROM ${schema}.trees t - JOIN ${schema}.stems s ON t.TreeID = s.TreeID - JOIN ${schema}.quadrats q ON s.QuadratID = q.QuadratID - JOIN ${schema}.plots p ON q.PlotID = p.PlotID - WHERE s.LocalX IS NULL - OR s.LocalY IS NULL - OR s.LocalX > p.DimensionX - OR s.LocalY > p.DimensionY - AND p.PlotID = ${currentPlotID};`, - largestDBHHOMBySpecies: `SELECT sp.SpeciesID, sp.SpeciesName, MAX(cm.MeasuredDBH) AS LargestDBH, MAX(cm.MeasuredHOM) AS LargestHOM - FROM ${schema}.species sp - JOIN ${schema}.trees t ON sp.SpeciesID = t.SpeciesID - JOIN ${schema}.stems s ON s.TreeID = t.TreeID - JOIN ${schema}.coremeasurements cm ON cm.StemID = s.StemID - GROUP BY sp.SpeciesID, sp.SpeciesName;`, - allTreesFromLastCensusPresent: `SELECT t.TreeID, - t.TreeTag, - t.SpeciesID - FROM ${schema}.trees t - JOIN - ${schema}.stems s_last ON t.TreeID = s_last.TreeID - JOIN - ${schema}.coremeasurements cm_last ON s_last.StemID = cm_last.StemID - WHERE cm_last.CensusID = ${currentCensusID} - 1 - AND NOT EXISTS (SELECT 1 - FROM ${schema}.stems s_current - JOIN - ${schema}.coremeasurements cm_current ON s_current.StemID = cm_current.StemID - WHERE t.TreeID = s_current.TreeID - AND cm_current.CensusID = ${currentCensusID}) - GROUP BY t.TreeID, t.TreeTag, t.SpeciesID;`, - numNewStemsPerQuadratPerCensus: `SELECT q.QuadratName, - s_current.StemID, - s_current.StemTag, - s_current.TreeID, - s_current.QuadratID, - s_current.LocalX, - s_current.LocalY, - s_current.CoordinateUnits - FROM ${schema}.quadrats q - JOIN - ${schema}.stems s_current ON q.QuadratID = s_current.QuadratID - JOIN - ${schema}.coremeasurements cm_current ON s_current.StemID = cm_current.StemID - WHERE cm_current.CensusID = ${currentCensusID} - AND NOT EXISTS (SELECT 1 - FROM ${schema}.stems s_last - JOIN - ${schema}.coremeasurements cm_last ON s_last.StemID = cm_last.StemID - WHERE s_current.StemID = s_last.StemID - AND cm_last.CensusID = ${currentCensusID} - 1) - ORDER BY q.QuadratName, s_current.StemID;`, - numNewStemsMinMaxByQuadratPerCensus: `WITH NewStems AS (SELECT s_current.QuadratID, - s_current.StemID - FROM ${schema}.stems s_current - JOIN - ${schema}.coremeasurements cm_current ON s_current.StemID = cm_current.StemID - WHERE cm_current.CensusID = ${currentCensusID} - AND NOT EXISTS (SELECT 1 - FROM ${schema}.stems s_last - JOIN - ${schema}.coremeasurements cm_last ON s_last.StemID = cm_last.StemID - WHERE s_current.StemID = s_last.StemID - AND cm_last.CensusID = ${currentCensusID} - 1)), - NewStemCounts AS (SELECT q.QuadratID, - q.QuadratName, - COUNT(ns.StemID) AS NewStemCount - FROM ${schema}.quadrats q - LEFT JOIN - NewStems ns ON q.QuadratID = ns.QuadratID - GROUP BY q.QuadratID, q.QuadratName), - LeastNewStems AS (SELECT 'Least New Stems' AS StemType, - QuadratName, - NewStemCount - FROM NewStemCounts - ORDER BY NewStemCount, QuadratName - LIMIT 1), - MostNewStems AS (SELECT 'Most New Stems' AS StemType, - QuadratName, - NewStemCount - FROM NewStemCounts - ORDER BY NewStemCount DESC, QuadratName DESC - LIMIT 1) - SELECT * - FROM LeastNewStems - UNION ALL - SELECT * - FROM MostNewStems;`, - numDeadStemsPerQuadratPerCensus: `SELECT q.QuadratName, - s.StemID, - s.StemTag, - s.TreeID, - s.QuadratID, - s.LocalX, - s.LocalY, - s.CoordinateUnits, - a.Code AS AttributeCode, - a.Description AS AttributeDescription, - a.Status AS AttributeStatus - FROM ${schema}.quadrats q - JOIN - ${schema}.stems s ON q.QuadratID = s.QuadratID - JOIN - ${schema}.coremeasurements cm ON s.StemID = cm.StemID - JOIN - ${schema}.cmattributes cma ON cm.CoreMeasurementID = cma.CoreMeasurementID - JOIN - ${schema}.attributes a ON cma.Code = a.Code - WHERE cm.CensusID = ${currentCensusID} - AND a.Status = 'dead' - ORDER BY q.QuadratName, s.StemID;`, - numDeadStemsPerSpeciesPerCensus: `SELECT sp.SpeciesName, - sp.SpeciesCode, - s.StemID, - s.StemTag, - s.TreeID, - s.QuadratID, - s.LocalX, - s.LocalY, - s.CoordinateUnits, - a.Code AS AttributeCode, - a.Description AS AttributeDescription, - a.Status AS AttributeStatus - FROM ${schema}.stems s - JOIN - ${schema}.coremeasurements cm ON s.StemID = cm.StemID - JOIN - ${schema}.cmattributes cma ON cm.CoreMeasurementID = cma.CoreMeasurementID - JOIN - ${schema}.attributes a ON cma.Code = a.Code - JOIN - ${schema}.trees t ON s.TreeID = t.TreeID - JOIN - ${schema}.species sp ON t.SpeciesID = sp.SpeciesID - WHERE cm.CensusID = @currentCensusID - AND a.Status = 'dead' - ORDER BY sp.SpeciesName, s.StemID;` - }; - let conn: PoolConnection | null = null; - try { - conn = await getConn(); - const results = await Promise.all([ - runQuery(conn, queries.numRecordsByQuadrat), - runQuery(conn, queries.allStemRecords), - runQuery(conn, queries.liveStemRecords), - runQuery(conn, queries.treeCount), - runQuery(conn, queries.countNumDeadMissingByCensus), - runQuery(conn, queries.treesOutsideLimits), - runQuery(conn, queries.largestDBHHOMBySpecies), - runQuery(conn, queries.allTreesFromLastCensusPresent), - runQuery(conn, queries.numNewStemsPerQuadratPerCensus), - runQuery(conn, queries.numNewStemsMinMaxByQuadratPerCensus), - runQuery(conn, queries.numDeadStemsPerQuadratPerCensus), - runQuery(conn, queries.numDeadStemsPerSpeciesPerCensus) - ]); - - const totalMeasurementCount = results[0].reduce((sum: number, record: { MeasurementCount: number }) => sum + record.MeasurementCount, 0); - - const response = { - numRecordsByQuadrat: { - totalMeasurementCount, - data: results[0] - }, - allStemRecords: { - count: results[1].length, - data: results[1] - }, - liveStemRecords: { - count: results[2].length, - data: results[2] - }, - treeCount: { - count: results[3].length, - data: results[3] - }, - countNumDeadMissingByCensus: { - count: results[4].length, - data: results[4] - }, - treesOutsideLimits: { - count: results[5].length, - data: results[5] - }, - largestDBHHOMBySpecies: { - count: results[6].length, - data: results[6] - }, - allTreesFromLastCensusPresent: { - count: results[7].length, - data: results[7] - }, - numNewStemsPerQuadratPerCensus: { - count: results[8].length, - data: results[8] - }, - numNewStemsMinMaxByQuadratPerCensus: { - count: results[9].length, - data: results[9] - }, - numDeadStemsPerQuadratPerCensus: { - count: results[10].length, - data: results[10] - }, - numDeadStemsPerSpeciesPerCensus: { - count: results[11].length, - data: results[11] - } - }; - - return new NextResponse(JSON.stringify(response), { - status: HTTPResponses.OK + const conn = await getConn(); + const query = `SELECT QueryID, QueryName, Description FROM ${schema}.postvalidationqueries WHERE IsEnabled IS TRUE;`; + const results = await runQuery(conn, query); + if (results.length === 0) { + return new NextResponse(JSON.stringify({ message: 'No queries found' }), { + status: HTTPResponses.NOT_FOUND }); - } catch (error: any) { - throw new Error('Post-Summary Census Staistics: SQL query failed: ' + error.message); - } finally { - if (conn) conn.release(); } + const postValidations = results.map((row: any) => ({ + queryID: row.QueryID, + queryName: row.QueryName, + queryDescription: row.Description + })); + return new NextResponse(JSON.stringify(postValidations), { + status: HTTPResponses.OK + }); } + +// searchParams: schema, plot, census +// export async function GET(request: NextRequest) { +// const schema = request.nextUrl.searchParams.get('schema'); +// if (!schema) throw new Error('no schema variable provided!'); +// const currentPlotParam = request.nextUrl.searchParams.get('plot'); +// if (!currentPlotParam) throw new Error('no current PlotParam'); +// const currentCensusParam = request.nextUrl.searchParams.get('census'); +// if (!currentCensusParam) throw new Error('no current CensusParam'); +// const currentPlotID = parseInt(currentPlotParam); +// const currentCensusID = parseInt(currentCensusParam); +// const queries = { +// numRecordsByQuadrat: `SELECT q.QuadratID, COUNT(cm.CoreMeasurementID) AS MeasurementCount +// FROM ${schema}.coremeasurements cm +// JOIN ${schema}.quadrats q ON q.CensusID = cm.CensusID +// WHERE cm.CensusID = ${currentCensusID} AND q.PlotID = ${currentPlotID} +// GROUP BY QuadratID;`, +// allStemRecords: `SELECT COUNT(s.StemID) AS TotalStems +// FROM ${schema}.stems s +// JOIN ${schema}.cmattributes cma ON s.StemID = cma.CoreMeasurementID +// JOIN ${schema}.attributes a ON cma.Code = a.Code +// JOIN ${schema}.quadrats q ON q.QuadratID = s.QuadratID +// WHERE q.CensusID = ${currentCensusID} AND q.PlotID = ${currentPlotID};`, +// liveStemRecords: `SELECT COUNT(s.StemID) AS LiveStems +// FROM ${schema}.stems s +// JOIN ${schema}.cmattributes cma ON s.StemID = cma.CoreMeasurementID +// JOIN ${schema}.attributes a ON cma.Code = a.Code +// JOIN ${schema}.quadrats q ON q.QuadratID = s.QuadratID +// WHERE a.Status = 'alive' +// AND q.CensusID = ${currentCensusID} AND q.PlotID = ${currentPlotID};`, +// treeCount: `SELECT COUNT(t.TreeID) AS TotalTrees +// FROM ${schema}.trees t +// JOIN ${schema}.stems s ON s.TreeID = t.TreeID +// JOIN ${schema}.quadrats q ON q.QuadratID = s.QuadratID +// WHERE q.CensusID = ${currentCensusID} AND q.PlotID = ${currentPlotID};`, +// countNumDeadMissingByCensus: `SELECT cm.CensusID, COUNT(s.StemID) AS DeadOrMissingStems +// FROM ${schema}.stems s +// JOIN ${schema}.cmattributes cma ON s.StemID = cma.CoreMeasurementID +// JOIN ${schema}.attributes a ON cma.Code = a.Code +// JOIN ${schema}.coremeasurements cm ON s.StemID = cm.StemID +// WHERE a.Status IN ('dead', 'missing') +// GROUP BY cm.CensusID;`, +// treesOutsideLimits: `SELECT t.TreeID, s.LocalX, s.LocalY, p.DimensionX, p.DimensionY +// FROM ${schema}.trees t +// JOIN ${schema}.stems s ON t.TreeID = s.TreeID +// JOIN ${schema}.quadrats q ON s.QuadratID = q.QuadratID +// JOIN ${schema}.plots p ON q.PlotID = p.PlotID +// WHERE s.LocalX IS NULL +// OR s.LocalY IS NULL +// OR s.LocalX > p.DimensionX +// OR s.LocalY > p.DimensionY +// AND p.PlotID = ${currentPlotID};`, +// largestDBHHOMBySpecies: `SELECT sp.SpeciesID, sp.SpeciesName, MAX(cm.MeasuredDBH) AS LargestDBH, MAX(cm.MeasuredHOM) AS LargestHOM +// FROM ${schema}.species sp +// JOIN ${schema}.trees t ON sp.SpeciesID = t.SpeciesID +// JOIN ${schema}.stems s ON s.TreeID = t.TreeID +// JOIN ${schema}.coremeasurements cm ON cm.StemID = s.StemID +// GROUP BY sp.SpeciesID, sp.SpeciesName;`, +// allTreesFromLastCensusPresent: `SELECT t.TreeID, +// t.TreeTag, +// t.SpeciesID +// FROM ${schema}.trees t +// JOIN +// ${schema}.stems s_last ON t.TreeID = s_last.TreeID +// JOIN +// ${schema}.coremeasurements cm_last ON s_last.StemID = cm_last.StemID +// WHERE cm_last.CensusID = ${currentCensusID} - 1 +// AND NOT EXISTS (SELECT 1 +// FROM ${schema}.stems s_current +// JOIN +// ${schema}.coremeasurements cm_current ON s_current.StemID = cm_current.StemID +// WHERE t.TreeID = s_current.TreeID +// AND cm_current.CensusID = ${currentCensusID}) +// GROUP BY t.TreeID, t.TreeTag, t.SpeciesID;`, +// numNewStemsPerQuadratPerCensus: `SELECT q.QuadratName, +// s_current.StemID, +// s_current.StemTag, +// s_current.TreeID, +// s_current.QuadratID, +// s_current.LocalX, +// s_current.LocalY, +// s_current.CoordinateUnits +// FROM ${schema}.quadrats q +// JOIN +// ${schema}.stems s_current ON q.QuadratID = s_current.QuadratID +// JOIN +// ${schema}.coremeasurements cm_current ON s_current.StemID = cm_current.StemID +// WHERE cm_current.CensusID = ${currentCensusID} +// AND NOT EXISTS (SELECT 1 +// FROM ${schema}.stems s_last +// JOIN +// ${schema}.coremeasurements cm_last ON s_last.StemID = cm_last.StemID +// WHERE s_current.StemID = s_last.StemID +// AND cm_last.CensusID = ${currentCensusID} - 1) +// ORDER BY q.QuadratName, s_current.StemID;`, +// numNewStemsMinMaxByQuadratPerCensus: `WITH NewStems AS (SELECT s_current.QuadratID, +// s_current.StemID +// FROM ${schema}.stems s_current +// JOIN +// ${schema}.coremeasurements cm_current ON s_current.StemID = cm_current.StemID +// WHERE cm_current.CensusID = ${currentCensusID} +// AND NOT EXISTS (SELECT 1 +// FROM ${schema}.stems s_last +// JOIN +// ${schema}.coremeasurements cm_last ON s_last.StemID = cm_last.StemID +// WHERE s_current.StemID = s_last.StemID +// AND cm_last.CensusID = ${currentCensusID} - 1)), +// NewStemCounts AS (SELECT q.QuadratID, +// q.QuadratName, +// COUNT(ns.StemID) AS NewStemCount +// FROM ${schema}.quadrats q +// LEFT JOIN +// NewStems ns ON q.QuadratID = ns.QuadratID +// GROUP BY q.QuadratID, q.QuadratName), +// LeastNewStems AS (SELECT 'Least New Stems' AS StemType, +// QuadratName, +// NewStemCount +// FROM NewStemCounts +// ORDER BY NewStemCount, QuadratName +// LIMIT 1), +// MostNewStems AS (SELECT 'Most New Stems' AS StemType, +// QuadratName, +// NewStemCount +// FROM NewStemCounts +// ORDER BY NewStemCount DESC, QuadratName DESC +// LIMIT 1) +// SELECT * +// FROM LeastNewStems +// UNION ALL +// SELECT * +// FROM MostNewStems;`, +// numDeadStemsPerQuadratPerCensus: `SELECT q.QuadratName, +// s.StemID, +// s.StemTag, +// s.TreeID, +// s.QuadratID, +// s.LocalX, +// s.LocalY, +// s.CoordinateUnits, +// a.Code AS AttributeCode, +// a.Description AS AttributeDescription, +// a.Status AS AttributeStatus +// FROM ${schema}.quadrats q +// JOIN +// ${schema}.stems s ON q.QuadratID = s.QuadratID +// JOIN +// ${schema}.coremeasurements cm ON s.StemID = cm.StemID +// JOIN +// ${schema}.cmattributes cma ON cm.CoreMeasurementID = cma.CoreMeasurementID +// JOIN +// ${schema}.attributes a ON cma.Code = a.Code +// WHERE cm.CensusID = ${currentCensusID} +// AND a.Status = 'dead' +// ORDER BY q.QuadratName, s.StemID;`, +// numDeadStemsPerSpeciesPerCensus: `SELECT sp.SpeciesName, +// sp.SpeciesCode, +// s.StemID, +// s.StemTag, +// s.TreeID, +// s.QuadratID, +// s.LocalX, +// s.LocalY, +// s.CoordinateUnits, +// a.Code AS AttributeCode, +// a.Description AS AttributeDescription, +// a.Status AS AttributeStatus +// FROM ${schema}.stems s +// JOIN +// ${schema}.coremeasurements cm ON s.StemID = cm.StemID +// JOIN +// ${schema}.cmattributes cma ON cm.CoreMeasurementID = cma.CoreMeasurementID +// JOIN +// ${schema}.attributes a ON cma.Code = a.Code +// JOIN +// ${schema}.trees t ON s.TreeID = t.TreeID +// JOIN +// ${schema}.species sp ON t.SpeciesID = sp.SpeciesID +// WHERE cm.CensusID = @currentCensusID +// AND a.Status = 'dead' +// ORDER BY sp.SpeciesName, s.StemID;` +// }; +// +// let conn: PoolConnection | null = null; +// try { +// conn = await getConn(); +// const results = await Promise.all([ +// runQuery(conn, queries.numRecordsByQuadrat), +// runQuery(conn, queries.allStemRecords), +// runQuery(conn, queries.liveStemRecords), +// runQuery(conn, queries.treeCount), +// runQuery(conn, queries.countNumDeadMissingByCensus), +// runQuery(conn, queries.treesOutsideLimits), +// runQuery(conn, queries.largestDBHHOMBySpecies), +// runQuery(conn, queries.allTreesFromLastCensusPresent), +// runQuery(conn, queries.numNewStemsPerQuadratPerCensus), +// runQuery(conn, queries.numNewStemsMinMaxByQuadratPerCensus), +// runQuery(conn, queries.numDeadStemsPerQuadratPerCensus), +// runQuery(conn, queries.numDeadStemsPerSpeciesPerCensus) +// ]); +// +// const totalMeasurementCount = results[0].reduce((sum: number, record: { MeasurementCount: number }) => sum + record.MeasurementCount, 0); +// +// const response = { +// numRecordsByQuadrat: { +// totalMeasurementCount, +// data: results[0] +// }, +// allStemRecords: { +// count: results[1].length, +// data: results[1] +// }, +// liveStemRecords: { +// count: results[2].length, +// data: results[2] +// }, +// treeCount: { +// count: results[3].length, +// data: results[3] +// }, +// countNumDeadMissingByCensus: { +// count: results[4].length, +// data: results[4] +// }, +// treesOutsideLimits: { +// count: results[5].length, +// data: results[5] +// }, +// largestDBHHOMBySpecies: { +// count: results[6].length, +// data: results[6] +// }, +// allTreesFromLastCensusPresent: { +// count: results[7].length, +// data: results[7] +// }, +// numNewStemsPerQuadratPerCensus: { +// count: results[8].length, +// data: results[8] +// }, +// numNewStemsMinMaxByQuadratPerCensus: { +// count: results[9].length, +// data: results[9] +// }, +// numDeadStemsPerQuadratPerCensus: { +// count: results[10].length, +// data: results[10] +// }, +// numDeadStemsPerSpeciesPerCensus: { +// count: results[11].length, +// data: results[11] +// } +// }; +// +// return new NextResponse(JSON.stringify(response), { +// status: HTTPResponses.OK +// }); +// } catch (error: any) { +// throw new Error('Post-Summary Census Staistics: SQL query failed: ' + error.message); +// } finally { +// if (conn) conn.release(); +// } +// } diff --git a/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts b/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts new file mode 100644 index 00000000..90a100dc --- /dev/null +++ b/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { HTTPResponses } from '@/config/macros'; +import { getConn, runQuery } from '@/components/processors/processormacros'; + +export async function GET(_request: NextRequest, { params }: { params: { schema: string; plotID: string; censusID: string; queryID: string } }) { + const { schema } = params; + const plotID = parseInt(params.plotID); + const censusID = parseInt(params.censusID); + const queryID = parseInt(params.queryID); + if (!schema || !plotID || !censusID || !queryID) { + return new NextResponse('Missing parameters', { status: HTTPResponses.INVALID_REQUEST }); + } + const conn = await getConn(); + const query = `SELECT QueryDefinition FROM ${schema}.postvalidationqueries WHERE QueryID = ${queryID}`; + const results = await runQuery(conn, query); + if (results.length === 0) { + return new NextResponse('Query not found', { status: HTTPResponses.NOT_FOUND }); + } + const replacements = { + schema: schema, + currentPlotID: plotID, + currentCensusID: censusID + }; + const formattedQuery = results[0].QueryDefinition.replace(/\${(.*?)}/g, (_match: any, p1: string) => replacements[p1 as keyof typeof replacements]); + const queryResults = await runQuery(conn, formattedQuery); + if (queryResults.length === 0) { + return new NextResponse('Query returned no results', { status: HTTPResponses.NOT_FOUND }); + } + return new NextResponse( + JSON.stringify({ + count: queryResults.length, + data: queryResults + }), + { status: HTTPResponses.OK } + ); +} diff --git a/frontend/app/global-error.tsx b/frontend/app/global-error.tsx index b11b2b17..377eaa8a 100644 --- a/frontend/app/global-error.tsx +++ b/frontend/app/global-error.tsx @@ -1,24 +1,13 @@ -'use client'; - -import React, { useEffect } from 'react'; -import { Box, Button, Typography } from '@mui/joy'; - -const GlobalErrorPage = ({ error, reset }: { error: Error; reset: () => void }) => { - useEffect(() => { - const timer = setTimeout(() => { - reset(); - }, 5000); - return () => clearTimeout(timer); - }, [reset]); +'use client'; // Error boundaries must be Client Components +export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { return ( - - An unexpected error occurred - {error.message} - Retrying in 5 seconds... - - + // global-error must include html and body tags + + +

Something went wrong!

+ + + ); -}; - -export default GlobalErrorPage; +} diff --git a/frontend/components/client/rollovermodal.tsx b/frontend/components/client/rollovermodal.tsx index ee486444..69db9cb9 100644 --- a/frontend/components/client/rollovermodal.tsx +++ b/frontend/components/client/rollovermodal.tsx @@ -278,7 +278,7 @@ export default function RolloverModal(props: RolloverModalProps) { } return ( - + + diff --git a/frontend/components/loginlogout.tsx b/frontend/components/loginlogout.tsx index adfc2731..1199c727 100644 --- a/frontend/components/loginlogout.tsx +++ b/frontend/components/loginlogout.tsx @@ -25,7 +25,7 @@ export const LoginLogout = () => { if (status == 'unauthenticated') { return ( - + UNK @@ -40,7 +40,7 @@ export const LoginLogout = () => { ); } else { return ( - + {typeof session?.user?.name == 'string' diff --git a/frontend/components/sidebar.tsx b/frontend/components/sidebar.tsx index 96465cbb..80343b76 100644 --- a/frontend/components/sidebar.tsx +++ b/frontend/components/sidebar.tsx @@ -51,6 +51,7 @@ export function SimpleToggler({ isOpen, renderToggle, children }: Readonly {renderToggle} - + - + {siteConfigProps.label} @@ -297,7 +300,7 @@ export default function Sidebar(props: SidebarProps) { const renderSiteValue = (option: SelectOption | null) => { if (!option) { - return Select a Site; + return Select a Site; } const selectedValue = option.value; @@ -306,15 +309,15 @@ export default function Sidebar(props: SidebarProps) { <> {selectedSite ? ( - {`Site: ${selectedSite?.siteName}`} + {`Site: ${selectedSite?.siteName}`} - + — Schema: {selectedSite.schemaName} ) : ( - + Select a Site )} @@ -324,7 +327,7 @@ export default function Sidebar(props: SidebarProps) { const renderPlotValue = (option: SelectOption | null) => { if (!option) { - return Select a Plot; + return Select a Plot; } const selectedValue = option.value; @@ -334,15 +337,17 @@ export default function Sidebar(props: SidebarProps) { <> {selectedPlot ? ( - {`Plot: ${selectedPlot?.plotName}`} + {`Plot: ${selectedPlot?.plotName}`} - + — {selectedPlot.numQuadrats || selectedPlot.numQuadrats === 0 ? `Quadrats: ${selectedPlot.numQuadrats}` : 'No Quadrats'} ) : ( - Select a Plot + + Select a Plot + )} ); @@ -350,7 +355,7 @@ export default function Sidebar(props: SidebarProps) { const renderCensusValue = (option: SelectOption | null) => { if (!option) { - return Select a Census; + return Select a Census; } const selectedValue = option.value; @@ -377,9 +382,13 @@ export default function Sidebar(props: SidebarProps) { return ( - {`Census: ${selectedCensus?.plotCensusNumber}`} + {`Census: ${selectedCensus?.plotCensusNumber}`} - + {census !== undefined && dateMessage} @@ -409,6 +418,7 @@ export default function Sidebar(props: SidebarProps) { autoFocus size={'md'} renderValue={renderCensusValue} + data-testid={'census-select-component'} onChange={async (_event: React.SyntheticEvent | null, selectedPlotCensusNumberStr: string | null) => { if (selectedPlotCensusNumberStr === '' || selectedPlotCensusNumberStr === null) await handleCensusSelection(undefined); else { @@ -436,6 +446,7 @@ export default function Sidebar(props: SidebarProps) { { event.stopPropagation(); handleOpenNewCensus(); @@ -449,7 +460,7 @@ export default function Sidebar(props: SidebarProps) { {censusListContext ?.sort((a, b) => (b?.plotCensusNumber ?? 0) - (a?.plotCensusNumber ?? 0)) .map(item => ( - {allowedSites?.map(site => ( - ))} @@ -589,7 +606,7 @@ export default function Sidebar(props: SidebarProps) { {otherSites?.map(site => ( - ))} @@ -669,11 +686,6 @@ export default function Sidebar(props: SidebarProps) { ForestGEO - {session?.user.userStatus !== 'field crew' && ( - - (Admin) - - )} @@ -685,7 +697,7 @@ export default function Sidebar(props: SidebarProps) {
{site !== undefined && ( <> - + @@ -693,7 +705,7 @@ export default function Sidebar(props: SidebarProps) { {plot !== undefined && ( <> - + @@ -783,15 +795,17 @@ export default function Sidebar(props: SidebarProps) { direction="down" > setHoveredIndex(index)} onMouseLeave={() => setHoveredIndex(null)} > {site !== undefined && plot !== undefined && census !== undefined ? ( - + ) : ( - + setHoveredIndex(index)} onMouseLeave={() => setHoveredIndex(null)} @@ -881,11 +896,17 @@ export default function Sidebar(props: SidebarProps) { return ( - + setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)} + > {site !== undefined && plot !== undefined && census !== undefined ? ( - + ) : ( - + col.field === 'actions' || diff --git a/frontend/config/macros/siteconfigs.ts b/frontend/config/macros/siteconfigs.ts index 195c10c0..a900b19a 100644 --- a/frontend/config/macros/siteconfigs.ts +++ b/frontend/config/macros/siteconfigs.ts @@ -66,6 +66,12 @@ export const siteConfigNav: SiteConfigProps[] = [ tip: '', icon: VisibilityIcon }, + { + label: 'Post-Census Statistics', + href: '/postvalidation', + tip: '', + icon: VisibilityIcon + }, { label: 'Uploaded Files', href: '/uploadedfiles', diff --git a/frontend/middleware.ts b/frontend/middleware.ts index e91173e8..6af284ff 100644 --- a/frontend/middleware.ts +++ b/frontend/middleware.ts @@ -20,14 +20,6 @@ export async function middleware(request: NextRequest) { // If user is not authenticated and tries to access protected routes, redirect to login url.pathname = '/login'; return NextResponse.redirect(url); - } else { - if (url.pathname.startsWith('/measurementshub/validations')) { - const status = session.userStatus; - if (status !== 'global' && status !== 'db admin') { - url.pathname = '/access-denied'; - return NextResponse.redirect(url); - } - } } } else if (url.pathname === '/') { // Redirect from home to dashboard if authenticated, or login if not diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts deleted file mode 100644 index cab99621..00000000 --- a/frontend/playwright.config.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -import dotenv from 'dotenv'; -import path from 'path'; - -dotenv.config({ path: path.resolve(__dirname, '.env.local') }); - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './tests', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry' - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] } - }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] } - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] } - } - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], - - /* Run your local dev server before starting the tests */ - webServer: { - command: 'npm run dev', - url: 'http://localhost:3000', - reuseExistingServer: !process.env.CI - } -}); diff --git a/frontend/tests/attributes-datagrid.spec.ts b/frontend/tests/attributes-datagrid.spec.ts deleted file mode 100644 index 2b97d16e..00000000 --- a/frontend/tests/attributes-datagrid.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { expect, test } from '@playwright/test'; -import dotenv from 'dotenv'; - -// Load environment variables from the .env file -dotenv.config(); // Make sure this is included - -test.describe('Attributes Data Upload with Azure AD Login', () => { - test.beforeEach(async ({ page }) => { - // Log environment variables to verify they are loaded (for debugging) - console.log('Azure AD User Email:', process.env.AZURE_AD_USER_EMAIL); - console.log('Azure AD User Password:', process.env.AZURE_AD_USER_PASSWORD); - - // Step 1: Log in to the website using Azure AD - await page.goto('/api/auth/signin'); - - // Fill in the Azure AD login details using environment variables - await page.fill('input[name="loginfmt"]', process.env.AZURE_AD_USER_EMAIL!); - await page.click('input[type="submit"]'); - - // Wait for the password field and input the password - await page.fill('input[name="passwd"]', process.env.AZURE_AD_USER_PASSWORD!); - await page.click('input[type="submit"]'); - - // Handle any additional Azure AD prompts like "Stay signed in?" - const staySignedInButton = page.locator('input[id="idSIButton9"]'); - if (await staySignedInButton.isVisible()) { - await staySignedInButton.click(); - } - - // Step 2: Wait for the session to be authenticated - await page.waitForURL('/dashboard'); // Assuming the user is redirected to /dashboard after login - - // Step 3: Select a Site, Plot, and Census using dropdowns - // Select Site - const siteSelect = await page.locator('.site-select'); // Adjust the selector if needed - await siteSelect.click(); - await page.click('text="Testing"'); // Replace "Test Site" with the actual site name you want to select - - // Select Plot - const plotSelect = await page.locator('.plot-selection'); // Adjust the selector if needed - await plotSelect.click(); - await page.click('text="testing"'); // Replace "Test Plot" with the actual plot name you want to select - - // Select Census - const censusSelect = await page.locator('.census-select'); // Adjust the selector if needed - await censusSelect.click(); - await page.click('text="Census: 1"'); - - // Step 4: Navigate to /fixeddatainput/attributes - await page.goto('/fixeddatainput/attributes'); - }); - - test('should open and close the upload modal', async ({ page }) => { - // Navigate to the page where your component is rendered - await page.goto('/path-to-your-component'); // Adjust this URL as needed - - // Ensure the 'Upload' button is present - const uploadButton = await page.locator('button:has-text("Upload")'); - await expect(uploadButton).toBeVisible(); - - // Click the 'Upload' button - await uploadButton.click(); - - // Check if the modal is open by looking for the modal's container or some text inside - const uploadModal = await page.locator('text=Upload'); // Adjust selector to match your modal's content - await expect(uploadModal).toBeVisible(); - - // Close the modal (assuming there's a 'Close' button or a way to close the modal) - const closeModalButton = await page.locator('button:has-text("Close")'); // Adjust to the actual close button selector - await closeModalButton.click(); - - // Verify that the modal is no longer visible - await expect(uploadModal).toBeHidden(); - }); -});