From 17c4fcafa1774aeaa792ee1c8d8c62a274ed29c6 Mon Sep 17 00:00:00 2001 From: Chaoran Chen Date: Sun, 26 Jan 2025 17:24:34 +0100 Subject: [PATCH] feat(website): support HTML custom display of table entries --- .../loculus/templates/_common-metadata.tpl | 3 + kubernetes/loculus/values.yaml | 3 + website/package-lock.json | 114 ++++++++++++++++++ website/package.json | 2 + .../DataTableEntryValue.tsx | 17 +++ website/src/types/config.ts | 1 + 6 files changed, 140 insertions(+) diff --git a/kubernetes/loculus/templates/_common-metadata.tpl b/kubernetes/loculus/templates/_common-metadata.tpl index efe49d4de6..44ae6d38f0 100644 --- a/kubernetes/loculus/templates/_common-metadata.tpl +++ b/kubernetes/loculus/templates/_common-metadata.tpl @@ -244,6 +244,9 @@ organisms: {{- if .customDisplay.displayGroup }} displayGroup: {{ quote .customDisplay.displayGroup }} {{- end }} + {{- if .customDisplay.html }} + html: {{ .customDisplay.html }} + {{- end }} {{- end }} {{- end }} diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index b300b8207f..4b14bfd406 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -168,6 +168,9 @@ defaultOrganismConfig: &defaultOrganismConfig guidance: Geo-coordinate longitude in decimal degree (WGS84) format, i.e. values in range -180 to 180, where positive values are east of the Prime Meridian. - name: geoLocCountry displayName: Collection country + customDisplay: + type: htmlTemplate + html: "__value__" generateIndex: true autocomplete: true initiallyVisible: true diff --git a/website/package-lock.json b/website/package-lock.json index a1aeadcb7d..96b52a332a 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -16,6 +16,7 @@ "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@tanstack/react-query": "^4.36.1", + "@types/sanitize-html": "^2.13.0", "@zodios/core": "^10.9.6", "@zodios/react": "^10.4.5", "astro": "^5.1.8", @@ -40,6 +41,7 @@ "react-toastify": "^11.0.3", "react-tooltip": "^5.28.0", "rsuite": "^5.77.0", + "sanitize-html": "^2.14.0", "unplugin-icons": "^22.0.0", "winston": "^3.17.0", "xlsx": "^0.18.5", @@ -3843,6 +3845,15 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "license": "MIT" }, + "node_modules/@types/sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==", + "license": "MIT", + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", @@ -6628,6 +6639,61 @@ "@babel/runtime": "^7.20.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -8709,6 +8775,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -11893,6 +11978,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, "node_modules/parse5": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", @@ -13371,6 +13462,29 @@ "node": ">=10" } }, + "node_modules/sanitize-html": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.14.0.tgz", + "integrity": "sha512-CafX+IUPxZshXqqRaG9ZClSlfPVjSxI0td7n07hk8QO2oO+9JDnlcL8iM8TWeOXOIBFgIOx6zioTzM53AOMn3g==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sass": { "version": "1.83.4", "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", diff --git a/website/package.json b/website/package.json index 7749098b53..2d790c9417 100644 --- a/website/package.json +++ b/website/package.json @@ -29,6 +29,7 @@ "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@tanstack/react-query": "^4.36.1", + "@types/sanitize-html": "^2.13.0", "@zodios/core": "^10.9.6", "@zodios/react": "^10.4.5", "astro": "^5.1.8", @@ -53,6 +54,7 @@ "react-toastify": "^11.0.3", "react-tooltip": "^5.28.0", "rsuite": "^5.77.0", + "sanitize-html": "^2.14.0", "unplugin-icons": "^22.0.0", "winston": "^3.17.0", "xlsx": "^0.18.5", diff --git a/website/src/components/SequenceDetailsPage/DataTableEntryValue.tsx b/website/src/components/SequenceDetailsPage/DataTableEntryValue.tsx index f3ec0b82ba..28f46cfb07 100644 --- a/website/src/components/SequenceDetailsPage/DataTableEntryValue.tsx +++ b/website/src/components/SequenceDetailsPage/DataTableEntryValue.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import sanitizeHtml from 'sanitize-html'; import { DataUseTermsHistoryModal } from './DataUseTermsHistoryModal'; import { SubstitutionsContainers } from './MutationBadge'; @@ -45,6 +46,13 @@ const CustomDisplayComponent: React.FC = ({ data, dataUseTermsHistory }) {value} )} + {customDisplay?.type === 'htmlTemplate' && customDisplay.html !== undefined && ( + /* eslint-disable @typescript-eslint/naming-convention */ +
+ /* eslint-enable @typescript-eslint/naming-convention */ + )} {customDisplay?.type === 'dataUseTerms' && ( <> {value} @@ -70,4 +78,13 @@ const PlainValueDisplay: React.FC<{ value: TableDataEntry['value'] }> = ({ value return None; }; +const generateCleanHtml = (trustedHtml: string, userValue: string): string => { + const cleanedValue = sanitizeHtml(userValue, { + allowedTags: [], + allowedAttributes: {}, + disallowedTagsMode: 'escape', + }); + return trustedHtml.replace('__value__', cleanedValue); +}; + export default CustomDisplayComponent; diff --git a/website/src/types/config.ts b/website/src/types/config.ts index a96d04a790..dda387617a 100644 --- a/website/src/types/config.ts +++ b/website/src/types/config.ts @@ -22,6 +22,7 @@ export const segmentedMutations = z.object({ export const customDisplay = z.object({ type: z.string(), url: z.string().optional(), + html: z.string().optional(), value: z.array(segmentedMutations).optional(), displayGroup: z.string().optional(), });