diff --git a/Dockerfile b/Dockerfile index 6e2c7c75..f624cca6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ COPY package.json package-lock.json ./ RUN npm install COPY ./vite.config.ts ./vite.demo.config.ts ./extensions.json ./tsconfig.json ./tsconfig.node.json ./ COPY ./packages ./packages +COPY ./options_tester.cjs ./options_tester.cjs + RUN VITE_TARGET_ENVIRONMENT=${TARGET_ENVIRONMENT} npm run build RUN VITE_TARGET_ENVIRONMENT=${TARGET_ENVIRONMENT} npm run build:demo diff --git a/build.sh b/build.sh new file mode 100644 index 00000000..5b10f837 --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +tsc --resolveJsonModule --esModuleInterop options_tester.ts +mv options_tester.js options_tester.cjs \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index db1ea38c..937c4ffb 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -21,11 +21,13 @@ services: spot: image: samply/rustyspot:main ports: - - 8080:8080 + - 8055:8055 environment: + RUST_LOG: "info" + CORS_ORIGIN: http://localhost:5173 BEAM_SECRET: "${LOCAL_BEAM_SECRET_SPOT}" BEAM_PROXY_URL: http://beam-proxy:8081 - BEAM_APP_ID: "spot.${LOCAL_BEAM_ID}.${BROKER_HOST}" + BEAM_APP_ID: "focus.${LOCAL_BEAM_ID}.${BROKER_HOST}" depends_on: - "beam-proxy" profiles: diff --git a/docker-compose.yml b/docker-compose.yml index 1c06385d..146da345 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,10 +36,13 @@ services: # For some reason, login.verbis.dkfz.de does not have a "groups" scope but this comes automatically through a # scope called microprofile-jwt. Remove the following line once we have a "groups" scope. - OAUTH2_PROXY_SCOPE=openid profile email + # Pass Authorization Header and some user information to spot + - OAUTH2_PROXY_SET_AUTHORIZATION_HEADER=true + - OAUTH2_PROXY_SET_XAUTHREQUEST=true labels: - "traefik.enable=true" - "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4180" - - "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User" + - "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=Authorization, X-Forwarded-User, X-Auth-Request-User, X-Auth-Request-Email" - "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4180" - "traefik.http.routers.oauth2.rule=Host(`${GUI_HOST}`) && PathPrefix(`/oauth2/`)" - "traefik.http.routers.oauth2.tls=true" @@ -55,6 +58,8 @@ services: spot: image: samply/rustyspot:main + ports: + - "8055:8055" environment: HTTP_PROXY: ${http_proxy} HTTPS_PROXY: ${https_proxy} @@ -66,6 +71,11 @@ services: CATALOGUE_URL: "${CATALOGUE_URL}" BIND_ADDR: 0.0.0.0:8055 PRISM_URL: http://prism:8066 + RUST_LOG: "info" + LOG_FILE: /requests.log + volumes: + - ./requests.log:/requests.log + depends_on: - "beam-proxy" labels: diff --git a/example.env b/example.env index 66837680..2e095cf5 100644 --- a/example.env +++ b/example.env @@ -11,6 +11,7 @@ GUI_HOST="data.dktk.dkfz.de|demo.lens.samply.de" BROKER_HOST="broker.ccp-it.dktk.dkfz.de" LOCAL_BEAM_ID="your-proxy-id" LOCAL_BEAM_SECRET_SPOT="insert-a-random-passphrase-here" +LOCAL_BEAM_SECRET_PRISM="insert-a-random-passphrase-here" # Request your OAUTH client from your oauth provider admin OAUTH_ISSUER_URL="the-discovery-adress-of-your-oauth-provider" diff --git a/options_tester.cjs b/options_tester.cjs new file mode 100644 index 00000000..9c41892e --- /dev/null +++ b/options_tester.cjs @@ -0,0 +1,36 @@ +"use strict"; +const __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; + +console.log( + "Checking Lens options for ", + process.env.VITE_TARGET_ENVIRONMENT, +); + +let optionsPath = ""; +if (process.env.VITE_TARGET_ENVIRONMENT === "production") { + optionsPath = "./packages/demo/public/options-ccp-prod.json"; +} else if (process.env.VITE_TARGET_ENVIRONMENT === "staging") { + optionsPath = "./packages/demo/public/options-ccp-demo.json"; +} else { + optionsPath = "./packages/demo/public/options-dev.json"; +} + +Object.defineProperty(exports, "__esModule", { value: true }); +const options_schema_json_1 = __importDefault(require("./packages/lib/src/types/options.schema.json")); +const schemasafe_1 = require("@exodus/schemasafe"); +const options_json_1 = __importDefault(require(optionsPath)); +console.log("Checking Lens options"); +const parse = (0, schemasafe_1.parser)(options_schema_json_1.default, { + includeErrors: true, + allErrors: true, +}); +const validJSON = parse(JSON.stringify(options_json_1.default)); +if (validJSON.valid === true) { + console.log("Options are valid"); +} +else if (typeof options_json_1.default === "object") { + console.error("Lens-Options are not conform with the JSON schema", validJSON.errors); + process.exit(1); +} diff --git a/options_tester.ts b/options_tester.ts new file mode 100644 index 00000000..d7751695 --- /dev/null +++ b/options_tester.ts @@ -0,0 +1,35 @@ +import optionsSchema from "./packages/lib/src/types/options.schema.json"; +import { parser } from "@exodus/schemasafe"; + +import devOptions from "./packages/demo/public/options-dev.json"; +import demoOptions from "./packages/demo/public/options-ccp-demo.json"; +import prodOptions from "./packages/demo/public/options-ccp-prod.json"; + +console.log( + "Checking Lens options for ", + import.meta.env.VITE_TARGET_ENVIRONMENT, +); + +let options = {}; +if (import.meta.env.VITE_TARGET_ENVIRONMENT === "production") { + options = prodOptions; +} else if (import.meta.env.VITE_TARGET_ENVIRONMENT === "staging") { + options = demoOptions; +} else { + options = devOptions; +} + +const parse = parser(optionsSchema, { + includeErrors: true, + allErrors: true, +}); +const validJSON = parse(JSON.stringify(options)); +if (validJSON.valid === true) { + console.log("Options are valid"); +} else if (typeof options === "object") { + console.error( + "Lens-Options are not conform with the JSON schema", + validJSON.errors, + ); + process.exit(1); +} diff --git a/package-lock.json b/package-lock.json index 174d0715..899af9f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@samply/lens", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@samply/lens", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { + "@exodus/schemasafe": "^1.3.0", "chart.js": "^4.4.0", "uuid": "^9.0.0" }, @@ -644,54 +645,6 @@ "node": ">=16" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", @@ -708,294 +661,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1052,6 +717,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "license": "MIT" + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -3315,9 +2986,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -3325,6 +2996,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -5975,10 +5647,11 @@ } }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", diff --git a/package.json b/package.json index a3da007d..deb1b70c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@samply/lens", "description": "A web component library for clinical data search and visualisation", - "version": "0.1.0", + "version": "0.2.0", "type": "module", "module": "dist/lens.js", "main": "dist/lens.umd.js", @@ -30,9 +30,9 @@ "start": "npm run dev -s", "dev": "vite --config vite.demo.config.ts", "build": "vite build", - "build:demo": "vite build --config vite.demo.config.ts", + "build:demo": "node options_tester.cjs && vite build --config vite.demo.config.ts", "preview": "vite preview --config vite.demo.config.ts", - "check": "svelte-check --tsconfig ./tsconfig.json", + "check": "node options_tester.cjs && svelte-check --tsconfig ./tsconfig.json", "lint": "lint-staged", "watch": "rimraf dist && vite build --watch", "link": "wait-on dist/types.d.ts && cd dist/ && npm link", @@ -69,6 +69,7 @@ "wait-on": "^7.2.0" }, "dependencies": { + "@exodus/schemasafe": "^1.3.0", "chart.js": "^4.4.0", "uuid": "^9.0.0" }, diff --git a/packages/demo/public/catalogues/catalogue-dktk.json b/packages/demo/public/catalogues/catalogue-dktk.json index fcf31fb5..04489451 100644 --- a/packages/demo/public/catalogues/catalogue-dktk.json +++ b/packages/demo/public/catalogues/catalogue-dktk.json @@ -2480,6 +2480,19 @@ "fieldType": "single-select", "type": "EQUALS", "criteria": [ + { + "key": "urn:dktk:code:65:2", + "name": "Neuroendokriner Tumor (NET) - Grad I", + "description": "", + "aggregatedValue": [ + [ + { + "value": "59847-4", + "name": "8240/3" + } + ] + ] + }, { "key": "urn:dktk:code:66:2", "name": "Neuroendokriner Tumor (NET) - Grad II", @@ -2513,25 +2526,6 @@ } ] ] - }, - { - "key": "urn:dktk:code:65:2", - "name": "Neuroendokriner Tumor (NET) - Grad I", - "description": "", - "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], - [ - { - "value": "59847-4", - "name": "8240/3" - } - ] - ] } ] }, @@ -6151,12 +6145,6 @@ "name": "Adipozytäres Weichteilsarkom", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -6182,12 +6170,6 @@ "name": "Adipoz. Weichteilsarkom - unsicheren Verhaltens", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "D%" - } - ], [ { "value": "59847-4", @@ -6201,12 +6183,6 @@ "name": "Fibroblastisches Weichteilsarkom", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -6244,12 +6220,6 @@ "name": "Fibrobl. Weichteilsarkom - unsicheren Verhaltens", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "D%" - } - ], [ { "value": "59847-4", @@ -6295,12 +6265,6 @@ "name": "Fibrohistiozytäres Weichteilsarkom", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -6314,12 +6278,6 @@ "name": "Fibrohistioz. Weichteilsarkom - unsicheren Verhaltens", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "D%" - } - ], [ { "value": "59847-4", @@ -6341,12 +6299,6 @@ "name": "Glattmuskuläres Weichteilsarkom/Leiomyosarkom", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -6360,12 +6312,6 @@ "name": "Perizytisches Weichteilsarkom", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -6379,12 +6325,6 @@ "name": "Perizyt. Weichteilsarkom - unsicheren Verhaltens", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "D%" - } - ], [ { "value": "59847-4", @@ -6402,12 +6342,6 @@ "name": "Skelettmuskuläres Weichteilsarkom/Rhabdomyosarkom", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -6441,12 +6375,6 @@ "name": "Synovialsarkom", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -6468,12 +6396,6 @@ "name": "Vaskuläres Weichteilsarkom", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -6534,12 +6456,6 @@ "name": "Vask. Weichteilsarkom - unsicheren Verhaltens", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "D%" - } - ], [ { "value": "59847-4", @@ -6561,12 +6477,6 @@ "name": "Weichteilsark. d. Nervenscheiden - unsicheren Verhaltens", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "D%" - } - ], [ { "value": "59847-4", @@ -6580,12 +6490,6 @@ "name": "Sonstiges Weichteilsarkom (unklare Differenzierung)", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -6647,12 +6551,6 @@ "name": "Sonstiges Weichteilsark. (unklare Diff.) - unsicheren Verhaltens", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "D%" - } - ], [ { "value": "59847-4", @@ -6687,12 +6585,6 @@ "name": "Undifferenzierter/unklassifizierter Tumor", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -7078,12 +6970,6 @@ "name": "Chorion-Ca", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -7097,12 +6983,6 @@ "name": "Germinom", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -7136,12 +7016,6 @@ "name": "Embryonal-Ca", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -7159,12 +7033,6 @@ "name": "Dottersack-Ca", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -7178,12 +7046,6 @@ "name": "Teratom", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -7217,12 +7079,6 @@ "name": "Weiterer Keimzelltumor", "description": "", "aggregatedValue": [ - [ - { - "value": "diagnosis", - "name": "C%" - } - ], [ { "value": "59847-4", @@ -7259,6 +7115,8 @@ "system": "", "fieldType": "number", "type": "BETWEEN", + "min": 1900, + "max": 2040, "criteria": [] }, { @@ -7267,6 +7125,8 @@ "system": "", "fieldType": "number", "type": "BETWEEN", + "min": 0, + "max": 150, "criteria": [] }, { diff --git a/packages/demo/public/delete_icon.svg b/packages/demo/public/delete_icon.svg new file mode 100644 index 00000000..917e1d65 --- /dev/null +++ b/packages/demo/public/delete_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/demo/public/options.json b/packages/demo/public/options-ccp-demo.json similarity index 76% rename from packages/demo/public/options.json rename to packages/demo/public/options-ccp-demo.json index 262f46f5..cc44bc01 100644 --- a/packages/demo/public/options.json +++ b/packages/demo/public/options-ccp-demo.json @@ -1,10 +1,29 @@ { "iconOptions": { + "deleteUrl": "delete_icon.svg", "infoUrl": "info-circle-svgrepo-com.svg", "selectAll": { "text": "Add all" } }, + "siteMappings": { + "berlin": "Berlin", + "berlin-test": "Berlin", + "bonn": "Bonn", + "dresden": "Dresden", + "essen": "Essen", + "frankfurt": "Frankfurt", + "freiburg": "Freiburg", + "hannover": "Hannover", + "mainz": "Mainz", + "muenchen-lmu": "München(LMU)", + "muenchen-tum": "München(TUM)", + "ulm": "Ulm", + "wuerzburg": "Würzburg", + "mannheim": "Mannheim", + "dktk-test": "DKTK-Test", + "hamburg": "Hamburg" + }, "chartOptions": { "patients": { "legendMapping": { @@ -171,8 +190,6 @@ { "stratifierCode": "Histologies", "stratumCode": "1" - }, - { } ] } @@ -192,5 +209,60 @@ "dataKey": "patients" } ] + }, + "backends": { + "spots": [ + { + "name": "DKTK", + "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", + "url": "https://backend.demo.lens.samply.de/prod/", + "sites": [ + "berlin", + "dresden", + "essen", + "frankfurt", + "freiburg", + "hannover", + "mainz", + "muenchen-lmu", + "muenchen-tum", + "ulm", + "wuerzburg", + "mannheim", + "dktk-test", + "hamburg" + ], + "catalogueKeyToResponseKeyMap": [ + [ + "gender", + "Gender" + ], + [ + "age_at_diagnosis", + "Age" + ], + [ + "diagnosis", + "diagnosis" + ], + [ + "medicationStatements", + "MedicationType" + ], + [ + "sample_kind", + "sample_kind" + ], + [ + "therapy_of_tumor", + "ProcedureType" + ], + [ + "75186-7", + "75186-7" + ] + ] + } + ] } } diff --git a/packages/demo/public/options-ccp-prod.json b/packages/demo/public/options-ccp-prod.json new file mode 100644 index 00000000..4888e2c4 --- /dev/null +++ b/packages/demo/public/options-ccp-prod.json @@ -0,0 +1,271 @@ +{ + "iconOptions": { + "deleteUrl": "delete_icon.svg", + "infoUrl": "info-circle-svgrepo-com.svg", + "selectAll": { + "text": "Add all" + } + }, + "siteMappings": { + "berlin": "Berlin", + "berlin-test": "Berlin", + "bonn": "Bonn", + "dresden": "Dresden", + "essen": "Essen", + "frankfurt": "Frankfurt", + "freiburg": "Freiburg", + "hannover": "Hannover", + "mainz": "Mainz", + "muenchen-lmu": "München(LMU)", + "muenchen-tum": "München(TUM)", + "ulm": "Ulm", + "wuerzburg": "Würzburg", + "mannheim": "Mannheim", + "dktk-test": "DKTK-Test", + "hamburg": "Hamburg" + }, + "chartOptions": { + "patients": { + "legendMapping": { + "berlin": "Berlin", + "berlin-test": "Berlin", + "bonn": "Bonn", + "dresden": "Dresden", + "essen": "Essen", + "frankfurt": "Frankfurt", + "freiburg": "Freiburg", + "hannover": "Hannover", + "mainz": "Mainz", + "muenchen-lmu": "München(LMU)", + "muenchen-tum": "München(TUM)", + "ulm": "Ulm", + "wuerzburg": "Würzburg", + "mannheim": "Mannheim", + "dktk-test": "DKTK-Test", + "hamburg": "Hamburg" + } + }, + "gender": { + "legendMapping": { + "male": "Männlich", + "female": "Weiblich", + "unknown": "Unbekannt", + "other": "Divers" + } + }, + "diagnosis": { + "hintText": [ + "Bei Patienten mit mehreren onkologischen Diagnosen werden auch Einträge angezeigt, die ggfs. nicht den ausgewählten Suchkriterien entsprechen." + ] + }, + "age_at_diagnosis": { + "hintText": [ + "Bei Patienten mit mehreren Erstdiagnosen werden auch Einträge angezeigt, die ggfs. außerhalb der Suchkriterien liegen. " + ] + }, + "75186-7": { + "hintText": [ + "\"verstorben\": ein Todesdatum ist dokumentiert oder das aktuelle Lebensalter ist größer 123 Jahre.", + "\"lebend\": wird angenommen, wenn kein Todesdatum dokumentiert ist oder das aktuelle Lebensalter nicht 123 Jahre überschritten hat.", + "\"unbekannt\": kein Geburtsdatum oder Todesdatum bekannt." + ] + }, + "therapy_of_tumor": { + "hintText": [ + "Bei Patienten mit mehreren onkologischen Diagnosen werden auch Einträge angezeigt, die ggfs. nicht den ausgewählten Suchkriterien entsprechen." + ], + "aggregations": [ + "medicationStatements" + ], + "tooltips": { + "OP": "Operationen", + "ST": "Strahlentherapien", + "medicationStatements": "Systemische Therapien" + } + }, + "medicationStatements": { + "hintText": [ + "Angezeigt wird die Art der systemischen oder abwartenden Therapie (ADT Basisdatensatz Versionen 2014, 2021). Bei Patienten mit mehreren onkologischen Diagnosen werden auch Einträge angezeigt, die ggfs. nicht den ausgewählten Suchkriterien entsprechen. " + ], + "tooltips": { + "CH": "Chemotherapie", + "HO": "Hormontherapie", + "IM": "Immun-/Antikörpertherapie", + "KM": "Knochenmarktransplantation", + "ZS": "zielgerichtete Substanzen", + "CI": "Chemo- + Immun-/Antikörpertherapie", + "CZ": "Chemotherapie + zielgerichtete Substanzen", + "CIZ": "Chemo- + Immun-/Antikörpertherapie + zielgerichtete Substanzen", + "IZ": "Immun-/Antikörpertherapie + zielgerichtete Substanzen", + "SZ": "Stammzelltransplantation (inklusive Knochenmarktransplantation)", + "AS": "Active Surveillance", + "WS": "Wait and see", + "WW": "Watchful Waiting", + "SO": "Sonstiges" + } + }, + "sample_kind": { + "hintText": [ + "Verteilung der Probentypen die mit den identifizierten Patienten verbunden sind." + ], + "accumulatedValues": [ + { + "name": "ffpe-tissue", + "values": [ + "tissue-ffpe", + "tumor-tissue-ffpe", + "normal-tissue-ffpe", + "other-tissue-ffpe" + ] + }, + { + "name": "frozen-tissue", + "values": [ + "tissue-frozen", + "tumor-tissue-frozen", + "normal-tissue-frozen", + "other-tissue-frozen" + ] + } + ], + "tooltips": { + "ffpe-tissue": "Gewebe FFPE", + "frozen-tissue": "Gewebe schockgefroren", + "tissue-other": "Gewebe, Andere Konservierungsart", + "whole-blood": "Vollblut", + "blood-serum": "Serum", + "blood-plasma": "Plasma", + "buffy-coat": "Buffy Coat", + "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)", + "dried-whole-blood": "Blutkarten", + "swab": "Abstrich", + "ascites": "Aszites", + "stool-faeces": "Stuhl", + "urine": "Urin", + "csf-liquor": "Liquor", + "bone-marrow": "Knochenmark", + "saliva": "Speichel", + "liquid-other": "Flüssigprobe, Andere", + "dna": "DNA", + "rna": "RNA", + "derivative-other": "Derivat, Andere" + }, + "legendMapping":{ + "ffpe-tissue": "Gewebe FFPE", + "frozen-tissue": "Gewebe schockgefroren", + "tissue-other": "Gewebe, Andere Konservierungsart", + "whole-blood": "Vollblut", + "blood-serum": "Serum", + "blood-plasma": "Plasma", + "buffy-coat": "Buffy Coat", + "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)", + "dried-whole-blood": "Blutkarten", + "swab": "Abstrich", + "ascites": "Aszites", + "stool-faeces": "Stuhl", + "urine": "Urin", + "csf-liquor": "Liquor", + "bone-marrow": "Knochenmark", + "saliva": "Speichel", + "liquid-other": "Flüssigprobe, Andere", + "dna": "DNA", + "rna": "RNA", + "derivative-other": "Derivat, Andere" + } + } + }, + "tableOptions": { + "headerData": [ + { + "title": "Standorte", + "dataKey": "site" + }, + { + "title": "Patienten", + "dataKey": "patients" + }, + { + "title": "Bioproben*", + "aggregatedDataKeys": [ + { + "groupCode": "specimen" + }, + { + "stratifierCode": "Histologies", + "stratumCode": "1" + } + ] + } + ], + "claimedText": "Processing..." + }, + "resultSummaryOptions": { + "title": "Ergebnisse", + "infoButtonText": "Um eine Re-Identifizierung zu erschweren, werden Standortergebnisse modifiziert und auf Zehnerstellen gerundet. Meldet ein Standort keinen Treffer, wird für diesen null angezeigt.", + "dataTypes": [ + { + "title": "Standorte", + "dataKey": "collections" + }, + { + "title": "Patienten", + "dataKey": "patients" + } + ] + }, + "backends": { + "spots": [ + { + "name": "DKTK", + "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", + "url": "https://backend.data.dktk.dkfz.de/prod/", + "sites": [ + "berlin", + "dresden", + "essen", + "frankfurt", + "freiburg", + "hannover", + "mainz", + "muenchen-lmu", + "muenchen-tum", + "ulm", + "wuerzburg", + "mannheim", + "dktk-test", + "hamburg" + ], + "catalogueKeyToResponseKeyMap": [ + [ + "gender", + "Gender" + ], + [ + "age_at_diagnosis", + "Age" + ], + [ + "diagnosis", + "diagnosis" + ], + [ + "medicationStatements", + "MedicationType" + ], + [ + "sample_kind", + "sample_kind" + ], + [ + "therapy_of_tumor", + "ProcedureType" + ], + [ + "75186-7", + "75186-7" + ] + ] + } + ] + } +} diff --git a/packages/demo/public/options-dev.json b/packages/demo/public/options-dev.json new file mode 100644 index 00000000..e781695f --- /dev/null +++ b/packages/demo/public/options-dev.json @@ -0,0 +1,308 @@ +{ + "iconOptions": { + "deleteUrl": "delete_icon.svg", + "infoUrl": "info-circle-svgrepo-com.svg", + "selectAll": { + "text": "Add all" + } + }, + "siteMappings": { + "berlin": "Berlin", + "berlin-test": "Berlin", + "bonn": "Bonn", + "dresden": "Dresden", + "essen": "Essen", + "frankfurt": "Frankfurt", + "freiburg": "Freiburg", + "hannover": "Hannover", + "mainz": "Mainz", + "muenchen-lmu": "München(LMU)", + "muenchen-tum": "München(TUM)", + "ulm": "Ulm", + "wuerzburg": "Würzburg", + "mannheim": "Mannheim", + "dktk-test": "DKTK-Test", + "hamburg": "Hamburg" + }, + "chartOptions": { + "patients": { + "legendMapping": { + "berlin": "Berlin", + "berlin-test": "Berlin", + "bonn": "Bonn", + "dresden": "Dresden", + "essen": "Essen", + "frankfurt": "Frankfurt", + "freiburg": "Freiburg", + "hannover": "Hannover", + "mainz": "Mainz", + "muenchen-lmu": "München(LMU)", + "muenchen-tum": "München(TUM)", + "ulm": "Ulm", + "wuerzburg": "Würzburg", + "mannheim": "Mannheim", + "dktk-test": "DKTK-Test", + "hamburg": "Hamburg" + } + }, + "gender": { + "legendMapping": { + "male": "Männlich", + "female": "Weiblich", + "unknown": "Unbekannt", + "other": "Divers" + } + }, + "diagnosis": { + "hintText": [ + "Bei Patienten mit mehreren onkologischen Diagnosen werden auch Einträge angezeigt, die ggfs. nicht den ausgewählten Suchkriterien entsprechen." + ] + }, + "age_at_diagnosis": { + "hintText": [ + "Bei Patienten mit mehreren Erstdiagnosen werden auch Einträge angezeigt, die ggfs. außerhalb der Suchkriterien liegen. " + ] + }, + "75186-7": { + "hintText": [ + "\"verstorben\": ein Todesdatum ist dokumentiert oder das aktuelle Lebensalter ist größer 123 Jahre.", + "\"lebend\": wird angenommen, wenn kein Todesdatum dokumentiert ist oder das aktuelle Lebensalter nicht 123 Jahre überschritten hat.", + "\"unbekannt\": kein Geburtsdatum oder Todesdatum bekannt." + ] + }, + "therapy_of_tumor": { + "aggregations": [ + "medicationStatements" + ], + "tooltips": { + "OP": "Operationen", + "ST": "Strahlentherapien", + "medicationStatements": "Systemische Therapien" + } + }, + "medicationStatements": { + "hintText": [ + "Art der systemischen oder abwartenden Therapie (ADT Basisdatensatz Versionen 2014, 2021)" + ], + "tooltips": { + "CH": "Chemotherapie", + "HO": "Hormontherapie", + "IM": "Immun-/Antikörpertherapie", + "KM": "Knochenmarktransplantation", + "ZS": "zielgerichtete Substanzen", + "CI": "Chemo- + Immun-/Antikörpertherapie", + "CZ": "Chemotherapie + zielgerichtete Substanzen", + "CIZ": "Chemo- + Immun-/Antikörpertherapie + zielgerichtete Substanzen", + "IZ": "Immun-/Antikörpertherapie + zielgerichtete Substanzen", + "SZ": "Stammzelltransplantation (inklusive Knochenmarktransplantation)", + "AS": "Active Surveillance", + "WS": "Wait and see", + "WW": "Watchful Waiting", + "SO": "Sonstiges" + } + }, + "sample_kind": { + "hintText": [ + "Verteilung der Probentypen die mit den identifizierten Patienten verbunden sind." + ], + "accumulatedValues": [ + { + "name": "ffpe-tissue", + "values": [ + "tissue-ffpe", + "tumor-tissue-ffpe", + "normal-tissue-ffpe", + "other-tissue-ffpe" + ] + }, + { + "name": "frozen-tissue", + "values": [ + "tissue-frozen", + "tumor-tissue-frozen", + "normal-tissue-frozen", + "other-tissue-frozen" + ] + } + ], + "tooltips": { + "ffpe-tissue": "Gewebe FFPE", + "frozen-tissue": "Gewebe schockgefroren", + "tissue-other": "Gewebe, Andere Konservierungsart", + "whole-blood": "Vollblut", + "blood-serum": "Serum", + "blood-plasma": "Plasma", + "buffy-coat": "Buffy Coat", + "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)", + "dried-whole-blood": "Blutkarten", + "swab": "Abstrich", + "ascites": "Aszites", + "stool-faeces": "Stuhl", + "urine": "Urin", + "csf-liquor": "Liquor", + "bone-marrow": "Knochenmark", + "saliva": "Speichel", + "liquid-other": "Flüssigprobe, Andere", + "dna": "DNA", + "rna": "RNA", + "derivative-other": "Derivat, Andere" + }, + "legendMapping":{ + "ffpe-tissue": "Gewebe FFPE", + "frozen-tissue": "Gewebe schockgefroren", + "tissue-other": "Gewebe, Andere Konservierungsart", + "whole-blood": "Vollblut", + "blood-serum": "Serum", + "blood-plasma": "Plasma", + "buffy-coat": "Buffy Coat", + "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)", + "dried-whole-blood": "Blutkarten", + "swab": "Abstrich", + "ascites": "Aszites", + "stool-faeces": "Stuhl", + "urine": "Urin", + "csf-liquor": "Liquor", + "bone-marrow": "Knochenmark", + "saliva": "Speichel", + "liquid-other": "Flüssigprobe, Andere", + "dna": "DNA", + "rna": "RNA", + "derivative-other": "Derivat, Andere" + } + } + }, + "tableOptions": { + "headerData": [ + { + "title": "Standorte", + "dataKey": "site" + }, + { + "title": "Patienten", + "dataKey": "patients" + }, + { + "title": "Bioproben*", + "aggregatedDataKeys": [ + { + "groupCode": "specimen" + }, + { + "stratifierCode": "Histologies", + "stratumCode": "1" + } + ] + } + ], + "claimedText": "Processing..." + }, + "resultSummaryOptions": { + "title": "Ergebnisse", + "infoButtonText": "Um eine Re-Identifizierung zu erschweren, werden Standortergebnisse modifiziert und auf Zehnerstellen gerundet. Meldet ein Standort keinen Treffer, wird für diesen null angezeigt.", + "dataTypes": [ + { + "title": "Standorte", + "dataKey": "collections" + }, + { + "title": "Patienten", + "dataKey": "patients" + } + ] + }, + "backends": { + "customBackends": [ + "someUrl" + ], + "spots": [ + { + "name": "DKTK", + "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", + "url": "http://localhost:8055", + "sites": [ + "berlin", + "dresden", + "essen", + "frankfurt", + "freiburg", + "hannover", + "mainz", + "muenchen-lmu", + "muenchen-tum", + "ulm", + "wuerzburg", + "mannheim", + "dktk-test", + "hamburg" + ], + "catalogueKeyToResponseKeyMap": [ + [ + "gender", + "Gender" + ], + [ + "age_at_diagnosis", + "Age" + ], + [ + "diagnosis", + "diagnosis" + ], + [ + "medicationStatements", + "MedicationType" + ], + [ + "sample_kind", + "sample_kind" + ], + [ + "therapy_of_tumor", + "ProcedureType" + ], + [ + "75186-7", + "75186-7" + ] + ] + } + ], + "blazes": [ + { + "name": "DKTK", + "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", + "url": "http://localhost:8080", + "catalogueKeyToResponseKeyMap": [ + [ + "gender", + "Gender" + ], + [ + "age_at_diagnosis", + "Age" + ], + [ + "diagnosis", + "diagnosis" + ], + [ + "medicationStatements", + "MedicationType" + ], + [ + "sample_kind", + "sample_kind" + ], + [ + "therapy_of_tumor", + "ProcedureType" + ], + [ + "75186-7", + "75186-7" + ] + ] + } + ] + } +} diff --git a/packages/demo/src/AppCCP.svelte b/packages/demo/src/AppCCP.svelte index aeadcefc..8030f4a7 100644 --- a/packages/demo/src/AppCCP.svelte +++ b/packages/demo/src/AppCCP.svelte @@ -1,4 +1,9 @@
- -

