diff --git a/backend/src/main/kotlin/org/pathoplexus/backend/controller/SubmissionController.kt b/backend/src/main/kotlin/org/pathoplexus/backend/controller/SubmissionController.kt index 02dbf41bc..8059184f9 100644 --- a/backend/src/main/kotlin/org/pathoplexus/backend/controller/SubmissionController.kt +++ b/backend/src/main/kotlin/org/pathoplexus/backend/controller/SubmissionController.kt @@ -214,7 +214,8 @@ class SubmissionController( @PathVariable sequenceId: Long, @PathVariable version: Long, @RequestParam username: String, - ): SequenceReview = databaseService.getReviewData(username, SequenceVersion(sequenceId, version)) + ): SequenceReview = + databaseService.getReviewData(username, SequenceVersion(sequenceId, version)) @Operation(description = SUBMIT_REVIEWED_SEQUENCE_DESCRIPTION) @ResponseStatus(HttpStatus.NO_CONTENT) @@ -279,9 +280,7 @@ class SubmissionController( ) fun deleteUserData( @RequestParam username: String, - ) { - databaseService.deleteUserSequences(username) - } + ) = databaseService.deleteUserSequences(username) @Operation(description = "Delete sequences") @DeleteMapping( @@ -289,9 +288,7 @@ class SubmissionController( ) fun deleteSequence( @RequestParam sequenceIds: List, - ) { - databaseService.deleteSequences(sequenceIds) - } + ) = databaseService.deleteSequences(sequenceIds) data class SequenceIdList( val sequenceIds: List, diff --git a/backend/src/main/kotlin/org/pathoplexus/backend/service/DatabaseService.kt b/backend/src/main/kotlin/org/pathoplexus/backend/service/DatabaseService.kt index 7ed99f23e..065354581 100644 --- a/backend/src/main/kotlin/org/pathoplexus/backend/service/DatabaseService.kt +++ b/backend/src/main/kotlin/org/pathoplexus/backend/service/DatabaseService.kt @@ -743,7 +743,7 @@ data class SequenceReview( val sequenceId: Long, val version: Long, val status: Status, - val data: ProcessedData, + val processedData: ProcessedData, val originalData: OriginalData, @Schema(description = "The preprocessing will be considered failed if this is not empty") val errors: List? = null, diff --git a/backend/src/test/kotlin/org/pathoplexus/backend/controller/GetDataToReviewEndpointTest.kt b/backend/src/test/kotlin/org/pathoplexus/backend/controller/GetDataToReviewEndpointTest.kt index ff80d7db6..8f087c8a2 100644 --- a/backend/src/test/kotlin/org/pathoplexus/backend/controller/GetDataToReviewEndpointTest.kt +++ b/backend/src/test/kotlin/org/pathoplexus/backend/controller/GetDataToReviewEndpointTest.kt @@ -39,7 +39,7 @@ class GetDataToReviewEndpointTest( assertThat(reviewData.sequenceId, `is`(firstSequence)) assertThat(reviewData.version, `is`(1)) - assertThat(reviewData.data, `is`(PreparedProcessedData.withErrors().data)) + assertThat(reviewData.processedData, `is`(PreparedProcessedData.withErrors().data)) } @Test diff --git a/website/README.md b/website/README.md index 4ee7eceb8..460e3ea81 100644 --- a/website/README.md +++ b/website/README.md @@ -87,3 +87,4 @@ docker pull ghcr.io/pathoplexus/website:latest ### General tips - Available scripts can be browsed in [`package.json`](./package.json) or by running `npm run` +- Tipps & Tricks for using icons from MUI https://mui.com/material-ui/guides/minimizing-bundle-size/ diff --git a/website/package-lock.json b/website/package-lock.json index e6dcefb57..1451f5c76 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.1", "dependencies": { "@astrojs/node": "^6.0.3", + "@emotion/react": "^11.11.1", + "@mui/icons-material": "^5.14.12", "@tanstack/react-query": "^4.36.1", "astro": "^3.3.0", "change-case": "^5.0.2", @@ -16,9 +18,11 @@ "express": "^4.18.2", "http-proxy-middleware": "^2.0.6", "luxon": "^3.4.0", + "neverthrow": "^6.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "winston": "^3.11.0" + "winston": "^3.11.0", + "zod": "^3.22.4" }, "devDependencies": { "@astrojs/check": "^0.2.1", @@ -745,7 +749,6 @@ "version": "7.23.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -845,7 +848,6 @@ "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", - "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", @@ -864,7 +866,6 @@ "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", - "dev": true, "dependencies": { "@emotion/memoize": "^0.8.1", "@emotion/sheet": "^1.2.2", @@ -876,14 +877,13 @@ "node_modules/@emotion/hash": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", - "dev": true + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", - "dev": true, + "devOptional": true, "dependencies": { "@emotion/memoize": "^0.8.1" } @@ -891,15 +891,12 @@ "node_modules/@emotion/memoize": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", - "dev": true + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { "version": "11.11.1", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", - "dev": true, - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", @@ -923,7 +920,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", - "dev": true, "dependencies": { "@emotion/hash": "^0.9.1", "@emotion/memoize": "^0.8.1", @@ -935,14 +931,13 @@ "node_modules/@emotion/sheet": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", - "dev": true + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" }, "node_modules/@emotion/styled": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", - "dev": true, + "devOptional": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", @@ -964,14 +959,12 @@ "node_modules/@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", - "dev": true + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", - "dev": true, "peerDependencies": { "react": ">=16.8.0" } @@ -979,14 +972,12 @@ "node_modules/@emotion/utils": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", - "dev": true + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "node_modules/@emotion/weak-memoize": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", - "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", - "dev": true + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, "node_modules/@esbuild/android-arm": { "version": "0.19.4", @@ -1405,7 +1396,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", - "dev": true, "dependencies": { "@floating-ui/utils": "^0.1.3" } @@ -1414,7 +1404,6 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", - "dev": true, "dependencies": { "@floating-ui/core": "^1.4.2", "@floating-ui/utils": "^0.1.3" @@ -1424,7 +1413,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz", "integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==", - "dev": true, "dependencies": { "@floating-ui/dom": "^1.5.1" }, @@ -1436,8 +1424,7 @@ "node_modules/@floating-ui/utils": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.4.tgz", - "integrity": "sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==", - "dev": true + "integrity": "sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==" }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", @@ -1572,7 +1559,6 @@ "version": "5.0.0-beta.19", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.19.tgz", "integrity": "sha512-maNBgAscddyPNzFZQUJDF/puxM27Li+NqSBsr/lAP8TLns2VvWS2SoL3OKFOIoRnAMKGY/Ic6Aot6gCYeQnssA==", - "dev": true, "dependencies": { "@babel/runtime": "^7.23.1", "@floating-ui/react-dom": "^2.0.2", @@ -1604,17 +1590,40 @@ "version": "5.14.13", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.13.tgz", "integrity": "sha512-3ZUbzcH4yloLKlV6Y+S0Edn2wef9t+EGHSfEkwVCn8E0ULdshifEFgfEroKRegQifDIwcKS/ofccxuZ8njTAYg==", - "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui" } }, + "node_modules/@mui/icons-material": { + "version": "5.14.12", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.14.12.tgz", + "integrity": "sha512-aFm6g/AIB3RQN9h/4MKoBoBybLZXeR3aDHWNx6KzemEpIlElUxv5uXRX5Qk1VC6v/YPkhbaPsiLLjsRSTiZF3w==", + "dependencies": { + "@babel/runtime": "^7.23.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.14.13", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.13.tgz", "integrity": "sha512-iPEFwhoVG789UVsXX4gqd1eJUlcLW1oceqwJYQN8Z4MpcAKfL9Lv3fda65AwG7pQ5lf+d7IbHzm4m48SWZxI2g==", - "dev": true, "dependencies": { "@babel/runtime": "^7.23.1", "@mui/base": "5.0.0-beta.19", @@ -1659,7 +1668,6 @@ "version": "5.14.13", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.13.tgz", "integrity": "sha512-5EFqk4tqiSwPguj4NW/6bUf4u1qoUWXy9lrKfNh9H6oAohM+Ijv/7qSxFjnxPGBctj469/Sc5aKAR35ILBKZLQ==", - "dev": true, "dependencies": { "@babel/runtime": "^7.23.1", "@mui/utils": "^5.14.13", @@ -1686,7 +1694,6 @@ "version": "5.14.13", "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.13.tgz", "integrity": "sha512-1ff/egFQl26hiwcUtCMKAkp4Sgqpm3qIewmXq+GN27fb44lDIACquehMFBuadOjceOFmbIXbayzbA46ZyqFYzA==", - "dev": true, "dependencies": { "@babel/runtime": "^7.23.1", "@emotion/cache": "^11.11.0", @@ -1718,7 +1725,6 @@ "version": "5.14.13", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.13.tgz", "integrity": "sha512-+5+Dx50lG4csbx2sGjrKLozXQJeCpJ4dIBZolyFLkZ+XphD1keQWouLUvJkPQ3MSglLLKuD37pp52YjMncZMEQ==", - "dev": true, "dependencies": { "@babel/runtime": "^7.23.1", "@mui/private-theming": "^5.14.13", @@ -1758,7 +1764,6 @@ "version": "7.2.6", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.6.tgz", "integrity": "sha512-7sjLQrUmBwufm/M7jw/quNiPK/oor2+pGUQP2CULRcFCArYTq78oJ3D5esTaL0UMkXKJvDqXn6Ike69yAOBQng==", - "dev": true, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0" }, @@ -1772,7 +1777,6 @@ "version": "5.14.13", "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.13.tgz", "integrity": "sha512-2AFpyXWw7uDCIqRu7eU2i/EplZtks5LAMzQvIhC79sPV9IhOZU2qwOWVnPtdctRXiQJOAaXulg+A37pfhEueQw==", - "dev": true, "dependencies": { "@babel/runtime": "^7.23.1", "@types/prop-types": "^15.7.7", @@ -1974,7 +1978,6 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -2306,8 +2309,7 @@ "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, "node_modules/@types/parse5": { "version": "6.0.3", @@ -2317,14 +2319,12 @@ "node_modules/@types/prop-types": { "version": "15.7.7", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.7.tgz", - "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==", - "dev": true + "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==" }, "node_modules/@types/react": { "version": "18.2.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.28.tgz", "integrity": "sha512-ad4aa/RaaJS3hyGz0BGegdnSRXQBkd1CCYDCdNjBPg90UUpLgo+WlJqb9fMYUxtehmzF3PJaTWqRZjko6BRzBg==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2344,7 +2344,6 @@ "version": "4.4.7", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.7.tgz", "integrity": "sha512-ICCyBl5mvyqYp8Qeq9B5G/fyBSRC0zx3XM3sCC6KkcMsNeAHqXBKkmat4GqdJET5jtYUpZXrxI5flve5qhi2Eg==", - "dev": true, "dependencies": { "@types/react": "*" } @@ -2352,8 +2351,7 @@ "node_modules/@types/scheduler": { "version": "0.16.4", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", - "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==", - "dev": true + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==" }, "node_modules/@types/semver": { "version": "7.5.3", @@ -3350,6 +3348,14 @@ "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.1.0.tgz", "integrity": "sha512-Mp+qrNhly+27bL/Zq8lGeUY+YrdoU0eDfIlAeGIPrzt0PnI/jGpvPUdCaugv4zbCrDkOUScFfcbeEiYumrdJnw==" }, + "node_modules/astro/node_modules/zod": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.1.tgz", + "integrity": "sha512-+dTu2m6gmCbO9Ahm4ZBDapx2O6ZY9QSPXst2WXjcznPMwf2YNpn3RevLx4KkZp1OPW/ouFcoBtBzFz/LeY69oA==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/astrojs-compiler-sync": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/astrojs-compiler-sync/-/astrojs-compiler-sync-0.3.3.tgz", @@ -3465,7 +3471,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -3756,7 +3761,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -4201,8 +4205,7 @@ "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/cookie": { "version": "0.5.0", @@ -4221,7 +4224,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -4307,8 +4309,7 @@ "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/daisyui": { "version": "3.9.2", @@ -4716,7 +4717,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dev": true, "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -4820,7 +4820,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -5018,7 +5017,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -5789,8 +5787,7 @@ "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "node_modules/find-up": { "version": "5.0.0", @@ -6406,8 +6403,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, - "peer": true, "dependencies": { "react-is": "^16.7.0" } @@ -6415,9 +6410,7 @@ "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "peer": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", @@ -6585,7 +6578,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -6867,8 +6859,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-async-function": { "version": "2.0.0", @@ -7442,8 +7433,7 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -7559,8 +7549,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/load-yaml-file": { "version": "0.2.0", @@ -8998,6 +8987,11 @@ "node": ">= 0.6" } }, + "node_modules/neverthrow": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-6.0.0.tgz", + "integrity": "sha512-kPZKRs4VkdloCGQXPoP84q4sT/1Z+lYM61AXyV8wWa2hnuo5KpPBF2S3crSFnMrOgUISmEBP8Vo/ngGZX60NhA==" + }, "node_modules/nlcst-to-string": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-3.1.1.tgz", @@ -9127,7 +9121,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9538,7 +9531,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -9550,7 +9542,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -9636,7 +9627,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -10170,7 +10160,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -10180,8 +10169,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/property-information": { "version": "6.3.0", @@ -10346,8 +10334,7 @@ "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-refresh": { "version": "0.14.0", @@ -10362,7 +10349,6 @@ "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "dev": true, "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -10443,8 +10429,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", @@ -10662,7 +10647,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -11749,7 +11733,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12065,8 +12048,7 @@ "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "dev": true + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "node_modules/sucrase": { "version": "3.34.0", @@ -14161,7 +14143,6 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, "engines": { "node": ">= 6" } @@ -14236,9 +14217,9 @@ } }, "node_modules/zod": { - "version": "3.21.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.1.tgz", - "integrity": "sha512-+dTu2m6gmCbO9Ahm4ZBDapx2O6ZY9QSPXst2WXjcznPMwf2YNpn3RevLx4KkZp1OPW/ouFcoBtBzFz/LeY69oA==", + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/website/package.json b/website/package.json index 740a9d521..054958d11 100644 --- a/website/package.json +++ b/website/package.json @@ -19,6 +19,8 @@ }, "dependencies": { "@astrojs/node": "^6.0.3", + "@emotion/react": "^11.11.1", + "@mui/icons-material": "^5.14.12", "@tanstack/react-query": "^4.36.1", "astro": "^3.3.0", "change-case": "^5.0.2", @@ -26,9 +28,11 @@ "express": "^4.18.2", "http-proxy-middleware": "^2.0.6", "luxon": "^3.4.0", + "neverthrow": "^6.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "winston": "^3.11.0" + "winston": "^3.11.0", + "zod": "^3.22.4" }, "devDependencies": { "@astrojs/check": "^0.2.1", @@ -39,7 +43,6 @@ "@mui/material": "^5.14.13", "@mui/x-date-pickers": "^6.16.2", "@playwright/test": "^1.39.0", - "uuid": "^9.0.1", "@tanstack/eslint-plugin-query": "^4.36.1", "@testing-library/dom": "^9.3.1", "@testing-library/jest-dom": "^6.1.3", @@ -69,6 +72,7 @@ "sass": "^1.69.3", "tailwindcss": "^3.3.3", "typescript": "^5.2.2", + "uuid": "^9.0.1", "vitest": "^0.34.6" } } diff --git a/website/src/api.ts b/website/src/api.ts index 50484fb15..06582b3a8 100644 --- a/website/src/api.ts +++ b/website/src/api.ts @@ -1,3 +1,6 @@ +import { err, ok, type Result } from 'neverthrow'; +import type z from 'zod'; + import type { BaseType, Config, InsertionCount, MutationProportionCount, SequenceType, ServiceUrls } from './types'; import { parseFasta } from './utils/parseFasta'; import { isAlignedSequence, isUnalignedSequence } from './utils/sequenceTypeHelpers'; @@ -36,18 +39,29 @@ export async function fetchInsertions( export type Log = { level: string; message: string; + instance?: string; +}; + +type ClientLogger = { + log: (log: Log) => Promise; + error: (message: string) => Promise; + info: (message: string) => Promise; }; -export const clientLogger = { - log: async ({ message, level }: Log): Promise => - fetch('/admin/logs.txt', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ level, message }), - }), - error: async (message: string): Promise => clientLogger.log({ level: 'error', message }), - info: async (message: string) => clientLogger.log({ level: 'info', message }), + +export const getClientLogger = (instance: string = 'client'): ClientLogger => { + const clientLogger = { + log: async ({ message, level, instance }: Log): Promise => + fetch('/admin/logs.txt', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ level, message, instance }), + }), + error: async (message: string): Promise => clientLogger.log({ level: 'error', instance, message }), + info: async (message: string) => clientLogger.log({ level: 'info', instance, message }), + }; + return clientLogger; }; export async function fetchSequence( @@ -74,3 +88,53 @@ export async function fetchSequence( } return fastaEntries[0].sequence; } + +type FetchParameter = { + endpoint: `/${string}`; + backendUrl: string; + zodSchema?: z.Schema; + options?: RequestInit; +}; + +export const clientFetch = async ({ + endpoint, + backendUrl, + zodSchema, + options, +}: FetchParameter): Promise> => { + const logger = getClientLogger('clientFetch ' + endpoint); + try { + const response = await fetch(`${backendUrl}${endpoint}`, options); + + if (!response.ok) { + await logger.error(`Failed to fetch user sequences with status ${response.status}`); + return err(`Failed to fetch user sequences ${JSON.stringify(await response.text())}`); + } + + try { + if (zodSchema === undefined) { + return ok(undefined as unknown as ResponseType); + } + + const parser = (candidate: unknown) => { + try { + return ok(zodSchema.parse(candidate)); + } catch (error) { + return err((error as Error).message); + } + }; + + const responseJson = await response.json(); + + return parser(responseJson); + } catch (error) { + await logger.error( + `Parsing the response review for sequence version failed with error '${JSON.stringify(error)}'`, + ); + return err(`Parsing the response review for sequence version failed with error '${JSON.stringify(error)}'`); + } + } catch (error) { + await logger.error(`Failed to fetch user sequences with error '${JSON.stringify(error)}'`); + return err(`Failed to fetch user sequences with error '${JSON.stringify(error)}'`); + } +}; diff --git a/website/src/components/DataUploadForm.tsx b/website/src/components/DataUploadForm.tsx index 7499447f1..c7429d497 100644 --- a/website/src/components/DataUploadForm.tsx +++ b/website/src/components/DataUploadForm.tsx @@ -1,7 +1,9 @@ import { CircularProgress, TextField } from '@mui/material'; import { type ChangeEvent, type FormEvent, useState } from 'react'; -import { clientLogger } from '../api.ts'; +import { getClientLogger } from '../api.ts'; + +const clientLogger = getClientLogger('DataUploadForm'); type DataUploadFormProps = { targetUrl: string; diff --git a/website/src/components/Review/DataRow.tsx b/website/src/components/Review/DataRow.tsx new file mode 100644 index 000000000..4e62aa893 --- /dev/null +++ b/website/src/components/Review/DataRow.tsx @@ -0,0 +1,76 @@ +import DangerousTwoToneIcon from '@mui/icons-material/DangerousTwoTone'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import { sentenceCase, snakeCase } from 'change-case'; +import { type FC } from 'react'; + +import { InputField, type KeyValuePair, type Row } from './InputField.tsx'; + +type NonEditableRowProps = { + row: Row; + editable: false; + customKey?: string; +}; + +type EditableRowProps = { + row: Row; + editable: (editedRow: Row) => void; + customKey?: string; +}; + +type RowProps = NonEditableRowProps | EditableRowProps; +export const DataRow: FC = ({ row, editable, customKey }) => { + const colorClassName = row.errors.length > 0 ? 'text-red-600' : row.warnings.length > 0 ? 'text-yellow-600' : ''; + + return ( + + {sentenceCase(row.key)}: + + + + + {editable === false ? ( +
{row.value}
+ ) : ( + + )} + + + ); +}; + +type ErrorAndWarningIconsProps = { + row: Row; +}; +const ErrorAndWarningIcons: FC = ({ row }) => { + return ( + <> + {row.warnings.length > 0 ? ( +
+ +
+ ) : null} + {row.errors.length > 0 ? ( +
+ +
+ ) : null} + + ); +}; + +type ProcessedDataRowProps = { + row: KeyValuePair; + customKey?: string; +}; + +export const ProcessedDataRow: FC = ({ row, customKey }) => { + return ( + + {sentenceCase(row.key)}: + + +
{row.value}
{' '} + + + ); +}; diff --git a/website/src/components/Review/InputField.tsx b/website/src/components/Review/InputField.tsx new file mode 100644 index 000000000..7b6bbf06b --- /dev/null +++ b/website/src/components/Review/InputField.tsx @@ -0,0 +1,64 @@ +import UndoTwoToneIcon from '@mui/icons-material/UndoTwoTone'; +import { type FC, useState } from 'react'; + +export type KeyValuePair = { + value: string; + key: string; +}; + +export type Row = { + warnings: string[]; + errors: string[]; + initialValue: string; +} & KeyValuePair; + +type InputFieldProps = { + row: Row; + onChange: (editedRow: Row) => void; + colorClassName: string; +}; + +export const InputField: FC = ({ row, onChange, colorClassName }) => { + const [isFocused, setIsFocused] = useState(false); + return ( + <> + onChange({ ...row, value: e.target.value })} + onFocus={() => setIsFocused(() => true)} + onBlur={() => setIsFocused(() => false)} + /> + + {isFocused && row.warnings.length + row.errors.length > 0 ? ( +
+ {row.errors.map((error) => ( +
+ {error} +
+ ))} + {row.warnings.map((warning) => ( +
+ {warning} +
+ ))} +
+ ) : null} + + ); +}; diff --git a/website/src/components/Review/ReviewPage.spec.tsx b/website/src/components/Review/ReviewPage.spec.tsx new file mode 100644 index 000000000..22789278c --- /dev/null +++ b/website/src/components/Review/ReviewPage.spec.tsx @@ -0,0 +1,138 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { sentenceCase } from 'change-case'; +import { beforeEach, describe, expect, test } from 'vitest'; + +import { ReviewPage } from './ReviewPage.tsx'; +import { testuser } from '../../../tests/e2e.fixture.ts'; +import type { ClientConfig, MetadataField, SequenceReview } from '../../types'; + +const queryClient = new QueryClient(); +const metadataKey = 'originalMetaDataField'; +const editableEntry = 'originalMetaDataValue'; +const defaultReviewData: SequenceReview = { + sequenceId: 1, + version: 1, + status: 'NEEDS_REVIEW', + errors: [ + { + source: [ + { + name: metadataKey, + type: 'Metadata', + }, + ], + message: 'errorMessage', + }, + ], + warnings: [ + { + source: [ + { + name: metadataKey, + type: 'Metadata', + }, + ], + message: 'warningMessage', + }, + ], + originalData: { + metadata: { + [metadataKey]: editableEntry, + }, + unalignedNucleotideSequences: { + originalUnalignedNucleotideSequencesField: 'originalUnalignedNucleotideSequencesValue', + }, + }, + processedData: { + metadata: { + processedMetaDataField: 'processedMetaDataValue', + }, + unalignedNucleotideSequences: { + processedUnalignedNucleotideSequencesField: 'processedUnalignedNucleotideSequencesValue', + }, + }, +}; + +const dummyConfig = {} as ClientConfig; +function renderReviewPage(reviewData: SequenceReview = defaultReviewData, clientConfig: ClientConfig = dummyConfig) { + render( + + + , + ); +} + +describe('ReviewPage', () => { + beforeEach(() => { + Object.defineProperty(window, 'location', { + value: { + href: '', + }, + }); + }); + + test('should render the form with submit button', async () => { + renderReviewPage(); + + const submitButton = screen.getByRole('button', { name: /Submit Review/i }); + expect(submitButton).toBeInTheDocument(); + + // jsdom cannot do HTMLDialogElements: https://github.com/testing-library/react-testing-library/issues/1106 + // await userEvent.click(submitButton); + }); + + test('should show original data and processed data', async () => { + renderReviewPage(); + + expect(screen.getByText(/Original Data/i)).toBeInTheDocument(); + + expectTextInMetadata.original(defaultReviewData.originalData.metadata); + + expect(screen.getByText(/Processed Data/i)).toBeInTheDocument(); + + expectTextInMetadata.processed(defaultReviewData.processedData.metadata); + }); + + test('should show error and warning tooltips', async () => { + renderReviewPage(); + + expect(document.querySelector('.tooltip[data-tip="errorMessage"]')).toBeTruthy(); + expect(document.querySelector('.tooltip[data-tip="warningMessage"]')).toBeTruthy(); + }); + + test('should edit, show errors and undo input', async () => { + renderReviewPage(); + + await userEvent.click(screen.getByDisplayValue(editableEntry)); + + expect(screen.getByText(/errorMessage/i)).toBeInTheDocument(); + expect(screen.getByText(/warningMessage/i)).toBeInTheDocument(); + + const someTextToAdd = '_addedText'; + await userEvent.type(screen.getByDisplayValue(editableEntry), someTextToAdd); + + expectTextInMetadata.original({ + [metadataKey]: editableEntry + someTextToAdd, + }); + const undoButton = document.querySelector(`.tooltip[data-tip="Revert to: ${editableEntry}"]`); + expect(undoButton).not.toBeNull(); + + await userEvent.click(undoButton!); + expectTextInMetadata.original(defaultReviewData.originalData.metadata); + }); +}); + +const expectTextInMetadata = { + original: (metadata: Record): void => + Object.entries(metadata).forEach(([key, value]) => { + expect(screen.getByText(sentenceCase(key) + ':')).toBeInTheDocument(); + expect(screen.getByDisplayValue(value.toString())).toBeInTheDocument(); + }), + processed: (metadata: Record): void => + Object.entries(metadata).forEach(([key, value]) => { + expect(screen.getByText(sentenceCase(key) + ':')).toBeInTheDocument(); + expect(screen.getByText(value.toString())).toBeInTheDocument(); + }), +}; diff --git a/website/src/components/Review/ReviewPage.tsx b/website/src/components/Review/ReviewPage.tsx new file mode 100644 index 000000000..9723aa51a --- /dev/null +++ b/website/src/components/Review/ReviewPage.tsx @@ -0,0 +1,252 @@ +import { sentenceCase, snakeCase } from 'change-case'; +import { type Result } from 'neverthrow'; +import { type FC, Fragment, useMemo, useRef, useState } from 'react'; + +import { DataRow, ProcessedDataRow } from './DataRow.tsx'; +import type { Row, KeyValuePair } from './InputField.tsx'; +import { clientFetch, getClientLogger } from '../../api.ts'; +import type { ClientConfig, ProcessingAnnotationSourceType, SequenceReview, UnprocessedData } from '../../types.ts'; +import { ManagedErrorFeedback } from '../Submission/ManagedErrorFeedback.tsx'; + +type ReviewPageProps = { + clientConfig: ClientConfig; + reviewData: SequenceReview; + username: string; +}; + +const logger = getClientLogger('ReviewPage'); + +export const ReviewPage: FC = ({ reviewData, clientConfig, username }) => { + const [editedMetadata, setEditedMetadata] = useState(mapMetadataToRow(reviewData)); + const [editedSequences, setEditedSequences] = useState(mapSequencesToRow(reviewData)); + + const [isErrorOpen, setIsErrorOpen] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const dialogRef = useRef(null); + + const handleOpenConfirmationDialog = () => { + if (dialogRef.current) { + dialogRef.current.showModal(); + } + }; + + const submitReviewForSequenceVersion = async () => { + const result = await submitReview(reviewData, editedMetadata, editedSequences, username, clientConfig); + await result.match( + async () => { + window.history.back(); + await logger.info('Successfully submitted review ' + reviewData.sequenceId + '.' + reviewData.version); + }, + async (error) => { + handleOpenError(`Failed to submit review with error '${JSON.stringify(error)})}'`); + }, + ); + }; + const handleOpenError = (message: string) => { + setErrorMessage(message); + setIsErrorOpen(true); + }; + + const handleCloseError = () => { + setErrorMessage(''); + setIsErrorOpen(false); + }; + + const processedSequenceRows = useMemo(() => mapProcessedSequencesToRow(reviewData), [reviewData]); + const processedMetadataRows = useMemo(() => mapProcessedMetadataToRow(reviewData), [reviewData]); + + return ( + <> + + + + + +
+
+ +
+ +

Do you really want to submit?

+ +
+ +
+ +
+
+
+
+ + + + + + {editedMetadata.map((field) => ( + + setEditedMetadata((prevRows: Row[]) => + prevRows.map((prevRow) => + prevRow.key === editedRow.key + ? { ...prevRow, value: editedRow.value } + : prevRow, + ), + ) + } + /> + ))} + + + {editedSequences.map((field) => ( + + setEditedSequences((prevRows: Row[]) => + prevRows.map((prevRow) => + prevRow.key === editedRow.key + ? { ...prevRow, value: editedRow.value } + : prevRow, + ), + ) + } + /> + ))} + + + + {processedMetadataRows.map((field) => ( + + ))} + {processedSequenceRows.map((sequenceRow) => ( + + ))} + {processedSequenceRows.map((sequenceRow) => + sequenceRow.data.map((field) => ( + + )), + )} + +
+ + ); +}; + +type SubtitleProps = { + title: string; + bold?: boolean; + customKey?: string; +}; +const Subtitle: FC = ({ title, bold, customKey }) => ( + + + + + {title} + + + +); + +const mapMetadataToRow = (reviewData: SequenceReview): Row[] => + Object.entries(reviewData.originalData.metadata).map(([key, value]) => ({ + key, + initialValue: value.toString(), + value: value.toString(), + ...mapErrorsAndWarnings(reviewData, key, 'Metadata'), + })); + +const mapSequencesToRow = (reviewData: SequenceReview): Row[] => + Object.entries(reviewData.originalData.unalignedNucleotideSequences).map(([key, value]) => ({ + key, + initialValue: value.toString(), + value: value.toString(), + ...mapErrorsAndWarnings(reviewData, key, 'NucleotideSequence'), + })); + +const mapProcessedMetadataToRow = (reviewData: SequenceReview): KeyValuePair[] => + Object.entries(reviewData.processedData.metadata).map(([key, value]) => ({ + key, + value: value.toString(), + })); + +type SequenceRow = { type: string; data: KeyValuePair[] }; + +const mapProcessedSequencesToRow = (reviewData: SequenceReview): SequenceRow[] => + Object.entries(reviewData.processedData) + .filter(([sequenceType]) => sequenceType !== 'metadata') + .map(([sequenceType, sequenceData]) => ({ + type: sequenceType, + data: Object.entries(sequenceData).map( + ([key, value]): KeyValuePair => ({ + key, + value: value.toString(), + }), + ), + })); + +const mapErrorsAndWarnings = ( + reviewData: SequenceReview, + key: string, + type: ProcessingAnnotationSourceType, +): { errors: string[]; warnings: string[] } => ({ + errors: (reviewData.errors ?? []) + .filter((error) => error.source.find((source) => source.name === key && source.type === type) !== undefined) + .map((error) => error.message), + warnings: (reviewData.warnings ?? []) + .filter((warning) => warning.source.find((source) => source.name === key && source.type === type) !== undefined) + .map((warning) => warning.message), +}); + +const submitReview = async ( + reviewData: SequenceReview, + editedMetadata: Row[], + editedSequences: Row[], + username: string, + clientConfig: ClientConfig, +): Promise> => { + const body: UnprocessedData = { + sequenceId: reviewData.sequenceId, + version: reviewData.version, + data: { + metadata: editedMetadata.reduce((prev, row) => ({ ...prev, [row.key]: row.value }), {}), + unalignedNucleotideSequences: editedSequences.reduce( + (prev, row) => ({ ...prev, [row.key]: row.value }), + {}, + ), + }, + }; + + return clientFetch({ + endpoint: `/submit-reviewed-sequence?username=${username}`, + backendUrl: clientConfig.backendUrl, + zodSchema: undefined, + options: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }, + }); +}; diff --git a/website/src/components/SearchPage/SearchForm.tsx b/website/src/components/SearchPage/SearchForm.tsx index e3ecf9eda..9464ca6c9 100644 --- a/website/src/components/SearchPage/SearchForm.tsx +++ b/website/src/components/SearchPage/SearchForm.tsx @@ -9,7 +9,7 @@ import { AutoCompleteField } from './fields/AutoCompleteField'; import { DateField } from './fields/DateField'; import { NormalTextField } from './fields/NormalTextField'; import { PangoLineageField } from './fields/PangoLineageField'; -import { clientLogger } from '../../api'; +import { getClientLogger } from '../../api'; import { useOffCanvas } from '../../hooks/useOffCanvas'; import type { ClientConfig, Filter } from '../../types'; import { OffCanvasOverlay } from '../OffCanvasOverlay'; @@ -22,6 +22,8 @@ interface SearchFormProps { clientConfig: ClientConfig; } +const clientLogger = getClientLogger('SearchForm'); + export const SearchForm: FC = ({ metadataSettings, clientConfig }) => { const [fieldValues, setFieldValues] = useState<(Filter & { label: string })[]>( metadataSettings.map((metadata) => ({ diff --git a/website/src/components/Submission/ManagedErrorFeedback.tsx b/website/src/components/Submission/ManagedErrorFeedback.tsx index 109aecadb..f60296291 100644 --- a/website/src/components/Submission/ManagedErrorFeedback.tsx +++ b/website/src/components/Submission/ManagedErrorFeedback.tsx @@ -16,6 +16,7 @@ export const ManagedErrorFeedback: FC = ({ message, open, on ); return ( ({ - clientLogger: { + getClientLogger: () => ({ error: vi.fn(), log: vi.fn(), info: vi.fn(), - }, + }), })); function renderSubmissionForm() { diff --git a/website/src/components/UserSequenceList/SequencesWithReview.tsx b/website/src/components/UserSequenceList/SequencesWithReview.tsx index 56126cc5e..f4c506172 100644 --- a/website/src/components/UserSequenceList/SequencesWithReview.tsx +++ b/website/src/components/UserSequenceList/SequencesWithReview.tsx @@ -27,7 +27,7 @@ export const SequencesWithReview: FC = ({ sequences, us {sequence.status} Click to view diff --git a/website/src/config.ts b/website/src/config.ts index 2c973e4ce..05040fef5 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -1,18 +1,17 @@ import fs from 'fs'; import path from 'path'; -import { clientLogger } from './api'; +import { getClientLogger } from './api'; import type { ClientConfig, Config, ReferenceGenomes, RuntimeConfig, ServerConfig, ServiceUrls } from './types'; import netlifyConfig from '../netlifyConfig/config.json'; import netlifyRuntimeConfig from '../netlifyConfig/runtime_config.json'; -const configDir = import.meta.env.CONFIG_DIR; - let _config: Config | null = null; let _runtimeConfig: RuntimeConfig | null = null; let _referenceGenomes: ReferenceGenomes | null = null; function getConfigDir(): string { + const configDir = import.meta.env.CONFIG_DIR; if (typeof configDir !== 'string' || configDir === '') { throw new Error(`CONFIG_DIR environment variable was not set during build time, is '${configDir}'`); } @@ -100,7 +99,7 @@ export async function fetchAutoCompletion( const response = await fetch(`${runtimeConfig.lapisUrl}/aggregated?fields=${field}&${filterParams}`); if (!response.ok) { - await clientLogger.error( + await getClientLogger('fetchAutoComplete').error( `Failed to fetch auto-completion data for field ${field} with status ${response.status}`, ); return []; diff --git a/website/src/logger.ts b/website/src/logger.ts index 47107abb7..9ad7e1555 100644 --- a/website/src/logger.ts +++ b/website/src/logger.ts @@ -26,3 +26,9 @@ const getLogger = (): Logger => { }; export const logger = getLogger(); +export const getInstanceLogger = (instance: string) => { + return { + info: (message: string) => logger.info(message, { instance }), + error: (message: string) => logger.error(message, { instance }), + }; +}; diff --git a/website/src/pages/admin/logs.txt.ts b/website/src/pages/admin/logs.txt.ts index 5397ead8d..78f7d2dc1 100644 --- a/website/src/pages/admin/logs.txt.ts +++ b/website/src/pages/admin/logs.txt.ts @@ -4,7 +4,7 @@ import { logger } from '../../logger'; export const POST: APIRoute = async ({ request }) => { const logToAppend = await request.json(); - logger.log(logToAppend.level, logToAppend.message); + logger.log({ level: logToAppend.level, message: logToAppend.message, instance: logToAppend.instance }); return new Response( JSON.stringify({ body: 'ok', diff --git a/website/src/pages/search/search.ts b/website/src/pages/search/search.ts index b796aa6ce..8c95588ef 100644 --- a/website/src/pages/search/search.ts +++ b/website/src/pages/search/search.ts @@ -1,8 +1,10 @@ import type { TableSequenceData } from '../../components/SearchPage/Table'; import { getConfig, getRuntimeConfig } from '../../config'; -import { logger } from '../../logger'; +import { getInstanceLogger } from '../../logger'; import type { Filter } from '../../types'; +const logger = getInstanceLogger('search.ts'); + export enum SearchStatus { OK, ERROR, diff --git a/website/src/pages/user/[username]/review/[sequenceId]/[version].astro b/website/src/pages/user/[username]/review/[sequenceId]/[version].astro new file mode 100644 index 000000000..22bbca1e4 --- /dev/null +++ b/website/src/pages/user/[username]/review/[sequenceId]/[version].astro @@ -0,0 +1,42 @@ +--- +import { getReviewForSequenceVersion } from './getReviewData'; +import { BackButton } from '../../../../../components/Navigation/BackButton'; +import { ReviewPage } from '../../../../../components/Review/ReviewPage'; +import { getRuntimeConfig } from '../../../../../config'; +import BaseLayout from '../../../../../layouts/BaseLayout.astro'; + +const version = Astro.params.version!; +const sequenceId = Astro.params.sequenceId!; + +const username = Astro.params.username!; + +const clientConfig = getRuntimeConfig().forClient; + +const reviewData = await getReviewForSequenceVersion(username, sequenceId, version); +--- + + +
+ +

+ Review for Id: {sequenceId}.{version} +

+
+ { + reviewData.match( + (reviewData) => ( + + ), + (error) => ( + <> +
+

+ Error while fetching review data for sequence version: {sequenceId}.{version} +

+
+
{error}
+ + ), + ) + } +
diff --git a/website/src/pages/user/[username]/review/[sequenceId]/getReviewData.ts b/website/src/pages/user/[username]/review/[sequenceId]/getReviewData.ts new file mode 100644 index 000000000..1d98d8f8c --- /dev/null +++ b/website/src/pages/user/[username]/review/[sequenceId]/getReviewData.ts @@ -0,0 +1,55 @@ +import { err, ok, type Result } from 'neverthrow'; +import type z from 'zod'; + +import { getRuntimeConfig } from '../../../../../config.ts'; +import { getInstanceLogger } from '../../../../../logger.ts'; +import { sequenceReview, type SequenceReview } from '../../../../../types.ts'; + +const logger = getInstanceLogger('getReviewData'); + +export const backendFetch = async ( + endpoint: `/${string}`, + zodSchema: z.Schema, + options?: RequestInit, +): Promise> => { + try { + const response = await fetch(`${getRuntimeConfig().forServer.backendUrl}${endpoint}`, options); + + if (!response.ok) { + logger.error(`Failed to fetch with status ${response.status}`); + return err(`Failed to fetch. Reason: ${JSON.stringify((await response.json()).detail)}`); + } + + try { + const parser = (candidate: unknown): Result, string> => { + try { + return ok(zodSchema.parse(candidate)); + } catch (error) { + return err((error as Error).message); + } + }; + + const responseJson = await response.json(); + return parser(responseJson); + } catch (error) { + logger.error(`Parsing the response failed with error '${JSON.stringify(error)}'`); + return err(`Parsing the response failed with error '${JSON.stringify(error)}'`); + } + } catch (error) { + logger.error(`Failed to fetch with error '${JSON.stringify(error)}'`); + return err(`Failed to fetch with error '${JSON.stringify(error)}'`); + } +}; + +export const getReviewForSequenceVersion = async ( + userName: string, + sequenceId: number | string, + version: number | string, +): Promise> => { + return backendFetch(`/get-data-to-review/${sequenceId}/${version}?username=${userName}`, sequenceReview, { + method: 'GET', + headers: { + accept: 'application/json', + }, + }); +}; diff --git a/website/src/pages/user/[username]/sequences/[id].astro b/website/src/pages/user/[username]/sequences/[id].astro deleted file mode 100644 index 63a5fb75e..000000000 --- a/website/src/pages/user/[username]/sequences/[id].astro +++ /dev/null @@ -1,80 +0,0 @@ ---- -import { getReviewData } from './getReviewData'; -import { BackButton } from '../../../../components/Navigation/BackButton'; -import BaseLayout from '../../../../layouts/BaseLayout.astro'; - -// eslint-disable-next-line -const id = Astro.params.id!; -// eslint-disable-next-line -const username = Astro.params.username!; - -const reviewDataOfUser = await getReviewData(username); -const reviewData = reviewDataOfUser.find((review) => review.sequenceId.toString() === id); ---- - - - { - reviewData === undefined ? ( -
- -

No review found with ID: {id}

-
- ) : ( - <> -
- -

Review for ID: {id}

-
-
- - - - - - {Object.entries(reviewData.data.metadata).map(([key, value]) => ( - - - - - ))} - - - - - - - - - - - - - {Object.values(reviewData.errors).map((error) => ( - - - - - ))} - - - - - - - {Object.values(reviewData.warnings).map((warning) => ( - - - - - ))} - -
Metadata -
{key}:{JSON.stringify(value, null, '\t')}
Unaligned Nucleotide Sequences -
main:{reviewData.data.unalignedNucleotideSequences.main}
Errors -
{error.source.fieldName}:{error.message}
Warnings -
{warning.source.fieldName}:{warning.message}
-
- - ) - } -
diff --git a/website/src/pages/user/[username]/sequences/getReviewData.ts b/website/src/pages/user/[username]/sequences/getReviewData.ts deleted file mode 100644 index cabac0299..000000000 --- a/website/src/pages/user/[username]/sequences/getReviewData.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { getRuntimeConfig } from '../../../../config'; -import { logger } from '../../../../logger'; - -type PangoLineage = string; - -export type SequenceReview = { - sequenceId: number; - errors: ProcessingAnnotation[]; - warnings: ProcessingAnnotation[]; - data: { - metadata: { - date: string; - host: string; - region: string; - country: string; - division: string; - pangoLineage: PangoLineage; - }; - unalignedNucleotideSequences: { - main: string; - }; - }; -}; - -export type ProcessingAnnotation = { - source: { - fieldName: string; - type: string; - }; - message: string; -}; - -export const getReviewData = async (name: string): Promise => { - try { - const config = getRuntimeConfig().forServer; - const mySequencesQuery = `${config.backendUrl}/get-data-to-review?submitter=${name}&numberOfSequences=1000`; - - const mySequencesResponse = await fetch(mySequencesQuery, { - method: 'GET', - headers: { - accept: 'application/x-ndjson', - }, - }); - - if (!mySequencesResponse.ok) { - logger.error(`Failed to fetch user sequences with status ${mySequencesResponse.status}`); - return []; - } - - const sequenceReviews: SequenceReview[] = []; - - const ndjsonText = await mySequencesResponse.text(); - const ndjsonLines = ndjsonText.split('\n'); - - ndjsonLines.forEach((line: string) => { - if (line.trim() === '') { - return; - } - - try { - const sequenceReview = JSON.parse(line) as SequenceReview; - sequenceReviews.push(sequenceReview); - } catch (error) { - logger.error(`Failed to parse JSON line: ${error}`); - } - }); - - return sequenceReviews; - } catch (error) { - logger.error(`Failed to fetch user sequences with error '${(error as Error).message}'`); - return []; - } -}; diff --git a/website/src/pages/user/[username]/user.ts b/website/src/pages/user/[username]/user.ts index 2be1654c4..abaf07549 100644 --- a/website/src/pages/user/[username]/user.ts +++ b/website/src/pages/user/[username]/user.ts @@ -1,18 +1,12 @@ import { getRuntimeConfig } from '../../../config'; -import { logger } from '../../../logger'; +import { getInstanceLogger } from '../../../logger'; +import type { SequenceStatusNames } from '../../../types.ts'; +const logger = getInstanceLogger('user.ts'); export enum ResponseStatus { OK = 'OK', ERROR = 'ERROR', } -export type SequenceStatusNames = - | 'RECEIVED' - | 'PROCESSING' - | 'NEEDS_REVIEW' - | 'REVIEWED' - | 'PROCESSED' - | 'SILO_READY' - | 'REVOKED_STAGING'; export type SequenceStatus = { status: SequenceStatusNames; diff --git a/website/src/styles/base.scss b/website/src/styles/base.scss index 3f9b1acb9..805979772 100644 --- a/website/src/styles/base.scss +++ b/website/src/styles/base.scss @@ -20,6 +20,7 @@ a { text-decoration: red; font-size: 24px; font-weight: 600; + word-break: break-all; } .subtitle{ @@ -31,3 +32,14 @@ a { .offCanvasTransform { @apply transform transition-transform duration-300 ease-in-out z-40; } + +.customTable { + display: table; + word-break: break-all; + border: none; +} +table, tr, td { + border: none; + padding: 5px 0; +} + diff --git a/website/src/types.ts b/website/src/types.ts index c129ce39f..503c0c34c 100644 --- a/website/src/types.ts +++ b/website/src/types.ts @@ -1,3 +1,5 @@ +import z from 'zod'; + export type BaseType = 'nucleotide' | 'aminoAcid'; export type SequenceType = @@ -64,6 +66,70 @@ export type HeaderId = { customId: string; }; +export type PangoLineage = string; + +export type UnprocessedData = { + sequenceId: number; + version: number; + data: { + metadata: { [key in string]: string | number | PangoLineage | Date }; + unalignedNucleotideSequences: { [key in string]: string }; + }; +}; + +export type Sequence = { + sequenceId: number; + version: number; + data: any; + errors?: any[]; + warnings?: any[]; +}; + +const sequenceStatusNames = z.union([ + z.literal('RECEIVED'), + z.literal('PROCESSING'), + z.literal('NEEDS_REVIEW'), + z.literal('REVIEWED'), + z.literal('PROCESSED'), + z.literal('SILO_READY'), + z.literal('REVOKED_STAGING'), +]); +export type SequenceStatusNames = z.infer; +const statusThatAllowsReview = z.union([z.literal('NEEDS_REVIEW'), z.literal('PROCESSED')]); + +const processingAnnotationSourceType = z.union([z.literal('Metadata'), z.literal('NucleotideSequence')]); +export type ProcessingAnnotationSourceType = z.infer; +const processingAnnotation = z.object({ + source: z.array( + z.object({ + name: z.string(), + type: processingAnnotationSourceType, + }), + ), + message: z.string(), +}); + +export type ProcessingAnnotation = z.infer; + +export const metadataField = z.union([z.string(), z.number(), z.date(), z.string()]); +export type MetadataField = z.infer; +export const sequenceReview = z.object({ + sequenceId: z.number(), + version: z.number(), + status: statusThatAllowsReview, + errors: z.array(processingAnnotation).nullable(), + warnings: z.array(processingAnnotation).nullable(), + originalData: z.object({ + metadata: z.record(metadataField), + unalignedNucleotideSequences: z.record(z.string()), + }), + processedData: z.object({ + metadata: z.record(metadataField), + unalignedNucleotideSequences: z.record(z.string()), + }), +}); +export type SequenceReview = z.infer; + export interface SequenceVersion { sequenceId: number; version: number; diff --git a/website/tailwind.config.cjs b/website/tailwind.config.cjs index 9f6cde4c5..45c71796b 100644 --- a/website/tailwind.config.cjs +++ b/website/tailwind.config.cjs @@ -21,7 +21,7 @@ module.exports = { "accent": "#1dcdbc", "neutral": "#ff0000", "base-100": "#ffffff", - "info": "#3abff8", + "info": "#9ab9bd", "success": "#36d399", "warning": "#fbbd23", "error": "#f87272", diff --git a/website/tests/pages/submit/submit.page.ts b/website/tests/pages/submit/submit.page.ts index 758d5d5ff..0b13af939 100644 --- a/website/tests/pages/submit/submit.page.ts +++ b/website/tests/pages/submit/submit.page.ts @@ -3,8 +3,9 @@ import { readFileSync } from 'fs'; import type { Locator, Page } from '@playwright/test'; import { approveProcessedData } from '../../../src/components/UserSequenceList/approveProcessedData.ts'; +import type { Sequence } from '../../../src/types.ts'; import { baseUrl, expect, metadataTestFile, sequencesTestFile, testuser } from '../../e2e.fixture'; -import { fakeProcessingPipeline, queryUnprocessedData, type Sequence } from '../../util/preprocessingPipeline.ts'; +import { fakeProcessingPipeline, queryUnprocessedData } from '../../util/preprocessingPipeline.ts'; export class SubmitPage { public readonly userField: Locator; diff --git a/website/tests/util/preprocessingPipeline.ts b/website/tests/util/preprocessingPipeline.ts index 23ab35e27..da657abb6 100644 --- a/website/tests/util/preprocessingPipeline.ts +++ b/website/tests/util/preprocessingPipeline.ts @@ -1,3 +1,4 @@ +import type { UnprocessedData } from '../../src/types.ts'; import { backendUrl } from '../e2e.fixture.ts'; export const fakeProcessingPipeline = async ({ @@ -69,20 +70,3 @@ export async function queryUnprocessedData(countOfSequences: number) { .filter((line) => line.length > 0) .map((line): UnprocessedData => JSON.parse(line)); } - -export type UnprocessedData = { - sequenceId: number; - version: number; - data: { - metadata: Record; - unalignedNucleotideSequences: Record; - }; -}; - -export type Sequence = { - sequenceId: number; - version: number; - data: any; - errors?: any[]; - warnings?: any[]; -};