CCP Explorer

-
-
-
-

Suchkriterien

- + - + +
+
+ +
+
+
+

Suchkriterien

+ + +
@@ -245,7 +209,7 @@ chartType="bar" headers={therapyHeaders} xAxisTitle="Art der Therapie" - yAxisTitle="Anzahl der Therapien" + yAxisTitle="Anzahl der Therapieeinträge" backgroundColor={JSON.stringify(barChartBackgroundColors)} />
@@ -255,7 +219,7 @@ catalogueGroupCode="medicationStatements" chartType="bar" xAxisTitle="Art der Therapie" - yAxisTitle="Anzahl der Therapien" + yAxisTitle="Anzahl der Therapieeinträge" backgroundColor={JSON.stringify(barChartBackgroundColors)} />
@@ -279,22 +243,29 @@ Clinical Communication Platform (CCP) NutzungsvereinbarungNutzungsvereinbarung DatenschutzDatenschutz - ImpressumImpressum - + + diff --git a/packages/demo/src/AppFragmentDevelopment.svelte b/packages/demo/src/AppFragmentDevelopment.svelte index 9c7e7a80..f14019a5 100644 --- a/packages/demo/src/AppFragmentDevelopment.svelte +++ b/packages/demo/src/AppFragmentDevelopment.svelte @@ -1,5 +1,13 @@
@@ -193,11 +193,7 @@

Search Button

- +

Result Summary Bar

@@ -207,7 +203,7 @@

Result Table

- +

Result Pie Chart

@@ -242,4 +238,5 @@
- + + diff --git a/packages/demo/src/backends/ast-to-cql-translator.ts b/packages/demo/src/backends/ast-to-cql-translator.ts new file mode 100644 index 00000000..6b6a8148 --- /dev/null +++ b/packages/demo/src/backends/ast-to-cql-translator.ts @@ -0,0 +1,380 @@ +/** + * TODO: Document this file. Move to Project + */ + +import type { + AstBottomLayerValue, + AstElement, + AstTopLayer, + MeasureItem, +} from "../../../../dist/types"; +import { + alias as aliasMap, + cqltemplate, + criterionMap, +} from "./cqlquery-mappings"; + +let codesystems: string[] = []; +let criteria: string[]; + +export const translateAstToCql = ( + query: AstTopLayer, + returnOnlySingeltons: boolean = true, + backendMeasures: string, + measures: MeasureItem[], + criterionList: string[], +): string => { + criteria = criterionList; + + /** + * DISCUSS: why is this even an array? + * in bbmri there is only concatted to the string + */ + codesystems = [ + // NOTE: We always need loinc, as the Deceased Stratifier is computed with it!!! + "codesystem loinc: 'http://loinc.org'", + ]; + + const cqlHeader = + "library Retrieve\n" + + "using FHIR version '4.0.0'\n" + + "include FHIRHelpers version '4.0.0'\n" + + "\n"; + + let singletons: string = ""; + singletons = backendMeasures; + singletons += resolveOperation(query); + + if (query.children.length == 0) { + singletons += "\ntrue"; + } + + if (returnOnlySingeltons) { + return singletons; + } + + return ( + cqlHeader + + getCodesystems() + + "context Patient\n" + + measures.map((measureItem: MeasureItem) => measureItem.cql).join("") + + singletons + ); +}; + +const resolveOperation = (operation: AstElement): string => { + let expression: string = ""; + + if ("children" in operation && operation.children.length > 1) { + expression += "("; + } + + "children" in operation && + operation.children.forEach((element: AstElement, index) => { + if ("children" in element) { + expression += resolveOperation(element); + } + if ( + "key" in element && + "type" in element && + "system" in element && + "value" in element + ) { + expression += getSingleton(element); + } + if (index < operation.children.length - 1) { + expression += + ")" + ` ${operation.operand.toLowerCase()} ` + "\n("; + } else { + if (operation.children.length > 1) { + expression += ")"; + } + } + }); + + return expression; +}; + +const getSingleton = (criterion: AstBottomLayerValue): string => { + let expression: string = ""; + + //TODO: Workaround for using the value of "Therapy of Tumor" as key. Need an additional field in catalogue + if (criterion.key === "therapy_of_tumor") { + criterion.key = criterion.value as string; + } + + const myCriterion = criterionMap.get(criterion.key); + + if (myCriterion) { + const myCQL = cqltemplate.get(myCriterion.type); + if (myCQL) { + switch (myCriterion.type) { + case "gender": + case "BBMRI_gender": + case "histology": + case "conditionValue": + case "BBMRI_conditionValue": + case "BBMRI_conditionSampleDiagnosis": + case "conditionBodySite": + case "conditionLocalization": + case "observation": + case "uiccstadium": + case "observationMetastasis": + case "observationMetastasisBodySite": + case "procedure": + case "procedureResidualstatus": + case "medicationStatement": + case "specimen": + case "BBMRI_specimen": + case "BBMRI_hasSpecimen": + case "hasSpecimen": + case "Organization": + case "observationMolecularMarkerName": + case "observationMolecularMarkerAminoacidchange": + case "observationMolecularMarkerDNAchange": + case "observationMolecularMarkerSeqRefNCBI": + case "observationMolecularMarkerEnsemblID": + case "department": + case "TNMp": + case "TNMc": { + if (typeof criterion.value === "string") { + // TODO: Check if we really need to do this or we can somehow tell cql to do that expansion it self + if ( + criterion.value.slice(-1) === "%" && + criterion.value.length == 5 + ) { + const mykey = criterion.value.slice(0, -2); + if (criteria != undefined) { + const expandedValues = criteria.filter( + (value) => value.startsWith(mykey), + ); + expression += getSingleton({ + key: criterion.key, + type: criterion.type, + system: criterion.system, + value: expandedValues, + }); + } + } else if ( + criterion.value.slice(-1) === "%" && + criterion.value.length == 6 + ) { + const mykey = criterion.value.slice(0, -1); + if (criteria != undefined) { + const expandedValues = criteria.filter( + (value) => value.startsWith(mykey), + ); + expandedValues.push( + criterion.value.slice(0, 5), + ); + expression += getSingleton({ + key: criterion.key, + type: criterion.type, + system: criterion.system, + value: expandedValues, + }); + } + } else { + expression += substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + criterion.value as string, + ); + } + } + if (typeof criterion.value === "boolean") { + expression += substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + ); + } + + if (criterion.value instanceof Array) { + if (criterion.value.length === 1) { + expression += substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + criterion.value[0], + ); + } else { + criterion.value.forEach((value: string) => { + expression += + "(" + + substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + value, + ) + + ") or\n"; + }); + expression = expression.slice(0, -4); + } + } + + break; + } + + case "conditionRangeDate": { + expression += substituteRangeCQLExpression( + criterion, + myCriterion, + "condition", + "Date", + myCQL, + ); + break; + } + + case "primaryConditionRangeDate": { + expression += substituteRangeCQLExpression( + criterion, + myCriterion, + "primaryCondition", + "Date", + myCQL, + ); + break; + } + + case "conditionRangeAge": { + expression += substituteRangeCQLExpression( + criterion, + myCriterion, + "condition", + "Age", + myCQL, + ); + break; + } + + case "primaryConditionRangeAge": { + expression += substituteRangeCQLExpression( + criterion, + myCriterion, + "primaryCondition", + "Age", + myCQL, + ); + break; + } + } + } + } + return expression; +}; + +const substituteRangeCQLExpression = ( + criterion: AstBottomLayerValue, + myCriterion: { type: string; alias?: string[] }, + criterionPrefix: string, + criterionSuffix: string, + rangeCQL: string, +): string => { + const input = criterion.value as { min: number; max: number }; + if (input === null) { + console.warn( + `Throwing away a ${criterionPrefix}Range${criterionSuffix} criterion, as it is not of type {min: number, max: number}!`, + ); + return ""; + } + if (input.min === 0 && input.max === 0) { + console.warn( + `Throwing away a ${criterionPrefix}Range${criterionSuffix} criterion, as both dates are undefined!`, + ); + return ""; + } else if (input.min === 0) { + const lowerThanDateTemplate = cqltemplate.get( + `${criterionPrefix}LowerThan${criterionSuffix}`, + ); + if (lowerThanDateTemplate) + return substituteCQLExpression( + criterion.key, + myCriterion.alias, + lowerThanDateTemplate, + "", + input.min, + input.max, + ); + } else if (input.max === 0) { + const greaterThanDateTemplate = cqltemplate.get( + `${criterionPrefix}GreaterThan${criterionSuffix}`, + ); + if (greaterThanDateTemplate) + return substituteCQLExpression( + criterion.key, + myCriterion.alias, + greaterThanDateTemplate, + "", + input.min, + input.max, + ); + } else { + return substituteCQLExpression( + criterion.key, + myCriterion.alias, + rangeCQL, + "", + input.min, + input.max, + ); + } + return ""; +}; + +const substituteCQLExpression = ( + key: string, + alias: string[] | undefined, + cql: string, + value?: string, + min?: number, + max?: number, +): string => { + let cqlString: string; + if (value) { + cqlString = cql.replace(/{{C}}/g, value); + } else { + cqlString = cql; + } + cqlString = cqlString.replace(new RegExp("{{K}}"), key); + if (alias && alias[0]) { + cqlString = cqlString.replace(new RegExp("{{A1}}", "g"), alias[0]); + const systemExpression = + "codesystem " + alias[0] + ": '" + aliasMap.get(alias[0]) + "'"; + if (!codesystems.includes(systemExpression)) { + codesystems.push(systemExpression); + } + } + if (alias && alias[1]) { + cqlString = cqlString.replace(new RegExp("{{A2}}", "g"), alias[1]); + const systemExpression = + "codesystem " + alias[1] + ": '" + aliasMap.get(alias[1]) + "'"; + if (!codesystems.includes(systemExpression)) { + codesystems.push(systemExpression); + } + } + if (min || min === 0) { + cqlString = cqlString.replace(new RegExp("{{D1}}"), min.toString()); + } + if (max || max === 0) { + cqlString = cqlString.replace(new RegExp("{{D2}}"), max.toString()); + } + return cqlString; +}; + +const getCodesystems = (): string => { + let codesystemString: string = ""; + + codesystems.forEach((systems) => { + codesystemString += systems + "\n"; + }); + + if (codesystems.length > 0) { + codesystemString += "\n"; + } + + return codesystemString; +}; diff --git a/packages/demo/src/backends/blaze.ts b/packages/demo/src/backends/blaze.ts new file mode 100644 index 00000000..a475acd7 --- /dev/null +++ b/packages/demo/src/backends/blaze.ts @@ -0,0 +1,109 @@ +import { buildLibrary, buildMeasure } from "../helpers/cql-measure"; +import { responseStore } from "../stores/response"; +import type { Site } from "../types/response"; +import { measureStore } from "../stores/measures"; + +let measureDefinitions; + +measureStore.subscribe((store) => { + measureDefinitions = store.map((measure) => measure.measure); +}); + +export class Blaze { + constructor( + private url: URL, + private name: string, + private auth: string = "", + ) {} + + /** + * sends the query to beam and updates the store with the results + * @param cql the query as cql string + * @param controller the abort controller to cancel the request + */ + async send(cql: string, controller?: AbortController): Promise { + try { + responseStore.update((store) => { + store.set(this.name, { status: "claimed", data: null }); + return store; + }); + const libraryResponse = await fetch( + new URL(`${this.url}/Library`), + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(buildLibrary(cql)), + signal: controller?.signal, + }, + ); + if (!libraryResponse.ok) { + this.handleError( + `Couldn't create Library in Blaze`, + libraryResponse, + ); + } + const library = await libraryResponse.json(); + const measureResponse = await fetch( + new URL(`${this.url}/Measure`), + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify( + buildMeasure(library.url, measureDefinitions), + ), + signal: controller.signal, + }, + ); + if (!measureResponse.ok) { + this.handleError( + `Couldn't create Measure in Blaze`, + measureResponse, + ); + } + const measure = await measureResponse.json(); + const dataResponse = await fetch( + new URL( + `${this.url}/Measure/$evaluate-measure?measure=${measure.url}&periodStart=2000&periodEnd=2030`, + ), + { + signal: controller.signal, + }, + ); + if (!dataResponse.ok) { + this.handleError( + `Couldn't evaluate Measure in Blaze`, + dataResponse, + ); + } + const blazeResponse: Site = await dataResponse.json(); + responseStore.update((store) => { + store.set(this.name, { + status: "succeeded", + data: blazeResponse, + }); + return store; + }); + } catch (err) { + if (err.name === "AbortError") { + console.log(`Aborting former blaze request.`); + } else { + console.error(err); + } + } + } + + async handleError(message: string, response: Response): Promise { + const errorMessage = await response.text(); + console.debug( + `${message}. Received error ${response.status} with message ${errorMessage}`, + ); + responseStore.update((store) => { + store.set(this.name, { status: "permfailed", data: null }); + return store; + }); + } +} diff --git a/packages/demo/src/backends/cql-measure.ts b/packages/demo/src/backends/cql-measure.ts new file mode 100644 index 00000000..21f3b59a --- /dev/null +++ b/packages/demo/src/backends/cql-measure.ts @@ -0,0 +1,92 @@ +import { v4 as uuidv4 } from "uuid"; +import type { Measure } from "../types/backend"; + +type BuildLibraryReturn = { + resourceType: string; + url: string; + status: string; + type: { + coding: { + system: string; + code: string; + }[]; + }; + content: { + contentType: string; + data: string; + }[]; +}; + +export const buildLibrary = (cql: string): BuildLibraryReturn => { + const libraryId = uuidv4(); + const encodedQuery = btoa(unescape(encodeURIComponent(cql))); + return { + resourceType: "Library", + url: "urn:uuid:" + libraryId, + status: "active", + type: { + coding: [ + { + system: "http://terminology.hl7.org/CodeSystem/library-type", + code: "logic-library", + }, + ], + }, + content: [ + { + contentType: "text/cql", + data: encodedQuery, + }, + ], + }; +}; + +type BuildMeasureReturn = { + resourceType: string; + url: string; + status: string; + subjectCodeableConcept: { + coding: { + system: string; + code: string; + }[]; + }; + library: string; + scoring: { + coding: { + system: string; + code: string; + }[]; + }; + group: Measure[]; +}; + +export const buildMeasure = ( + libraryUrl: string, + measures: Measure[], +): BuildMeasureReturn => { + const measureId = uuidv4(); + return { + resourceType: "Measure", + url: "urn:uuid:" + measureId, + status: "active", + subjectCodeableConcept: { + coding: [ + { + system: "http://hl7.org/fhir/resource-types", + code: "Patient", + }, + ], + }, + library: libraryUrl, + scoring: { + coding: [ + { + system: "http://terminology.hl7.org/CodeSystem/measure-scoring", + code: "cohort", + }, + ], + }, + group: measures, // configuration.resultRequests.map(request => request.measures) + }; +}; diff --git a/packages/demo/src/backends/cqlquery-mappings.ts b/packages/demo/src/backends/cqlquery-mappings.ts new file mode 100644 index 00000000..12bcac63 --- /dev/null +++ b/packages/demo/src/backends/cqlquery-mappings.ts @@ -0,0 +1,429 @@ +export const alias = new Map([ + ["icd10", "http://fhir.de/CodeSystem/bfarm/icd-10-gm"], + ["loinc", "http://loinc.org"], + ["gradingcs", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/GradingCS"], + ["ops", "http://fhir.de/CodeSystem/bfarm/ops"], + ["morph", "urn:oid:2.16.840.1.113883.6.43.1"], + ["lokalisation_icd_o_3", "urn:oid:2.16.840.1.113883.6.43.1"], + [ + "bodySite", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/SeitenlokalisationCS", + ], + [ + "Therapieart", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/SYSTTherapieartCS", + ], + ["specimentype", "https://fhir.bbmri.de/CodeSystem/SampleMaterialType"], + [ + "uiccstadiumcs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/UiccstadiumCS", + ], + [ + "lokalebeurteilungresidualstatuscs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/LokaleBeurteilungResidualstatusCS", + ], + [ + "gesamtbeurteilungtumorstatuscs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/GesamtbeurteilungTumorstatusCS", + ], + [ + "verlauflokalertumorstatuscs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VerlaufLokalerTumorstatusCS", + ], + [ + "verlauftumorstatuslymphknotencs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VerlaufTumorstatusLymphknotenCS", + ], + [ + "verlauftumorstatusfernmetastasencs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VerlaufTumorstatusFernmetastasenCS", + ], + [ + "vitalstatuscs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VitalstatusCS", + ], + ["jnucs", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/JNUCS"], + [ + "fmlokalisationcs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/FMLokalisationCS", + ], + ["TNMTCS", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMTCS"], + ["TNMNCS", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMNCS"], + ["TNMMCS", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMMCS"], + [ + "TNMySymbolCS", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMySymbolCS", + ], + [ + "TNMrSymbolCS", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMrSymbolCS", + ], + [ + "TNMmSymbolCS", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMmSymbolCS", + ], + ["molecularMarker", "http://www.genenames.org"], + + ["BBMRI_icd10", "http://hl7.org/fhir/sid/icd-10"], + ["BBMRI_icd10gm", "http://fhir.de/CodeSystem/dimdi/icd-10-gm"], + [ + "BBMRI_SampleMaterialType", + "https://fhir.bbmri.de/CodeSystem/SampleMaterialType", + ], //specimentype + [ + "BBMRI_StorageTemperature", + "https://fhir.bbmri.de/CodeSystem/StorageTemperature", + ], + [ + "BBMRI_SmokingStatus", + "http://hl7.org/fhir/uv/ips/ValueSet/current-smoking-status-uv-ips", + ], +]); + +export const cqltemplate = new Map([ + ["gender", "Patient.gender = '{{C}}'"], + ["conditionValue", "exists [Condition: Code '{{C}}' from {{A1}}]"], + [ + "conditionBodySite", + "exists from [Condition] C\nwhere C.bodySite.coding contains Code '{{C}}' from {{A1}}", + ], + //TODO Revert to first expression if https://github.com/samply/blaze/issues/808 is solved + // ["conditionLocalization", "exists from [Condition] C\nwhere C.bodySite.coding contains Code '{{C}}' from {{A1}}"], + [ + "conditionLocalization", + "exists from [Condition] C\nwhere C.bodySite.coding.code contains '{{C}}'", + ], + [ + "conditionRangeDate", + "exists from [Condition] C\nwhere year from C.onset between {{D1}} and {{D2}}", + ], + [ + "conditionLowerThanDate", + "exists from [Condition] C\nwhere year from C.onset <= {{D2}}", + ], + [ + "conditionGreaterThanDate", + "exists from [Condition] C\nwhere year from C.onset >= {{D1}}", + ], + [ + "conditionRangeAge", + "exists [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) between {{D1}} and {{D2}}", + ], + [ + "conditionLowerThanAge", + "exists [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) <= {{D2}}", + ], + [ + "conditionGreaterThanAge", + "exists [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) >= {{D1}}", + ], + [ + "primaryConditionRangeDate", + "year from PrimaryDiagnosis.onset between {{D1}} and {{D2}}", + ], + [ + "primaryConditionLowerThanDate", + "year from PrimaryDiagnosis.onset <= {{D2}}", + ], + [ + "primaryConditionGreaterThanDate", + "year from PrimaryDiagnosis.onset >= {{D1}}", + ], + [ + "primaryConditionRangeAge", + "AgeInYearsAt(FHIRHelpers.ToDateTime(PrimaryDiagnosis.onset)) between {{D1}} and {{D2}}", + ], + [ + "primaryConditionLowerThanAge", + "AgeInYearsAt(FHIRHelpers.ToDateTime(PrimaryDiagnosis.onset)) <= {{D2}}", + ], + [ + "primaryConditionGreaterThanAge", + "AgeInYearsAt(FHIRHelpers.ToDateTime(PrimaryDiagnosis.onset)) >= {{D1}}", + ], + //TODO Revert to first expression if https://github.com/samply/blaze/issues/808 is solved + // ["observation", "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value.coding contains Code '{{C}}' from {{A2}}"], + [ + "observation", + "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'", + ], + [ + "observationMetastasis", + "exists from [Observation: Code '21907-1' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'", + ], + [ + "observationMetastasisBodySite", + "exists from [Observation: Code '21907-1' from {{A1}}] O\nwhere O.bodySite.coding.code contains '{{C}}'", + ], + [ + "observationMolecularMarkerName", + "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value.coding contains Code '{{C}}' from {{A2}}", + ], + [ + "observationMolecularMarkerAminoacidchange", + "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'", + ], //TODO @ThomasK replace C with S + [ + "observationMolecularMarkerDNAchange", + "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'", + ], + [ + "observationMolecularMarkerSeqRefNCBI", + "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'", + ], + [ + "observationMolecularMarkerEnsemblID", + "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'", + ], + ["procedure", "exists [Procedure: category in Code '{{K}}' from {{A1}}]"], + [ + "procedureResidualstatus", + "exists from [Procedure: category in Code 'OP' from {{A1}}] P\nwhere P.outcome.coding.code contains '{{C}}'", + ], + [ + "medicationStatement", + "exists [MedicationStatement: category in Code '{{K}}' from {{A1}}]", + ], + ["hasSpecimen", "exists [Specimen]"], + ["specimen", "exists [Specimen: Code '{{C}}' from {{A1}}]"], + [ + "TNMc", + "exists from [Observation: Code '21908-9' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value.coding contains Code '{{C}}' from {{A2}}", + ], + [ + "TNMp", + "exists from [Observation: Code '21902-2' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value.coding contains Code '{{C}}' from {{A2}}", + ], + [ + "Organization", + "Patient.managingOrganization.reference = \"Organization Ref\"('Klinisches Krebsregister/ITM')", + ], + [ + "department", + "exists from [Encounter] I\nwhere I.identifier.value = '{{C}}' ", + ], + [ + "uiccstadium", + "(exists ([Observation: Code '21908-9' from loinc] O where O.value.coding.code contains '{{C}}')) or (exists ([Observation: Code '21902-2' from loinc] O where O.value.coding.code contains '{{C}}'))", + ], + ["histology", "exists from [Observation: Code '59847-4' from loinc] O\n"], + + ["BBMRI_gender", "Patient.gender"], + [ + "BBMRI_conditionSampleDiagnosis", + "((exists[Condition: Code '{{C}}' from {{A1}}]) or (exists[Condition: Code '{{C}}' from {{A2}}])) or (exists from [Specimen] S where (S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/SampleDiagnosis').value.coding.code contains '{{C}}'))", + ], + ["BBMRI_conditionValue", "exists [Condition: Code '{{C}}' from {{A1}}]"], + [ + "BBMRI_conditionRangeDate", + "exists from [Condition] C\nwhere FHIRHelpers.ToDateTime(C.onset) between {{D1}} and {{D2}}", + ], + [ + "BBMRI_conditionRangeAge", + "exists from [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) between Ceiling({{D1}}) and Ceiling({{D2}})", + ], + ["BBMRI_age", "AgeInYears() between Ceiling({{D1}}) and Ceiling({{D2}})"], + [ + "BBMRI_observation", + "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'", + ], + [ + "BBMRI_observationSmoker", + "exists from [Observation: Code '72166-2' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'", + ], + [ + "BBMRI_observationRange", + "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value between {{D1}} and {{D2}}", + ], + [ + "BBMRI_observationBodyWeight", + "exists from [Observation: Code '29463-7' from {{A1}}] O\nwhere ((O.value as Quantity) < {{D1}} 'kg' and (O.value as Quantity) > {{D2}} 'kg')", + ], + [ + "BBMRI_observationBMI", + "exists from [Observation: Code '39156-5' from {{A1}}] O\nwhere ((O.value as Quantity) < {{D1}} 'kg/m2' and (O.value as Quantity) > {{D2}} 'kg/m2')", + ], + ["BBMRI_hasSpecimen", "exists [Specimen]"], + ["BBMRI_specimen", "exists [Specimen: Code '{{C}}' from {{A1}}]"], + ["BBMRI_retrieveSpecimenByType", "(S.type.coding.code contains '{{C}}')"], + [ + "BBMRI_retrieveSpecimenByTemperature", + "(S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/StorageTemperature').value.coding.code contains '{{C}}')", + ], + [ + "BBMRI_retrieveSpecimenBySamplingDate", + "(FHIRHelpers.ToDateTime(S.collection.collected) between {{D1}} and {{D2}})", + ], + [ + "BBMRI_retrieveSpecimenByFastingStatus", + "(S.collection.fastingStatus.coding.code contains '{{C}}')", + ], + [ + "BBMRI_samplingDate", + "exists from [Specimen] S\nwhere FHIRHelpers.ToDateTime(S.collection.collected) between {{D1}} and {{D2}}", + ], + [ + "BBMRI_fastingStatus", + "exists from [Specimen] S\nwhere S.collection.fastingStatus.coding.code contains '{{C}}'", + ], + [ + "BBMRI_storageTemperature", + "exists from [Specimen] S where (S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/StorageTemperature').value.coding contains Code '{{C}}' from {{A1}})", + ], +]); + +export const criterionMap = new Map( + [ + ["gender", { type: "gender" }], + ["histology", { type: "histology", alias: ["loinc"] }], + ["diagnosis", { type: "conditionValue", alias: ["icd10"] }], + ["bodySite", { type: "conditionBodySite", alias: ["bodySite"] }], + [ + "urn:oid:2.16.840.1.113883.6.43.1", + { type: "conditionLocalization", alias: ["lokalisation_icd_o_3"] }, + ], + ["59542-1", { type: "observation", alias: ["loinc", "gradingcs"] }], //grading + [ + "metastases_present", + { type: "observationMetastasis", alias: ["loinc", "jnucs"] }, + ], //Fernmetastasen vorhanden + [ + "localization_metastases", + { + type: "observationMetastasisBodySite", + alias: ["loinc", "fmlokalisationcs"], + }, + ], //Fernmetastasen + ["OP", { type: "procedure", alias: ["Therapieart"] }], //Operation + ["ST", { type: "procedure", alias: ["Therapieart"] }], //Strahlentherapie + ["CH", { type: "medicationStatement", alias: ["Therapieart"] }], //Chemotherapie + ["HO", { type: "medicationStatement", alias: ["Therapieart"] }], //Hormontherapie + ["IM", { type: "medicationStatement", alias: ["Therapieart"] }], //Immuntherapie + ["KM", { type: "medicationStatement", alias: ["Therapieart"] }], //Knochenmarktransplantation + ["59847-4", { type: "observation", alias: ["loinc", "morph"] }], //Morphologie + ["year_of_diagnosis", { type: "conditionRangeDate" }], + ["year_of_primary_diagnosis", { type: "primaryConditionRangeDate" }], + ["sample_kind", { type: "specimen", alias: ["specimentype"] }], + ["pat_with_samples", { type: "hasSpecimen" }], + ["age_at_diagnosis", { type: "conditionRangeAge" }], + ["age_at_primary_diagnosis", { type: "primaryConditionRangeAge" }], + ["21908-9", { type: "uiccstadium", alias: ["loinc", "uiccstadiumcs"] }], + ["21905-5", { type: "TNMc", alias: ["loinc", "TNMTCS"] }], //tnm component + ["21906-3", { type: "TNMc", alias: ["loinc", "TNMNCS"] }], //tnm component + ["21907-1", { type: "TNMc", alias: ["loinc", "TNMMCS"] }], //tnm component + ["42030-7", { type: "TNMc", alias: ["loinc", "TNMmSymbolCS"] }], //tnm component + ["59479-6", { type: "TNMc", alias: ["loinc", "TNMySymbolCS"] }], //tnm component + ["21983-2", { type: "TNMc", alias: ["loinc", "TNMrSymbolCS"] }], //tnm component + ["21899-0", { type: "TNMp", alias: ["loinc", "TNMTCS"] }], //tnm component + ["21900-6", { type: "TNMp", alias: ["loinc", "TNMNCS"] }], //tnm component + ["21901-4", { type: "TNMp", alias: ["loinc", "TNMMCS"] }], //tnm component + ["42030-7", { type: "TNMp", alias: ["loinc", "TNMmSymbolCS"] }], //tnm component + ["59479-6", { type: "TNMp", alias: ["loinc", "TNMySymbolCS"] }], //tnm component + ["21983-2", { type: "TNMp", alias: ["loinc", "TNMrSymbolCS"] }], //tnm component + + ["Organization", { type: "Organization" }], //organization + [ + "48018-6", + { + type: "observationMolecularMarkerName", + alias: ["loinc", "molecularMarker"], + }, + ], //molecular marker name + [ + "48005-3", + { + type: "observationMolecularMarkerAminoacidchange", + alias: ["loinc"], + }, + ], //molecular marker + [ + "81290-9", + { type: "observationMolecularMarkerDNAchange", alias: ["loinc"] }, + ], //molecular marker + [ + "81248-7", + { type: "observationMolecularMarkerSeqRefNCBI", alias: ["loinc"] }, + ], //molecular marker + [ + "81249-5", + { type: "observationMolecularMarkerEnsemblID", alias: ["loinc"] }, + ], //molecular marker + + [ + "local_assessment_residual_tumor", + { + type: "procedureResidualstatus", + alias: ["Therapieart", "lokalebeurteilungresidualstatuscs"], + }, + ], //lokalebeurteilungresidualstatuscs + [ + "21976-6", + { + type: "observation", + alias: ["loinc", "gesamtbeurteilungtumorstatuscs"], + }, + ], //GesamtbeurteilungTumorstatus + [ + "LA4583-6", + { + type: "observation", + alias: ["loinc", "verlauflokalertumorstatuscs"], + }, + ], //LokalerTumorstatus + [ + "LA4370-8", + { + type: "observation", + alias: ["loinc", "verlauftumorstatuslymphknotencs"], + }, + ], //TumorstatusLymphknoten + [ + "LA4226-2", + { + type: "observation", + alias: ["loinc", "verlauftumorstatusfernmetastasencs"], + }, + ], //TumorstatusFernmetastasen + ["75186-7", { type: "observation", alias: ["loinc", "vitalstatuscs"] }], //Vitalstatus + //["Organization", {type: "Organization"}], + ["Organization", { type: "department" }], + + ["BBMRI_gender", { type: "BBMRI_gender" }], + [ + "BBMRI_diagnosis", + { + type: "BBMRI_conditionSampleDiagnosis", + alias: ["BBMRI_icd10", "BBMRI_icd10gm"], + }, + ], + [ + "BBMRI_body_weight", + { type: "BBMRI_observationBodyWeight", alias: ["loinc"] }, + ], //Body weight + ["BBMRI_bmi", { type: "BBMRI_observationBMI", alias: ["loinc"] }], //BMI + [ + "BBMRI_smoking_status", + { type: "BBMRI_observationSmoker", alias: ["loinc"] }, + ], //Smoking habit + ["BBMRI_donor_age", { type: "BBMRI_age" }], + ["BBMRI_date_of_diagnosis", { type: "BBMRI_conditionRangeDate" }], + [ + "BBMRI_sample_kind", + { type: "BBMRI_specimen", alias: ["BBMRI_SampleMaterialType"] }, + ], + [ + "BBMRI_storage_temperature", + { + type: "BBMRI_storageTemperature", + alias: ["BBMRI_StorageTemperature"], + }, + ], + ["BBMRI_pat_with_samples", { type: "BBMRI_hasSpecimen" }], + ["BBMRI_diagnosis_age_donor", { type: "BBMRI_conditionRangeAge" }], + [ + "BBMRI_fasting_status", + { type: "BBMRI_fastingStatus", alias: ["loinc"] }, + ], + ["BBMRI_sampling_date", { type: "BBMRI_samplingDate" }], + ], +); diff --git a/packages/demo/src/backends/spot.ts b/packages/demo/src/backends/spot.ts new file mode 100644 index 00000000..fa1f0d38 --- /dev/null +++ b/packages/demo/src/backends/spot.ts @@ -0,0 +1,107 @@ +/** + * TODO: document this class + */ + +import type { + ResponseStore, + SiteData, + Status, + BeamResult, +} from "../../../../dist/types"; + +export class Spot { + private currentTask!: string; + + constructor( + private url: URL, + private sites: Array, + ) {} + + /** + * sends the query to beam and updates the store with the results + * @param query the query as base64 encoded string + * @param updateResponse the function to update the response store + * @param controller the abort controller to cancel the request + */ + async send( + query: string, + updateResponse: (response: ResponseStore) => void, + controller: AbortController, + ): Promise { + try { + this.currentTask = crypto.randomUUID(); + const beamTaskResponse = await fetch( + `${this.url}beam?sites=${this.sites.toString()}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: import.meta.env.PROD ? "include" : "omit", + body: JSON.stringify({ + id: this.currentTask, + sites: this.sites, + query: query, + }), + signal: controller.signal, + }, + ); + if (!beamTaskResponse.ok) { + const error = await beamTaskResponse.text(); + console.debug( + `Received ${beamTaskResponse.status} with message ${error}`, + ); + throw new Error(`Unable to create new beam task.`); + } + + console.info(`Created new Beam Task with id ${this.currentTask}`); + + const eventSource = new EventSource( + `${this.url.toString()}beam/${this.currentTask}?wait_count=${this.sites.length}`, + { + withCredentials: true, + }, + ); + + /** + * Listenes to the new_result event from beam and updates the response store + */ + eventSource.addEventListener("new_result", (message) => { + const response: BeamResult = JSON.parse(message.data); + if (response.task !== this.currentTask) return; + const site: string = response.from.split(".")[1]; + const status: Status = response.status; + const body: SiteData = + status === "succeeded" + ? JSON.parse(atob(response.body)) + : null; + + const parsedResponse: ResponseStore = new Map().set(site, { + status: status, + data: body, + }); + updateResponse(parsedResponse); + }); + + // read error events from beam + eventSource.addEventListener("error", (message) => { + console.error(`Beam returned error ${message}`); + eventSource.close(); + }); + + // event source in javascript throws an error then the event source is closed by backend + eventSource.onerror = () => { + console.info( + `Querying results from sites for task ${this.currentTask} finished.`, + ); + eventSource.close(); + }; + } catch (err) { + if (err instanceof Error && err.name === "AbortError") { + console.log(`Aborting request ${this.currentTask}`); + } else { + console.error(err); + } + } + } +} diff --git a/packages/demo/src/ccp.css b/packages/demo/src/ccp.css index ce2674da..6412fccc 100644 --- a/packages/demo/src/ccp.css +++ b/packages/demo/src/ccp.css @@ -69,6 +69,14 @@ button { } header { + background-color: var(--ghost-white); + position: sticky; + top: 0px; + z-index: 1; + padding: var(--gap-xs); +} + +.header-wrapper { background-color: var(--white); padding: var(--gap-xs); display: grid; @@ -76,6 +84,7 @@ header { grid-template-columns: 1fr 1fr 1fr; border-radius: var(--border-radius-small); border: solid 1px var(--lightest-gray); + } .logo img { @@ -98,41 +107,52 @@ header h1 { margin: 0; } -main>div, -header, +/* .grid, footer { margin: var(--gap-xs); -} +} */ .search { - display: grid; - grid-template-columns: minmax(0, 1fr) auto auto; - grid-gap: var(--gap-s); - padding: var(--gap-xxs) 0; + padding: var(--gap-xs) var(--gap-xs) var(--gap-s); background-color: var(--ghost-white); position: sticky; position: -webkit-sticky; - top: 0; + top: 86px; z-index: 1; } +.search-wrapper { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + grid-gap: var(--gap-s); + /* padding: var(--gap-xxs) 0; */ + background-color: var(--ghost-white); +} + .grid { position: relative; display: grid; grid-template-columns: 400px 1fr; grid-gap: var(--gap-m); + padding: 0 var(--gap-xs) var(--gap-xs); } -.catalogue { - padding: var(--gap-s); - border-radius: var(--border-radius-small); +.catalogue-wrapper { + background-color: var(--white); border: solid 1px var(--lightest-gray); + border-radius: var(--border-radius-small); + } + + .catalogue { + border-radius: var(--border-radius-small); + padding: var(--gap-s); background-color: var(--white); grid-row: 1/-1; overflow-y: scroll; - height: 100vh; + height: calc(100vh - 226px); position: sticky; - top: 40px; + top: 153px; + } .catalogue h2 { @@ -148,6 +168,7 @@ footer { display: grid; grid-template-columns: repeat(4, 1fr); grid-gap: var(--gap-xs); + grid-column: 2/3; } @@ -231,9 +252,6 @@ footer { background-color: var(--white); border-radius: var(--border-radius-small); border: solid 1px var(--lightest-gray); - position: sticky; - position: -webkit-sticky; - bottom: 0; } footer a { @@ -307,15 +325,31 @@ lens-search-bar-multiple::part(info-button-dialogue) { text-align: left; } -lens-search-bar-multiple::part(query-delete-button-item) { - border-color: var(--white); +lens-search-bar::part(delete-button-icon-item), +lens-search-bar-multiple::part(delete-button-icon-item) { + filter: invert(41%) sepia(43%) saturate(4610%) hue-rotate(357deg) brightness(96%) contrast(90%); + transform: translate(-1px, -1px); + width: 20px; } -lens-search-bar-multiple::part(query-delete-button-group) { - background-color: var(--white); - border-color: var(--white); +lens-search-bar::part(delete-button-icon-group), +lens-search-bar-multiple::part(delete-button-icon) { + filter: invert(41%) sepia(43%) saturate(4610%) hue-rotate(357deg) brightness(96%) contrast(90%); + transform: translate(0px, 2px); +} + +lens-search-bar::part(delete-button-icon), +lens-search-bar-multiple::part(delete-button-icon-value) { + transform: translate(-1px, -1px); + width: 20px; +} + +lens-search-bar::part(delete-button-icon):hover, +lens-search-bar-multiple::part(delete-button-icon-value):hover { + filter: invert(38%) sepia(78%) saturate(1321%) hue-rotate(352deg) brightness(92%) contrast(99%); } + lens-search-button::part(lens-search-button) { background-color: var(--light-blue); } @@ -326,7 +360,6 @@ lens-search-button::part(lens-search-button):hover { lens-search-button::part(lens-search-button):active { background-color: #003d7e; - transform: translateX(1px); } /** @@ -337,6 +370,11 @@ lens-catalogue::part(lens-catalogue) { padding-left: 8px; } +lens-catalogue::part(autocomplete-formfield-input) { + border: solid 1px var(--dark-gray); + border-radius: 0; +} + lens-catalogue::part(autocomplete-formfield-input):focus { border-color: var(--light-blue); } @@ -438,20 +476,6 @@ lens-catalogue::part(info-button-icon):hover { text-align: left; } -lens-info-button::part(info-button-dialogue):hover {background-color: #b8bfb8} - -lens-info-button::part(info-button):hover {background-color: #b8bfb8} - -lens-info-button::part(info-button):active { - background-color: #585958; - transform: translateX(1px); -} - -lens-info-button::part(info-button-dialogue):active { - background-color: #585958; - transform: translateX(1px); -} - .result-table-hint-text { padding-top: 20px; display: flex; diff --git a/packages/demo/src/fragment-development.css b/packages/demo/src/fragment-development.css index 7ed33451..148490e3 100644 --- a/packages/demo/src/fragment-development.css +++ b/packages/demo/src/fragment-development.css @@ -1,4 +1,4 @@ -@import "../../../node_modules/@samply/lens/dist/style.css"; +/* @import "../../../node_modules/@samply/lens/dist/style.css"; */ @import "../../lib/src/styles/index.css"; /** diff --git a/packages/demo/src/main.ts b/packages/demo/src/main.ts index 2fc0469d..92f73e87 100644 --- a/packages/demo/src/main.ts +++ b/packages/demo/src/main.ts @@ -18,7 +18,7 @@ import App from "./AppCCP.svelte"; // import './gba.css' const app = new App({ - target: document.getElementById("app"), + target: document.getElementById("app") as HTMLElement, }); export default app; diff --git a/packages/lib/src/classes/blaze.ts b/packages/lib/src/classes/blaze.ts index a475acd7..702536b2 100644 --- a/packages/lib/src/classes/blaze.ts +++ b/packages/lib/src/classes/blaze.ts @@ -1,18 +1,12 @@ import { buildLibrary, buildMeasure } from "../helpers/cql-measure"; -import { responseStore } from "../stores/response"; -import type { Site } from "../types/response"; -import { measureStore } from "../stores/measures"; - -let measureDefinitions; - -measureStore.subscribe((store) => { - measureDefinitions = store.map((measure) => measure.measure); -}); +import type { Site, SiteData } from "../types/response"; +import type { Measure, ResponseStore } from "../types/backend"; export class Blaze { constructor( private url: URL, private name: string, + private updateResponse: (response: ResponseStore) => void, private auth: string = "", ) {} @@ -20,13 +14,21 @@ export class Blaze { * sends the query to beam and updates the store with the results * @param cql the query as cql string * @param controller the abort controller to cancel the request + * @param measureDefinitions the measure definitions to send to blaze */ - async send(cql: string, controller?: AbortController): Promise { + async send( + cql: string, + controller: AbortController, + measureDefinitions: Measure[], + ): Promise { try { - responseStore.update((store) => { - store.set(this.name, { status: "claimed", data: null }); - return store; - }); + let response: ResponseStore = new Map().set( + this.name, + { status: "claimed", data: {} as SiteData }, + ); + + this.updateResponse(response); + const libraryResponse = await fetch( new URL(`${this.url}/Library`), { @@ -80,15 +82,15 @@ export class Blaze { ); } const blazeResponse: Site = await dataResponse.json(); - responseStore.update((store) => { - store.set(this.name, { - status: "succeeded", - data: blazeResponse, - }); - return store; + + response = new Map().set(this.name, { + status: "succeeded", + data: blazeResponse.data, }); + + this.updateResponse(response); } catch (err) { - if (err.name === "AbortError") { + if (err instanceof Error && err.name === "AbortError") { console.log(`Aborting former blaze request.`); } else { console.error(err); @@ -101,9 +103,11 @@ export class Blaze { console.debug( `${message}. Received error ${response.status} with message ${errorMessage}`, ); - responseStore.update((store) => { - store.set(this.name, { status: "permfailed", data: null }); - return store; - }); + + const failedResponse: ResponseStore = new Map().set( + this.name, + { status: "permfailed", data: null }, + ); + this.updateResponse(failedResponse); } } diff --git a/packages/lib/src/classes/spot.ts b/packages/lib/src/classes/spot.ts index 11c76bf8..e0a312d9 100644 --- a/packages/lib/src/classes/spot.ts +++ b/packages/lib/src/classes/spot.ts @@ -2,24 +2,9 @@ * TODO: document this class */ -import { responseStore } from "../stores/response"; -import type { ResponseStore } from "../types/backend"; - import type { SiteData, Status } from "../types/response"; +import type { ResponseStore } from "../types/backend"; -type BeamResult = { - body: string; - from: string; - metadata: string; - status: Status; - task: string; - to: string[]; -}; - -/** - * Implements requests to multiple targets through the middleware spot (see: https://github.com/samply/spot). - * The responses are received via Server Sent Events - */ export class Spot { private currentTask!: string; @@ -31,9 +16,14 @@ export class Spot { /** * sends the query to beam and updates the store with the results * @param query the query as base64 encoded string + * @param updateResponse the function to update the response store * @param controller the abort controller to cancel the request */ - async send(query: string, controller?: AbortController): Promise { + async send( + query: string, + updateResponse: (response: ResponseStore) => void, + controller: AbortController, + ): Promise { try { this.currentTask = crypto.randomUUID(); const beamTaskResponse = await fetch( @@ -62,6 +52,9 @@ export class Spot { console.info(`Created new Beam Task with id ${this.currentTask}`); + /** + * Listenes to the new_result event from beam and updates the response store + */ const eventSource = new EventSource( `${this.url.toString()}beam/${this.currentTask}?wait_count=${this.sites.length}`, { @@ -78,10 +71,11 @@ export class Spot { ? JSON.parse(atob(response.body)) : null; - responseStore.update((store: ResponseStore): ResponseStore => { - store.set(site, { status: status, data: body }); - return store; + const parsedResponse: ResponseStore = new Map().set(site, { + status: status, + data: body, }); + updateResponse(parsedResponse); }); // read error events from beam diff --git a/packages/lib/src/components/DataPasser.wc.svelte b/packages/lib/src/components/DataPasser.wc.svelte index 5d9676a4..c321aeb5 100644 --- a/packages/lib/src/components/DataPasser.wc.svelte +++ b/packages/lib/src/components/DataPasser.wc.svelte @@ -6,8 +6,8 @@ diff --git a/packages/lib/src/components/Options.wc.svelte b/packages/lib/src/components/Options.wc.svelte index f2bc28fb..600ac086 100644 --- a/packages/lib/src/components/Options.wc.svelte +++ b/packages/lib/src/components/Options.wc.svelte @@ -15,13 +15,58 @@ */ import { lensOptions } from "../stores/options"; import { catalogue } from "../stores/catalogue"; + import { measureStore } from "../stores/measures"; import { iconStore } from "../stores/icons"; + import type { MeasureStore } from "../types/backend"; import type { Criteria } from "../types/treeData"; + import optionsSchema from "../types/options.schema.json"; + import catalogueSchema from "../types/catalogue.schema.json"; + import { parser } from "@exodus/schemasafe"; import type { LensOptions } from "../types/options"; + import { uiSiteMappingsStore } from "../stores/mappings"; export let options: LensOptions = {}; export let catalogueData: Criteria[] = []; + export let measures: MeasureStore = {} as MeasureStore; + /** + * Validate the options against the schema before passing them to the store + */ + $: { + const parse = parser(optionsSchema, { + includeErrors: true, + allErrors: true, + }); + const validJSON = parse(JSON.stringify(options)); + if (validJSON.valid === true) { + $lensOptions = options; + } else if (typeof options === "object") { + console.error( + "Lens-Options are not conform with the JSON schema", + validJSON.errors, + ); + } + } + + $: { + const parse = parser(catalogueSchema, { + includeErrors: true, + allErrors: true, + }); + const validJSON = parse(JSON.stringify(catalogueData)); + if (validJSON.valid === true) { + $catalogue = catalogueData; + } else if (typeof catalogueData === "object") { + console.error( + "Catalogue is not conform with the JSON schema", + validJSON.errors, + ); + } + } + /** + * updates the icon store with the options passed in + * @param options the Lens options + */ const updateIconStore = (options: LensOptions): void => { iconStore.update((store) => { if (typeof options === "object" && "iconOptions" in options) { @@ -35,6 +80,12 @@ ) { store.set("infoUrl", options.iconOptions.infoUrl); } + if ( + "deleteUrl" in options.iconOptions && + typeof options.iconOptions["deleteUrl"] === "string" + ) { + store.set("deleteUrl", options.iconOptions.deleteUrl); + } if ( "selectAll" in options.iconOptions && typeof options.iconOptions["selectAll"] === "object" && @@ -58,7 +109,22 @@ }); }; + /** + * watches the backendConfig for changes to populate the uiSiteMappingsStore with a map + * web components' props are json, meaning that Maps are not supported + * therefore it's a 2d array of strings which is converted to a map + */ + $: uiSiteMappingsStore.update((mappings) => { + if (!options?.siteMappings) return mappings; + Object.entries(options?.siteMappings)?.forEach((site) => { + mappings.set(site[0], site[1]); + }); + + return mappings; + }); + $: $lensOptions = options; $: updateIconStore(options); $: $catalogue = catalogueData; + $: $measureStore = measures; diff --git a/packages/lib/src/components/buttons/InfoButtonComponent.wc.svelte b/packages/lib/src/components/buttons/InfoButtonComponent.wc.svelte index 446f1d43..9ce8f23a 100644 --- a/packages/lib/src/components/buttons/InfoButtonComponent.wc.svelte +++ b/packages/lib/src/components/buttons/InfoButtonComponent.wc.svelte @@ -37,7 +37,8 @@ tooltipOpen = false; }; - const displayQueryInfo = (queryItem?: QueryItem): void => { + const displayQueryInfo = (e: MouseEvent, queryItem?: QueryItem): void => { + const target: HTMLElement = e.target as HTMLElement; if (showQuery) { if (onlyChildInfo && queryItem !== undefined) { let childMessage = buildHumanReadableRecursively( @@ -53,13 +54,19 @@ : [noQueryMessage]; } } - tooltipOpen = !tooltipOpen; + if ( + target.getAttribute("part") !== "info-button-dialogue" && + target.getAttribute("part") !== "info-button-dialogue-message" + ) { + tooltipOpen = !tooltipOpen; + } }; + {#if deleteUrl} + delete icon + {:else} + ✕ + {/if} + diff --git a/packages/lib/src/components/catalogue/DataTreeElement.svelte b/packages/lib/src/components/catalogue/DataTreeElement.svelte index a70438dd..71348ee1 100644 --- a/packages/lib/src/components/catalogue/DataTreeElement.svelte +++ b/packages/lib/src/components/catalogue/DataTreeElement.svelte @@ -224,6 +224,7 @@ {:else if "fieldType" in element && element.fieldType === "number"} {#each numberInput.values as numberInputValues (numberInputValues.queryBindId)} { + if (from === null) return; + + let min: number | null = + "min" in element && typeof element.min === "number" + ? element.min + : null; + let max: number | null = + "max" in element && typeof element.max === "number" + ? element.max + : null; + + if (min !== null && from <= min) { + from = min; + } else if (max !== null && from >= max) { + from = max; + } + }; + + /** + * handles the "to" input field + * when the catalogue element has min or max values, they are used on focus out if the input is out of bounds + */ + const handleInputTo = (): void => { + if (to === null) return; + + let min: number | null = + "min" in element && typeof element.min === "number" + ? element.min + : null; + let max: number | null = + "max" in element && typeof element.max === "number" + ? element.max + : null; + + if (min !== null && to <= min) { + to = min; + } else if (max !== null && to >= max) { + to = max; + } + }; + /** * build the proper name for the query value * @returns the "from", "≥ from", "≤ to", "from - to" or "invalid" @@ -95,7 +140,7 @@ {to && from > to ? ' formfield-error' : ''}" type="number" bind:value={from} - min="0" + on:focusout={handleInputFrom} /> @@ -108,7 +153,7 @@ {to && from > to ? ' formfield-error' : ''}" type="number" bind:value={to} - min="0" + on:focusout={handleInputTo} /> diff --git a/packages/lib/src/components/results/ChartComponent.wc.svelte b/packages/lib/src/components/results/ChartComponent.wc.svelte index 4d7368bb..e7866b50 100644 --- a/packages/lib/src/components/results/ChartComponent.wc.svelte +++ b/packages/lib/src/components/results/ChartComponent.wc.svelte @@ -205,7 +205,7 @@ if (site === undefined || site.data === null) return 0; - let data = site.data.group.find( + let data = site?.data?.group?.find( (groupItem) => groupItem.code.text === catalogueGroupCode, ); return data?.population[0]?.count || 0; diff --git a/packages/lib/src/components/results/ResultTableComponent.wc.svelte b/packages/lib/src/components/results/ResultTableComponent.wc.svelte index e51f5f24..c32a4547 100644 --- a/packages/lib/src/components/results/ResultTableComponent.wc.svelte +++ b/packages/lib/src/components/results/ResultTableComponent.wc.svelte @@ -24,10 +24,7 @@ let claimedText: string; $: claimedText = - (($lensOptions?.tableOptions && - $lensOptions.tableOptions?.claimedText && - $lensOptions.tableOptions.claimedText) as string) || - "Processing..."; + ($lensOptions?.tableOptions?.claimedText as string) || "Processing..."; /** * data-types for the table diff --git a/packages/lib/src/components/search-bar/SearchBarComponent.wc.svelte b/packages/lib/src/components/search-bar/SearchBarComponent.wc.svelte index 5f148d2e..1f231cf6 100644 --- a/packages/lib/src/components/search-bar/SearchBarComponent.wc.svelte +++ b/packages/lib/src/components/search-bar/SearchBarComponent.wc.svelte @@ -326,8 +326,8 @@ diff --git a/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts b/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts index 819f5922..0e7182f4 100644 --- a/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts +++ b/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts @@ -13,17 +13,7 @@ import { criterionMap, } from "./cqlquery-mappings"; import { getCriteria } from "../stores/catalogue"; -import type { Measure } from "../types/backend"; -import { measureStore } from "../stores/measures"; - -/** - * Get all cql from the project specific measures from the store - */ -let measuresCql: string[] = []; - -measureStore.subscribe((measures: Measure[]) => { - measuresCql = measures.map((measure) => measure.cql); -}); +import type { MeasureItem } from "../types/backend"; let codesystems: string[] = []; let criteria: string[]; @@ -32,6 +22,7 @@ export const translateAstToCql = ( query: AstTopLayer, returnOnlySingeltons: boolean = true, backendMeasures: string, + measures: MeasureItem[], ): string => { criteria = getCriteria("diagnosis"); @@ -66,7 +57,7 @@ export const translateAstToCql = ( cqlHeader + getCodesystems() + "context Patient\n" + - measuresCql.join("") + + measures.map((measureItem: MeasureItem) => measureItem.cql).join("") + singletons ); }; diff --git a/packages/lib/src/helpers/cql-measure.ts b/packages/lib/src/helpers/cql-measure.ts index d93b3c10..21f3b59a 100644 --- a/packages/lib/src/helpers/cql-measure.ts +++ b/packages/lib/src/helpers/cql-measure.ts @@ -1,4 +1,5 @@ import { v4 as uuidv4 } from "uuid"; +import type { Measure } from "../types/backend"; type BuildLibraryReturn = { resourceType: string; @@ -57,12 +58,12 @@ type BuildMeasureReturn = { code: string; }[]; }; - group: object[]; + group: Measure[]; }; export const buildMeasure = ( libraryUrl: string, - measures: object[], + measures: Measure[], ): BuildMeasureReturn => { const measureId = uuidv4(); return { diff --git a/packages/lib/src/interfaces/DataPasser.d.ts b/packages/lib/src/interfaces/DataPasser.d.ts deleted file mode 100644 index 43399717..00000000 --- a/packages/lib/src/interfaces/DataPasser.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface LensDataPasser { - getQueryAPI(): QueryItem[][]; - addStratifierToQueryAPI(params: addStratifierToQueryAPIParams): void; - removeItemFromQuyeryAPI(params: removeItemFromQuyeryAPIParams): void; - removeValueFromQueryAPI(params: removeValueFromQueryAPIParams): void; - getResponseAPI(): ResponseStore; - getAstAPI(): AstTopLayer; -} - -export default LensDataPasser; diff --git a/packages/lib/src/stores/catalogue.ts b/packages/lib/src/stores/catalogue.ts index 927351e5..379d8d07 100644 --- a/packages/lib/src/stores/catalogue.ts +++ b/packages/lib/src/stores/catalogue.ts @@ -22,7 +22,7 @@ export const activeNumberInputs = writable([]); * @returns array of strings containing the bottom level items' keys */ export const getCriteria = (category: string): string[] => { - let bottomLevelItems; + let bottomLevelItems = [] as string[]; catalogue.subscribe((catalogue) => { bottomLevelItems = getBottomLevelItems(catalogue, category); diff --git a/packages/lib/src/stores/measures.ts b/packages/lib/src/stores/measures.ts index b078c7cb..0684b461 100644 --- a/packages/lib/src/stores/measures.ts +++ b/packages/lib/src/stores/measures.ts @@ -1,9 +1,9 @@ -import { writable } from 'svelte/store'; -import type { Measure } from '../types/backend'; +import { writable } from "svelte/store"; +import type { MeasureStore } from "../types/backend"; /** - * Store to hold the measures + * Store to hold the measures * populated by the search button */ -export const measureStore = writable([]); \ No newline at end of file +export const measureStore = writable(); diff --git a/packages/lib/src/stores/query.ts b/packages/lib/src/stores/query.ts index 90d9ee6d..e67b7584 100644 --- a/packages/lib/src/stores/query.ts +++ b/packages/lib/src/stores/query.ts @@ -216,12 +216,12 @@ export const addStratifier = ({ catalogue, catalogueGroupCode, queryGroupIndex = 0, - groupRange, -}): void => { + groupRange = 1, +}: AddStratifierParams): void => { let queryItem: QueryItem; catalogue.forEach((parentCategory: Category) => { if ("childCategories" in parentCategory) { - parentCategory.childCategories.forEach( + parentCategory.childCategories?.forEach( (childCategorie: Category) => { if ( childCategorie.key === catalogueGroupCode && diff --git a/packages/lib/src/stores/response.ts b/packages/lib/src/stores/response.ts index 018e5770..1fb13003 100644 --- a/packages/lib/src/stores/response.ts +++ b/packages/lib/src/stores/response.ts @@ -4,6 +4,35 @@ import type { ResponseStore } from "../types/backend"; export const responseStore = writable(new Map()); +/** + * updates the response store with a given response + * @param response - the response to update the store with + */ +export const updateResponseStore = (response: ResponseStore): void => { + let store: ResponseStore; + responseStore.subscribe((s: ResponseStore) => (store = s)); + + const changes = new Map(); + + response.forEach((value, key) => { + if (store.get(key)?.status === response.get(key)?.status) { + return; + } + changes.set(key, value); + }); + + if (changes.size === 0) { + return; + } + + responseStore.update((store: ResponseStore): ResponseStore => { + changes.forEach((value, key) => { + store.set(key, value); + }); + return store; + }); +}; + /** * @param store - the response store * @param code - the code to search for @@ -37,9 +66,9 @@ export const getSitePopulationForCode = ( code: string, ): number => { let population: number = 0; - if (!site) return population; + if (!site || !site.group) return population; - site.group.forEach((group) => { + site?.group?.forEach((group) => { if (group.code.text === code) { population += group.population[0].count; } @@ -105,7 +134,7 @@ export const getSitePopulationForStratumCode = ( stratumCode: string, stratifier: string, ): number => { - if (!site) return 0; + if (!site || !site.group) return 0; let population: number = 0; @@ -164,7 +193,7 @@ export const getSiteStratifierCodesForGroupCode = ( site: SiteData, code: string, ): string[] => { - if (!site) return [""]; + if (!site || !site.group) return [""]; const codes: string[] = []; site.group.forEach((groupItem) => { diff --git a/packages/lib/src/styles/catalogue.css b/packages/lib/src/styles/catalogue.css index 9d5fcb2d..ca9dfa81 100644 --- a/packages/lib/src/styles/catalogue.css +++ b/packages/lib/src/styles/catalogue.css @@ -82,6 +82,7 @@ lens-catalogue::part(info-button-icon) { } lens-catalogue::part(info-button-dialogue) { + cursor: auto; position: absolute; border: none; background-color: var(--white); diff --git a/packages/lib/src/styles/info-button.css b/packages/lib/src/styles/info-button.css index 3ef44c1c..1087cd10 100644 --- a/packages/lib/src/styles/info-button.css +++ b/packages/lib/src/styles/info-button.css @@ -13,6 +13,7 @@ lens-info-button::part(info-button-title) { } lens-info-button::part(info-button-dialogue) { + cursor: auto; position: absolute; border: none; background-color: var(--white); diff --git a/packages/lib/src/styles/result-chart.css b/packages/lib/src/styles/result-chart.css index 72f2090d..dab4fe5e 100644 --- a/packages/lib/src/styles/result-chart.css +++ b/packages/lib/src/styles/result-chart.css @@ -35,6 +35,7 @@ lens-chart::part(info-button-icon) { height: 16px; } lens-chart::part(info-button-dialogue) { + cursor: auto; position: absolute; border: none; background-color: var(--white); diff --git a/packages/lib/src/styles/result-table.css b/packages/lib/src/styles/result-table.css index b824377b..b2ece29a 100644 --- a/packages/lib/src/styles/result-table.css +++ b/packages/lib/src/styles/result-table.css @@ -86,6 +86,7 @@ lens-result-table::part(info-button-icon) { lens-result-table::part(info-button-dialogue) { + cursor: auto; position: absolute; border: none; background-color: var(--white); diff --git a/packages/lib/src/styles/results-overview.css b/packages/lib/src/styles/results-overview.css index 3a683022..ed0cc317 100644 --- a/packages/lib/src/styles/results-overview.css +++ b/packages/lib/src/styles/results-overview.css @@ -35,6 +35,7 @@ lens-result-summary::part(info-button-title) { } lens-result-summary::part(info-button-dialogue) { + cursor: auto; position: absolute; border: none; background-color: var(--white); diff --git a/packages/lib/src/styles/searchbars.css b/packages/lib/src/styles/searchbars.css index 4d45a723..36c7392e 100644 --- a/packages/lib/src/styles/searchbars.css +++ b/packages/lib/src/styles/searchbars.css @@ -167,6 +167,51 @@ lens-search-bar-multiple::part(lens-searchbar-add-button) { margin-left: var(--gap-xs); } +/** +* Lens Search Bar Info Button +*/ + +lens-search-bar-multiple::part(info-button), +lens-search-bar::part(info-button){ + background-color: var(--blue); + border-color: var(--blue); + position: relative; + padding: 0; + border: 0; + top: +2px; +} + +lens-search-bar::part(info-button-icon), +lens-search-bar-multiple::part(info-button-icon) { + height: calc(var(--font-size-s) + 8px); + width: calc(var(--font-size-s) + 8px); + filter: brightness(0) invert(1); + box-sizing: content-box; + border-radius: 50%; +} + +lens-search-bar::part(info-button-icon):hover, +lens-search-bar-multiple::part(info-button-icon):hover { + cursor: pointer; +} + +lens-search-bar::part(info-button-dialogue), +lens-search-bar-multiple::part(info-button-dialogue) { + position: absolute; + border: none; + background-color: var(--white); + width: max-content; + max-width: 200px; + z-index: 100; + padding: var(--gap-s); + top: calc(var(--gap-m) + 4px); + right: -20px; + border: solid 1px var(--light-blue); + border-radius: var(--border-radius-small); + text-align: left; +} + + /** * delete buttons in searchbar and chips @@ -187,6 +232,7 @@ lens-search-bar-multiple::part(query-delete-button) { lens-search-bar::part(query-delete-button):hover, lens-search-bar-multiple::part(query-delete-button):hover { border: solid 1px var(--orange); + color: var(--orange) } lens-search-bar::part(query-delete-button-value), @@ -194,7 +240,7 @@ lens-search-bar-multiple::part(query-delete-button-value) { font-size: var(--font-size-xxs); color: var(--white); margin: 0 var(--gap-xs) 0 var(--gap-xxs); - background-color: var(--light-blue); + background-color: var(--blue); border: var(--white) 1px solid; } @@ -204,7 +250,7 @@ lens-search-bar-multiple::part(query-delete-button-item) { position: absolute; top: -6px; right: -10px; - border: solid 2px var(--white); + border: solid 1px var(--white); } lens-search-bar::part(query-delete-button-group), @@ -226,3 +272,8 @@ lens-search-bar-multiple::part(lens-search-button) { margin-left: auto; margin-right: 45px; } + +lens-search-bar::part(info-button-dialogue), +lens-search-bar-multiple::part(info-button-dialogue) { + left: 0px; +} diff --git a/packages/lib/src/types/backend.ts b/packages/lib/src/types/backend.ts index 62974bc5..4c739b07 100644 --- a/packages/lib/src/types/backend.ts +++ b/packages/lib/src/types/backend.ts @@ -1,16 +1,76 @@ +import type { AstTopLayer } from "./ast"; +import type { BlazeOption } from "./blaze"; import type { Site } from "./response"; +import type { SpotOption } from "./spot"; -export type Measure = { +export type MeasureGroup = { + name: string; + measures: MeasureItem[]; +}; + +export type MeasureItem = { key: string; - measure: object; + measure: Measure; cql: string; }; +export type Measure = { + code: { + text: string; + }; + extension: [ + { + url: string; + valueCode: string; + }, + ]; + population: [ + { + code: { + coding: [ + { + system: string; + code: string; + }, + ]; + }; + criteria: { + language: string; + expression: string; + }; + }, + ]; + stratifier: [ + { + code: { + text: string; + }; + criteria: { + language: string; + expression: string; + }; + }, + ]; +}; -export type BackendConfig = { - url: string; - backends: string[]; - uiSiteMap: string[][]; - catalogueKeyToResponseKeyMap: string[][]; +export type MeasureOption = { + name: string; + measures: MeasureItem[]; }; +export type MeasureStore = MeasureOption[]; + export type ResponseStore = Map; + +export type BackendOptions = { + spots?: SpotOption[]; + blazes?: BlazeOption[]; + customAstBackends?: string[]; +}; + +export interface QueryEvent extends Event { + detail: { + ast: AstTopLayer; + updateResponse: (response: Map) => void; + abortController: AbortController; + }; +} diff --git a/packages/lib/src/types/blaze.ts b/packages/lib/src/types/blaze.ts new file mode 100644 index 00000000..068dd119 --- /dev/null +++ b/packages/lib/src/types/blaze.ts @@ -0,0 +1,8 @@ +export type BlazeOption = { + name: string; + url: string; + auth?: string; + backendMeasures: string; + uiSiteMap: string[][]; + catalogueKeyToResponseKeyMap: string[][]; +}; diff --git a/packages/lib/src/types/catalogue.schema.json b/packages/lib/src/types/catalogue.schema.json new file mode 100644 index 00000000..a5c94784 --- /dev/null +++ b/packages/lib/src/types/catalogue.schema.json @@ -0,0 +1,136 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Search Parameter Catalogue", + "description": "A catalogue of search parameters", + "type": "array", + "items": { + "$ref": "#/$defs/categoryItem" + }, + "$defs": { + "childCategories": { + "type": "array", + "items": { + "$ref": "#/$defs/categoryItem" + } + }, + "categoryItem": { + "type": "object", + "properties": { + "key": { + "type": "string", + "pattern": "^.+$" + }, + "name": { + "type": "string", + "pattern": "^.+$" + }, + "subCategoryName": { + "type": "string", + "pattern": "^.+$" + }, + "infoButtonText": { + "type": "array", + "description": "The text to display in the info button", + "items": { + "type": "string", + "pattern": "^.*$" + } + }, + "system": { + "type": "string", + "pattern": "^.*$" + }, + "fieldType": { + "enum": [ + "single-select", + "number", + "autocomplete" + ] + }, + "type": { + "enum": [ + "EQUALS", + "BETWEEN" + ] + }, + "min": { + "type": "number" + }, + "max": { + "type": "number" + }, + "childCategories": { + "$ref": "#/$defs/childCategories" + }, + "criteria": { + "$ref": "#/$defs/criteria" + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [ + "key", + "name" + ] + }, + "criteria": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "pattern": "^.+$" + }, + "name": { + "type": "string", + "pattern": "^.+$" + }, + "description": { + "type": "string", + "pattern": "^.*$" + }, + "infoButtonText": { + "type": "array", + "description": "The text to display in the info button", + "items": { + "type": "string", + "pattern": "^.+$" + } + }, + "aggregatedValue": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "pattern": "^.+$" + }, + "name": { + "type": "string", + "pattern": "^.+$" + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [ + "value", + "name" + ] + } + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [ + "key", + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/lib/src/types/dataPasser.ts b/packages/lib/src/types/dataPasser.ts new file mode 100644 index 00000000..72c25ca7 --- /dev/null +++ b/packages/lib/src/types/dataPasser.ts @@ -0,0 +1,32 @@ +import type { AstTopLayer } from "./ast"; +import type { ResponseStore } from "./backend"; +import type { QueryItem, QueryValue } from "./queryData"; + +export type AddStratifierToQueryAPIParams = { + label: string; + catalogueGroupCode: string; + groupRange?: number; + queryGroupIndex?: number; +}; + +export type RemoveItemFromQuyeryAPIParams = { + queryObject: QueryItem; + queryGroupIndex?: number; +}; + +export type RemoveValueFromQueryAPIParams = { + queryItem: QueryItem; + value: QueryValue; + queryGroupIndex?: number; +}; + +export interface LensDataPasser extends HTMLElement { + getQueryAPI(): QueryItem[][]; + getResponseAPI(): ResponseStore; + getAstAPI(): AstTopLayer; + getCriteriaAPI(category: string): string[]; + addStratifierToQueryAPI(params: AddStratifierToQueryAPIParams): void; + removeItemFromQuyeryAPI(params: RemoveItemFromQuyeryAPIParams): void; + removeValueFromQueryAPI(params: RemoveValueFromQueryAPIParams): void; + updateResponseAPI(params: ResponseStore): void; +} diff --git a/packages/lib/src/types/options.schema.json b/packages/lib/src/types/options.schema.json new file mode 100644 index 00000000..42bdfc2f --- /dev/null +++ b/packages/lib/src/types/options.schema.json @@ -0,0 +1,353 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Lens Options", + "description": "The options for the lens", + "type": "object", + "properties": { + "iconOptions": { + "type": "object", + "properties": { + "infoUrl": { + "type": "string", + "pattern": "^.+$", + "description": "The icon to use for the info button" + }, + "deleteUrl": { + "type": "string", + "pattern": "^.+$", + "description": "The icon to use for the info button" + }, + "selectAll": { + "type": "object", + "properties": { + "text": { + "type": "string", + "pattern": "^.+$", + "description": "The text to display for the select all button" + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + }, + "siteMappings":{ + "type": "object", + "patternProperties": { + "^.+$": { + "type": "string", + "pattern": "^.+$" + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + }, + "chartOptions": { + "type": "object", + "patternProperties": { + "^.+$": { + "type": "object", + "properties": { + "legendMapping": { + "type": "object", + "patternProperties": { + "^.+$": { + "type": "string", + "pattern": "^.+$" + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + }, + "hintText": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+$", + "description": "The hint text to display as overlay of the info button" + } + }, + "aggregations": { + "type": "array", + "description": "add strings of other data keys to include in the chart", + "items": { + "type": "string", + "pattern": "^.+$" + } + }, + "accumulatedValues": { + "type": "array", + "description": "aggregate values of other data keys to include in the chart", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^.+$", + "description": "The name to be displayed in the chart" + }, + "values": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+$", + "description": "The data key to be aggregated" + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + } + }, + "tooltips": { + "type": "object", + "patternProperties": { + "^.+$": { + "type": "string", + "pattern": "^.+$", + "description": "The tooltip to display while hovering over the chart data" + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + }, + "tableOptions": { + "type": "object", + "properties": { + "headerData": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "the title of the column", + "pattern": "^.+$" + }, + "dataKey": { + "type": "string", + "description": "a single key to display in the table", + "pattern": "^.+$" + }, + "aggregatedDataKeys": { + "type": "array", + "description": "an array of keys to aggregate and display in the table as single value", + "items": { + "type": "object", + "properties": { + "groupCode": { + "type": "string", + "pattern": "^.+$" + }, + "stratifierCode": { + "type": "string", + "pattern": "^.+$" + }, + "stratumCode": { + "type": "string", + "pattern": "^.+$" + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [ + "title" + ] + } + }, + "claimedText": { + "type": "string", + "pattern": "^.+$", + "description": "The text to be displayed when query is being executed" + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": ["headerData"] + }, + "resultSummaryOptions": { + "type": "object", + "properties": { + "title": { + "type": "string", + "pattern": "^.+$" + }, + "infoButtonText": { + "type": "string", + "pattern": "^.+$" + }, + "dataTypes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "pattern": "^.+$" + }, + "dataKey": { + "type": "string", + "pattern": "^.+$" + }, + "aggregatedDataKeys": { + "type": "array", + "description": "an array of keys to aggregate and display in the result summary as single value", + "items": { + "type": "object", + "properties": { + "groupCode": { + "type": "string", + "pattern": "^.+$" + }, + "stratifierCode": { + "type": "string", + "pattern": "^.+$" + }, + "stratumCode": { + "type": "string", + "pattern": "^.+$" + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + }, + "backends": { + "type": "object", + "properties": { + "customBackends": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+$", + "description": "The URL of the custom backend" + } + }, + "spots": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^.+$", + "description": "The name of the spot" + }, + "backendMeasures": { + "type": "string", + "pattern": "^.+$", + "description": "The measures of the spot" + }, + "url": { + "type": "string", + "pattern": "^.+$", + "description": "The URL of the spot" + }, + "sites": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+$", + "description": "The sites of the spot" + } + }, + "catalogueKeyToResponseKeyMap": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+$", + "description": "The mapping of the catalogue key to the response key" + } + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": ["name", "backendMeasures", "url", "sites"] + } + }, + "blazes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^.+$", + "description": "The name of the blaze" + }, + "backendMeasures": { + "type": "string", + "pattern": "^.+$", + "description": "The measures of the blaze" + }, + "url": { + "type": "string", + "pattern": "^.+$", + "description": "The URL of the blaze" + }, + "catalogueKeyToResponseKeyMap": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+$", + "description": "The mapping of the catalogue key to the response key" + } + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": ["name", "backendMeasures", "url"] + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] +} \ No newline at end of file diff --git a/packages/lib/src/types/spot.ts b/packages/lib/src/types/spot.ts new file mode 100644 index 00000000..59e08f2f --- /dev/null +++ b/packages/lib/src/types/spot.ts @@ -0,0 +1,19 @@ +import type { Status } from "./response"; + +export type BeamResult = { + body: string; + from: string; + metadata: string; + status: Status; + task: string; + to: string[]; +}; + +export type SpotOption = { + name: string; + backendMeasures: string; + url: string; + sites: string[]; + uiSiteMap: string[][]; + catalogueKeyToResponseKeyMap: string[][]; +}; diff --git a/packages/lib/src/types/treeData.ts b/packages/lib/src/types/treeData.ts index 41553ef4..0d8f1526 100644 --- a/packages/lib/src/types/treeData.ts +++ b/packages/lib/src/types/treeData.ts @@ -1,32 +1,41 @@ -export type TreeNode = Category[] |Category | Criteria | AggregatedValue[] | AggregatedValue +export type TreeNode = + | Category[] + | Category + | Criteria + | AggregatedValue[] + | AggregatedValue; -export type Category = { - key: string; - name: string; - childCategories?: Category[]; - infoButtonText?: string[]; - subCategoryName?: string; -} | { - key: string; - name: string; - system?: string; - fieldType: 'single-select' | 'autocomplete' | 'number'; - type: 'EQUALS' | 'BETWEEN'; - criteria: | Criteria[]; - description?: string; - infoButtonText?: string[]; -} +export type Category = + | { + key: string; + name: string; + childCategories?: Category[]; + infoButtonText?: string[]; + subCategoryName?: string; + } + | { + key: string; + name: string; + system?: string; + fieldType: "single-select" | "autocomplete" | "number"; + type: "EQUALS" | "BETWEEN"; + min?: number; + max?: number; + criteria: Criteria[]; + description?: string; + infoButtonText?: string[]; + }; export type Criteria = { key: string; name: string; description?: string; - aggregatedValue?: AggregatedValue[][] -} + aggregatedValue?: AggregatedValue[][]; +}; export type AggregatedValue = { value: string; name: string; type: string; system?: string; -} +};