diff --git a/.env b/.env index 6f1a09a2..c27226af 100644 --- a/.env +++ b/.env @@ -5,4 +5,7 @@ NEXT_PUBLIC_CCDA_VALIDATOR_CURES_V1_SENDER_URL=https://api.github.com/repos/onc- NEXT_PUBLIC_CCDA_VALIDATOR_CURES_V1_RECEIVER_URL=https://api.github.com/repos/onc-healthit/2015-edition-cures-update-data/contents/Cures Update Receiver SUT Test Data NEXT_PUBLIC_CCDA_VALIDATOR_CURES_DOWNLOAD_URL=https://codeload.github.com/onc-healthit/2015-edition-cures-update-data/zip/master NEXT_PUBLIC_RELEASE_VERSION_URL=https://raw.githubusercontent.com/onc-healthit/site-content/master/site-ui-4/version.md -NEXT_PUBLIC_RELEASE_DATE_URL=https://raw.githubusercontent.com/onc-healthit/site-content/master/site-ui-4/release-date.md \ No newline at end of file +NEXT_PUBLIC_RELEASE_DATE_URL=https://raw.githubusercontent.com/onc-healthit/site-content/master/site-ui-4/release-date.md + +# TODO: Allow for this to be dynamic per environment file and delete from here. See: https://phase.dev/blog/nextjs-public-runtime-variables/ +NEXT_PUBLIC_SCORECARD_SAVESCORECARDSERVICE_API=https://ccda.healthit.gov/scorecard/savescorecardservice diff --git a/.github/workflows/create-deploy-build.yml b/.github/workflows/create-deploy-build.yml index 5c6fca68..2f6ff906 100644 --- a/.github/workflows/create-deploy-build.yml +++ b/.github/workflows/create-deploy-build.yml @@ -43,16 +43,19 @@ jobs: run: npm run build - name: Create Build Artifact run: | + ls -la mkdir ./build cp ./.next -pR ./build/.next cp ./next.config.js ./build/next.config.js cp ./public -pR ./build/public cp ./package.json ./build/package.json cp ./package-lock.json ./build/package-lock.json + ls -la ./build - name: Upload build folder uses: actions/upload-artifact@v4 with: name: BuildArtifact + include-hidden-files: true path: ./build retention-days: 7 deploy: diff --git a/.prettierrc b/.prettierrc index 0ba8bf09..d0afb198 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,7 +6,7 @@ "experimentalTernaries": false, "singleQuote": true, "jsxSingleQuote": false, - "quoteProps": "consistent", + "quoteProps": "as-needed", "trailingComma": "es5", "singleAttributePerLine": false, "htmlWhitespaceSensitivity": "css", diff --git a/package-lock.json b/package-lock.json index f52e85d0..29a67fa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,10 @@ "@mui/lab": "^5.0.0-alpha.173", "@mui/material": "^5.16.0", "@mui/material-nextjs": "^5.15.5", + "@mui/x-charts": "^7.17.0", "@mui/x-data-grid": "^7.12.1", - "axios": "^1.6.8", - "dompurify": "^3.1.2", + "axios": "^1.7.7", + "dompurify": "^3.1.6", "lodash": "^4.17.21", "marked": "^12.0.2", "next": "14.2.3", @@ -246,9 +247,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -916,6 +917,81 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/@mui/x-charts": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.17.0.tgz", + "integrity": "sha512-xDH/lOnb57+VBIA7q+1KlC0Ht1O46d/N2MEl1tUq1JYIXhA2Owi5cp+bcaof8Rvw5ApCmkoBxyUIjqT0guNIwA==", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/utils": "^5.16.6", + "@mui/x-charts-vendor": "7.16.0", + "@mui/x-internals": "7.17.0", + "@react-spring/rafz": "^9.7.4", + "@react-spring/web": "^9.7.4", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts-vendor": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-7.16.0.tgz", + "integrity": "sha512-MyMCCl7eAM53rLbjqP4zbMy5hYtdeqCjAYCH2jpvBKdgugm2eaPLKOPM8bUVfen0wHA8BXleQrIrNceytFPyZA==", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@types/d3-color": "^3.1.3", + "@types/d3-delaunay": "^6.0.4", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-scale": "^4.0.8", + "@types/d3-shape": "^3.1.6", + "@types/d3-time": "^3.0.3", + "d3-color": "^3.1.0", + "d3-delaunay": "^6.0.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "delaunator": "^5.0.1", + "robust-predicates": "^3.0.2" + } + }, + "node_modules/@mui/x-charts/node_modules/@mui/x-internals": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.17.0.tgz", + "integrity": "sha512-FLlAGSJl/vsuaA/8hPGazXFppyzIzxApJJDZMoTS0geUmHd0hyooISV2ltllLmrZ/DGtHhI08m8GGnHL6/vVeg==", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/utils": "^5.16.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@mui/x-data-grid": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.12.1.tgz", @@ -1172,6 +1248,72 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-spring/animated": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.4.tgz", + "integrity": "sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==", + "dependencies": { + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.4.tgz", + "integrity": "sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.4.tgz", + "integrity": "sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA==" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.4.tgz", + "integrity": "sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==", + "dependencies": { + "@react-spring/rafz": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.4.tgz", + "integrity": "sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz", + "integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/core": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz", @@ -1202,6 +1344,50 @@ "cypress": "*" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1971,9 +2157,9 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2554,6 +2740,111 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2656,6 +2947,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2718,9 +3017,9 @@ } }, "node_modules/dompurify": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.5.tgz", - "integrity": "sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA==" + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" }, "node_modules/ecc-jsbn": { "version": "0.1.2", @@ -4289,6 +4588,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -7333,6 +7640,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index 14d17d92..f354e741 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,10 @@ "@mui/lab": "^5.0.0-alpha.173", "@mui/material": "^5.16.0", "@mui/material-nextjs": "^5.15.5", + "@mui/x-charts": "^7.17.0", "@mui/x-data-grid": "^7.12.1", - "axios": "^1.6.8", - "dompurify": "^3.1.2", + "axios": "^1.7.7", + "dompurify": "^3.1.6", "lodash": "^4.17.21", "marked": "^12.0.2", "next": "14.2.3", diff --git a/src/app/(resources)/docs-and-vids/page.tsx b/src/app/(resources)/docs/page.tsx similarity index 100% rename from src/app/(resources)/docs-and-vids/page.tsx rename to src/app/(resources)/docs/page.tsx diff --git a/src/app/(resources)/faqs/page.tsx b/src/app/(resources)/faqs/page.tsx deleted file mode 100644 index b3d84e34..00000000 --- a/src/app/(resources)/faqs/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import FAQHomeComponent from '@/components/resources/FAQHome' -import CommunicationFab from '@/components/shared/CommunicationFab' -const FAQHome = () => { - return ( - <> - - - - ) -} - -export default FAQHome diff --git a/src/app/api/direct/register/[id]/route.ts b/src/app/api/direct/register/[id]/route.ts new file mode 100644 index 00000000..2bcf3aa2 --- /dev/null +++ b/src/app/api/direct/register/[id]/route.ts @@ -0,0 +1,150 @@ +import { NextRequest, NextResponse } from 'next/server' +import _ from 'lodash' +import { authOptions } from '@/lib/auth' +import { getServerSession } from 'next-auth' + +const ETT_API_URL = process.env.ETT_API_URL + +export async function GET(req: NextRequest, { params }: { params: { id: string } }) { + const ettAPIUrl = `${ETT_API_URL}/registration/contact/${params.id}` + try { + const response = await fetch(ettAPIUrl, { + method: 'GET', + }) + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { + success: false, + error: data, + }, + { status: 500 } + ) + } + return NextResponse.json( + data.map((x: { result: string }) => x.result), + { status: 200 } + ) + } catch (error: unknown) { + if (error instanceof Error) { + return NextResponse.json( + { + success: false, + error: error.message || 'An error occurred', + }, + { status: 500 } + ) + } + } +} + +export async function POST(request: NextRequest, { params }: { params: { id: string } }) { + const body = await request.json() + const emailAddressToRegister = body.contactEmailAddressToAdd?.toLowerCase() + if (_.isEmpty(emailAddressToRegister)) { + return NextResponse.json( + { + success: false, + error: 'Email address is required', + }, + { status: 500 } + ) + } + const ettAPIUrl = `${ETT_API_URL}/registration/contact/${params.id}` + try { + const response = await fetch(ettAPIUrl, { + method: 'POST', + body: emailAddressToRegister, + }) + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { + success: false, + error: data, + }, + { status: 500 } + ) + } + return NextResponse.json(data, { status: 200 }) + } catch (error: unknown) { + if (error instanceof Error) { + return NextResponse.json( + { + success: false, + error: error.message || 'An error occurred', + }, + { status: 500 } + ) + } + } +} + +export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { + const session = await getServerSession(authOptions) + const jsessionid = session?.user?.jsessionid ?? '' + if (request.nextUrl.searchParams.has('contact')) { + const contactEmailAddress = request.nextUrl.searchParams.get('contact') + const ettAPIUrl = `${ETT_API_URL}/registration/contact/${params.id}/${contactEmailAddress}` + try { + const response = await fetch(ettAPIUrl, { + method: 'DELETE', + headers: session + ? { 'Content-Type': 'application/json', Cookie: `JSESSIONID=${jsessionid}` } + : { 'Content-Type': 'application/json' }, + }) + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { + success: false, + error: data, + }, + { status: 500 } + ) + } + return NextResponse.json(data, { status: 200 }) + } catch (error: unknown) { + if (error instanceof Error) { + return NextResponse.json( + { + success: false, + error: error.message || 'An error occurred', + }, + { status: 500 } + ) + } + } + } else { + try { + const ettAPIUrl = `${ETT_API_URL}/registration/direct/${params.id}` + const response = await fetch(ettAPIUrl, { + method: 'DELETE', + headers: session + ? { 'Content-Type': 'application/json', Cookie: `JSESSIONID=${jsessionid}` } + : { 'Content-Type': 'application/json' }, + }) + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { + success: false, + error: data, + }, + { status: 500 } + ) + } + return NextResponse.json(data, { status: 200 }) + } catch (error: unknown) { + if (error instanceof Error) { + return NextResponse.json( + { + success: false, + error: error.message || 'An error occurred', + }, + { status: 500 } + ) + } + } + } +} diff --git a/src/app/api/direct/register/route.ts b/src/app/api/direct/register/route.ts new file mode 100644 index 00000000..4cbd25a8 --- /dev/null +++ b/src/app/api/direct/register/route.ts @@ -0,0 +1,111 @@ +import { authOptions } from '@/lib/auth' +import { getServerSession } from 'next-auth' +import { NextRequest, NextResponse } from 'next/server' +import _ from 'lodash' +const ETT_API_URL = process.env.ETT_API_URL + +const getETTProperties = async () => { + const response = await fetch(`${ETT_API_URL}/properties`) + return await response.json() +} +async function isEmailDomainValid(emailAddressToRegister: string): Promise { + const emailDomain = emailAddressToRegister.split('@')[1] + const invalidDomains = await getInvalidDomains() + if (invalidDomains.includes(emailDomain)) { + return false + } + return true +} + +export async function POST(request: NextRequest) { + const session = await getServerSession(authOptions) + if (!session) { + return NextResponse.json(false) + } + try { + const jsessionid = session?.user?.jsessionid ?? '' + const body = await request.json() + const emailAddressToRegister = body.directEmailAddress?.toLowerCase() + console.log(`DEBUG: emailAddressToRegister: ${emailAddressToRegister}`) + if (_.isEmpty(emailAddressToRegister)) { + return NextResponse.json( + { + success: false, + error: 'Email address is required', + }, + { status: 500 } + ) + } + const isValidEmailDomain = await isEmailDomainValid(emailAddressToRegister) + if (!isValidEmailDomain) { + return NextResponse.json( + { + success: false, + error: 'Invalid email domain', + }, + { status: 500 } + ) + } + const response = await fetch(`${ETT_API_URL}/registration/direct`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Cookie: `JSESSIONID=${jsessionid}`, + }, + body: emailAddressToRegister, + }) + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { + success: false, + error: data, + }, + { status: 500 } + ) + } + return NextResponse.json(data, { status: 200 }) + } catch (error: unknown) { + if (error instanceof Error) { + return NextResponse.json( + { + success: false, + error: error.message || 'An error occurred', + }, + { status: 500 } + ) + } + } +} + +export async function GET() { + const session = await getServerSession(authOptions) + const jsessionid = session?.user?.jsessionid ?? '' + const response = await fetch(`${ETT_API_URL}/registration/direct`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Cookie: `JSESSIONID=${jsessionid}`, + }, + }) + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { + success: false, + error: data, + }, + { status: 500 } + ) + } + return NextResponse.json(data, { status: 200 }) +} + +async function getInvalidDomains() { + const properties = await getETTProperties() + const dcdt2014Domain = `${properties?.dcdt2014domain}` + const dcdt2015Domain = `${properties?.dcdt2015domain}` + const dcdtDomain = 'dcdt31' + const invalidDomains = [dcdt2014Domain, dcdt2015Domain, dcdtDomain] + return invalidDomains +} diff --git a/src/app/direct/validate/page.tsx b/src/app/direct/validate/page.tsx deleted file mode 100644 index 4f11e2d1..00000000 --- a/src/app/direct/validate/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import ValidateComponent from '@/components/direct/validate/Validate' - -const Validate = () => { - return -} - -export default Validate diff --git a/src/app/direct/xdm/page.tsx b/src/app/direct/xdm/page.tsx new file mode 100644 index 00000000..841d934c --- /dev/null +++ b/src/app/direct/xdm/page.tsx @@ -0,0 +1,7 @@ +import XDMComponent from '@/components/direct/validators/XDMValidator' + +const Validate = () => { + return +} + +export default Validate diff --git a/src/app/direct/xdr-edge/page.tsx b/src/app/direct/xdr-edge/page.tsx new file mode 100644 index 00000000..8fa1c65d --- /dev/null +++ b/src/app/direct/xdr-edge/page.tsx @@ -0,0 +1,7 @@ +import XDREdgeComponent from '@/components/direct/xdr-edge/XDREdgeTestTool' + +const XDREdgeTest = () => { + return +} + +export default XDREdgeTest diff --git a/src/assets/SMTPTestCases.tsx b/src/assets/SMTPTestCases.tsx index 888848d7..e5707db1 100644 --- a/src/assets/SMTPTestCases.tsx +++ b/src/assets/SMTPTestCases.tsx @@ -1581,6 +1581,7 @@ const testCases = { criteria: "['h1-1','h2-10','sa1-1','sc2-10']", sutHisp: true, sutEdge: false, + ccdaFileRequired: true, fields: [ { label: 'C-CDA Document Type', @@ -1622,6 +1623,7 @@ const testCases = { criteria: "['h1-1','h2-10','sa1-1','sc2-10']", sutHisp: true, sutEdge: false, + ccdaFileRequired: true, fields: [ { label: 'C-CDA Document Type', @@ -1663,6 +1665,7 @@ const testCases = { criteria: "['h1-1','sa1-1','sc2-10']", sutHisp: true, sutEdge: false, + ccdaFileRequired: true, fields: [ { label: 'C-CDA Document Type', diff --git a/src/components/archived/ArchiveCard.cy.tsx b/src/components/archived/ArchiveCard.cy.tsx deleted file mode 100644 index 595c50ae..00000000 --- a/src/components/archived/ArchiveCard.cy.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' -import ArchiveCard from './ArchiveCard' - -describe('', () => { - it('renders', () => { - cy.mount() - }) -}) diff --git a/src/components/archived/ArchiveCard.tsx b/src/components/archived/ArchiveCard.tsx index 6d09b137..b767b0ad 100644 --- a/src/components/archived/ArchiveCard.tsx +++ b/src/components/archived/ArchiveCard.tsx @@ -1,36 +1,63 @@ import * as React from 'react' import Card from '@mui/material/Card' -import CardContent from '@mui/material/CardContent' import Typography from '@mui/material/Typography' -import { Button, CardActions } from '@mui/material' -import ArrowForwardIcon from '@mui/icons-material/ArrowForward' +import Divider from '@mui/material/Divider' +import { CardHeader, CardContent, List, ListItem, ListItemText, Box } from '@mui/material' +export interface QAItem { + question: string + answer: string +} -export interface CriteriaCardProps { - cardHeader: string - description: string +interface FAQCardProps { + header: string + items: QAItem[] } -const ArchiveCard = ({ cardHeader, description }: CriteriaCardProps) => { + +const ArchiveCard = ({ header, items }: FAQCardProps) => { return ( - - - {cardHeader} - - - {description} - + + + + + {items.map((item, index) => ( + + + + + + Q: + + + {item.question} + + + + + A: + + + {item.answer.split('\n').map((i, key) => { + return ( + + {i} +
+
+ ) + })} +
+
+
+
+
+ ))} +
- - -
) } diff --git a/src/components/archived/ArchiveHome.tsx b/src/components/archived/ArchiveHome.tsx index b9539eff..27c0c33e 100644 --- a/src/components/archived/ArchiveHome.tsx +++ b/src/components/archived/ArchiveHome.tsx @@ -1,37 +1,42 @@ 'use client' -// MUI Imports -import { Box, Container, Link } from '@mui/material' -import BannerBox from '../shared/BannerBox' -// Global Imports -// Styles +import { Card, CardContent, CardHeader, Container, Typography, Link, Divider } from '@mui/material' +import Box from '@mui/material/Box' +import BannerBox from '@shared/BannerBox' import SubMenu, { menuProps } from '@/components/shared/SubMenu' import palette from '@/styles/palette' -import ArchiveCard from './ArchiveCard' -import ArchiveFilter from './ArchiveFilter' -// Images +import FAQCard from './ArchiveCard' +import faq from './data/FAQ.json' +import VideoItem from '../resources/VideoItem' +import ForwardToInboxOutlinedIcon from '@mui/icons-material/ForwardToInboxOutlined' -const ArchiveHome = () => { +export default function ArchiveHome() { const menuItems: menuProps[] = [ - { heading: 'All', href: '' }, - { heading: 'C-CDA', href: '' }, - { heading: 'Direct', href: '' }, - { heading: 'Edge', href: '' }, - { heading: 'XDM', href: '' }, - { heading: 'Other', href: '' }, - { heading: 'Contact Us', href: '' }, + { heading: 'Overview', href: '#overview' }, + { heading: 'FAQs', href: '#faq' }, + { heading: 'C-CDA', href: '#ccda' }, + { heading: 'Direct', href: '#direct' }, + { heading: 'XDM', href: '#xdm' }, + { heading: 'XDR', href: '#xdr' }, + { heading: 'SMTP', href: '#smtp' }, + { heading: 'Other', href: '#other' }, + { heading: 'Videos', href: '#videos' }, + { + heading: 'Contact Us', + href: 'mailto:edge-test-tool@googlegroups.com', + icon: , + }, ] - function trackMenuItemClick(heading: string) { if (typeof window.gtag === 'function') { - window.gtag('event', 'Click archive sub menu', { + window.gtag('event', 'Click FAQs sub menu', { event_category: 'Navigation', event_label: heading, }) } } + return ( - - {/* Global Header */} +
@@ -39,75 +44,137 @@ const ArchiveHome = () => { } heading={'Archived tools, files and other additional content'} + isTourButton={false} description={ - <> - Unearth a treasure trove of archived resources including tools, files, and more! Please be aware that these - materials are no longer actively maintained. Despite this, they offer valuable insights and historical - context. Dive into our curated collection to explore and discover valuable resources for your projects and - endeavors! - + 'This section contains archived materials, including FAQs and videos. While these items are no longer actively maintained, they may provide useful insights and historical context. Feel free to explore the collection for reference and background information.' } /> - - - - - - - - - - - - - - + + + + + + + + + + + The Frequently Asked Questions (FAQ) section of the SITE website compiles questions and inquiries + we have received in the past. You can search for answers by SITE's features and + functionality. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - +
) } - -export default ArchiveHome diff --git a/src/components/resources/data/FAQ.json b/src/components/archived/data/FAQ.json similarity index 83% rename from src/components/resources/data/FAQ.json rename to src/components/archived/data/FAQ.json index fee6a13a..407cc3ea 100644 --- a/src/components/resources/data/FAQ.json +++ b/src/components/archived/data/FAQ.json @@ -2,7 +2,7 @@ "Overview": [ { "question": "What is SITE?", - "answer": "Discover the core concept and purpose behind SITE, and how it can benefit you or your organization." + "answer": "The Standards Implementation & Testing Environment (SITE) is a centralized collection of testing tools and resources designed to assist health IT developers and users in fully evaluating specific technical standards and maximizing the potential of their health IT implementations. SITE includes test tools, sample data, collaboration resources, and useful links." }, { "question": "How do I get started with SITE?", @@ -10,19 +10,11 @@ }, { "question": "What are the key features of SITE?", - "answer": "Explore the various tools, functions, and capabilities that SITE offers to help you achieve your goals." - }, - { - "question": "How can I connect with other users on SITE?", - "answer": "Learn how to build connections, join communities, and engage with fellow SITE users." - }, - { - "question": "Is SITE compatible with mobile devices?", - "answer": "Find out if SITE is accessible on smartphones and tablets, and how to make the most of the mobile experience." + "answer": "SITE includes ONC Certification Tools, General Testing Tools, and Resources. ONC Certification Tools include FHIR resources, C-CDA testing, clinical quality measure testing, electronic prescribing tools, public health reporting testing, and alternative testing. Our General Testing Tools include a suite of general testing tools, including the CPOE Evaluation Tool, IHE Testing Tools, Lantern Project, and the Inferno HL7 FHIR Validator. In addition to this FAQ resource, videos and documentation help enhance your understanding of SITE." }, { "question": "How can I report issues or seek assistance on SITE?", - "answer": "Get information on how to report bugs, seek technical support, or contact our customer service team." + "answer": "Contact us at edge-test-tool@googlegroups.com or access our forum for the latest questions & answers. \nQuestions about the applicability of the initial set of standards, implementation specifications, and certification criteria should be directed to ONC at ONC.Certification@hhs.gov. Questions about the functions and activities of the ONC-ATLs and ONC-ACBs should also be directed to ONC at ONC.Certification@hhs.gov." } ], "Direct": [ @@ -36,7 +28,7 @@ }, { "question": "Why am I not receiving a validation report?", - "answer": "Please verify that you have registered your direct address with a corresponding contact address which is where the validation report will be delivered, see https://ttpedge.sitenv.org/ttp/#/direct/register. If you have registered, please check the spam folder to see if the validation report has been held there. Also, the local mailbox (the part before the '@' symbol) portion is case-sensitive;verify that the case in what was registered matches what is being sent." + "answer": "Please verify that you have registered your direct address with a corresponding contact address which is where the validation report will be delivered, see https://ttpedge.sitenv.org/ttp/#/direct/register. If you have registered, please check the spam folder to see if the validation report has been held there. Also, the local mailbox (the part before the '@' symbol) portion is case-sensitive; verify that the case in what was registered matches what is being sent." }, { "question": "Why can't the ETT find my certificate via LDAP?", @@ -48,7 +40,7 @@ }, { "question": "What is the difference between these two validator sides?: 1. This one is for certification: https://ttpedge.sitenv.org/ttp/#/validators 2. Another that some people have used: https://sitenv.org/sandbox-ccda/ccda-validator", - "answer": "The backend is the same; its simply a different interface." + "answer": "The backend is the same; it's simply a different interface." }, { "question": "In the Direct section of the ETT, sending a direct message to the system under test results in a validation result for the MDNs the SUT produces. I don't believe this case should be sending validation results at all since the testing is around the certificates rather than the MDNs. It looks like the MDNs are being validated as direct messages whether or not the MDN is wrapped. The ETT also seems to be fine with the MDNs in test cases 39, 40 and 41. Should we be receiving validation results for our MDNs in this case?", @@ -56,15 +48,15 @@ }, { "question": "I'm not getting validation emails from the Direct Testing part of the ETT?", - "answer": "The address registered, needs to have a contact email address associated with it." + "answer": "The address registered needs to have a contact email address associated with it." }, { "question": "SMTP MT Test 39, 40 and 41 are not working. All have been working for us previously. After clicking \"Run\", I see no attempt at an inbound SMTP connection to my server. I have tried using both a hostname and an IP address?", - "answer": "Please upload your trust anchor/public cert using the \"Upload Certificate\" on the left bottom corner of the \"More Info\" section for the these tests." + "answer": "Please upload your trust anchor/public cert using the \"Upload Certificate\" on the left bottom corner of the \"More Info\" section for these tests." }, { "question": "I have imported the invalid trust anchor in DIRECT Server & removed the valid Trust anchor of the ETT. I'm sending an email from the ETT, selecting 'GOOD_CERT' what is the expected functionality. My DIRECT Server is not rejecting this message, though I have an invalid trust anchor in my DIRECT Server.", - "answer": "The certificate that you are selecting is the “Signing Certificate”. So a GOOD CERT will send a message with a signing certificate that is valid. The trust anchor, that you have, should dictate whether messages from a particular address is accepted or rejected. If you don’t have the right trust anchor, then the message should be rejected. That is the expected behavior." + "answer": "The certificate that you are selecting is the “Signing Certificate”. So, a GOOD CERT will send a message with a signing certificate that is valid. The trust anchor that you have should dictate whether messages from a particular address are accepted or rejected. If you don’t have the right trust anchor, then the message should be rejected. That is the expected behavior." }, { "question": "Not sure what I'm missing. I've uploaded the trust cert and the log shows a message sent to the proper address 1: SENDING EMAIL WITH MESSAGE DISPOSITION NOTIFICATION HEADER TO sand...@direct.... 2: Email sent Successfully 3: Message-ID of the email sent However nothing is hitting our SMTP server. I was able to get messages from the DCDT tests as well as Send Direct Message tests. Am I missing a setup somewhere?", @@ -72,7 +64,7 @@ }, { "question": "What are the SMTP MT addresses?", - "answer": "The addresses are listed below.Also consult with your ALT for any changes or updates.\nSMTP MT Test 21: processeddispatched6@ttpedge.sitenv.org\nSMTP MT Test 23: badaddresst@ttpds2.siten.org \n SMTP MT Test 24: provider1@direct2.sitenv.org\nSMTP MT Test 25: failure15@ttpds.sitenv.org\nSMTP MT Test 26: nomdn8@ttpedge.sitenv.org\nSMTP MT Test 27: processedonly5@ttpedge.sitenv.org\nSMTP MT Test 29: processeddispatched6@ttpedge.sitenv.org" + "answer": "The addresses are listed below. Also consult with your ALT for any changes or updates.\nSMTP MT Test 21: processeddispatched6@ttpedge.sitenv.org\nSMTP MT Test 23: badaddresst@ttpds2.siten.org \n SMTP MT Test 24: provider1@direct2.sitenv.org\nSMTP MT Test 25: failure15@ttpds.sitenv.org\nSMTP MT Test 26: nomdn8@ttpedge.sitenv.org\nSMTP MT Test 27: processedonly5@ttpedge.sitenv.org\nSMTP MT Test 29: processeddispatched6@ttpedge.sitenv.org" } ], "SMTP": [ @@ -90,7 +82,7 @@ }, { "question": "SMTP MT Test 1 (Message to Bad Address); I am responding with a DSN rather than an MDN. I am using SMTP for both sending and receiving. I am not using IMAP or POP at this time. Is that acceptable? If not, why not?", - "answer": "For these cases, ETT acts as the EDGE SMTP sender (and also the notification SMTP receiver) - and so, the ETT is set up to check for the MDNs in the failure15@ttpds.sitenv.org inbox. The SMTP/SMTP cases are designed such that ETT as an Edge SMTP server sends through the HISP, and expects the HISP to deliver the mail to its inbox failure15@hisp-testing2.nist.gov." + "answer": "For these cases, ETT acts as the EDGE SMTP sender (and also the notification SMTP receiver) - and so, the ETT is set up to check for the MDNs in the failure15@ttpds.sitenv.org inbox. The SMTP/SMTP cases are designed such that ETT as an Edge SMTP server sends through the HISP and expects the HISP to deliver the mail to its inbox failure15@hisp-testing2.nist.gov." }, { "question": "I entered my profile information, went to message tracking and followed the instructions for each of the testcases. These test cases are failing: EDGE SMTP MT Test 18(a), EDGE SMTP MT Test 47(a), EDGE SMTP MT Test 45 and EDGE SMTP MT Test 46. Can anyone outline the procedure of testing these?", @@ -120,7 +112,7 @@ }, { "question": "I ran XDR 5 with his endpoint and I see this error in the logs. \nCaused by: { code:\"500\" extendedCode:\"0\" reason:\"XdsInternal error\n SOAP Fault: com.ctc.wstx.exc.WstxParsingException: Undeclared namespace prefix \"soapenv\" at [row,col {unknown-source}]: [1,158]\" reasonPhrase:\"Internal Server Error\" }", - "answer": "expand on the error message, the requirement that the format (Simple SOAP vs MTOM) be the same is specified in IHE ITI Technical Framework, Volume 2, Appendix V: V.8.1 Simple SOAP vs MTOM, \"Both the request and the response messages shall use the same encoding.\"\nMore readable descriptions and the background behind this requirement can be found:\nhttps://wiki.ihe.net/index.php/XDS.b_Implementation#More_on_MTOM_vs_MTOM.2FXOP\nhttps://wiki.ihe.net/index.php/XDS.b_Implementation#Interpreting_Web_Services_Specifications_for_XDS.b_and_XCA\nMost web service packages/libraries (such as Apache Axis2, etc) will make this switch automatically.\n- The timeout for the registry / XDS Toolkit is 60 seconds." + "answer": "expand on the error message, the requirement that the format (Simple SOAP vs MTOM) be the same is specified in IHE ITI Technical Framework, Volume 2, Appendix V: V.8.1 Simple SOAP vs MTOM, \"Both the request and the response messages shall use the same encoding.\"\nMore readable descriptions and the background behind this requirement can be found:\nhttps://wiki.ihe.net/index.php/XDS.b_Implementation#More_on_MTOM_vs_MTOM.2FXOP\nhttps://wiki.ihe.net/index.php/XDS.b_Implementation#Interpreting_Web_Services_Specifications_for_XDS.b_and_XCA\nMost web service packages/libraries (such as Apache Axis2, etc.) will make this switch automatically.\n- The timeout for the registry / XDS Toolkit is 60 seconds." }, { "question": "What are the XDR, Direct From Address, email endpoints?", @@ -131,7 +123,7 @@ "answer": "For this test, all the SUT needs to do is establish an HTTPS connection using the provided certificates. Please download the certificates found at the top of the page." }, { - "question": "Edge XDR Test 5 (Receive): I am getting Request from ETT tool I validate it and genererate the below response with return type \"String\"\n\nContent-Type: multipart/related; boundary=MIMEBoundaryurn_uuid_45f3cde8-1e8f-4f81-bfdb-c40365cc698a; type=\"application/xop+xml\"; start=\"0.urn:uuid:45f3cde8-1e8f-4f81-bfdb-c40365cc698a@apache.org\"; start-info=\"application/soap+xml\"; action=\"urn:ihe:iti:2007:ProvideAndRegisterDocumentSet-bResponse\"...\n\nWant to know how to validate XDR soap header & body ?\nAnd also what should be response format ?", + "question": "Edge XDR Test 5 (Receive): I am getting Request from ETT tool I validate it and generate the below response with return type \"String\"\n\nContent-Type: multipart/related; boundary=MIMEBoundaryurn_uuid_45f3cde8-1e8f-4f81-bfdb-c40365cc698a; type=\"application/xop+xml\"; start=\"0.urn:uuid:45f3cde8-1e8f-4f81-bfdb-c40365cc698a@apache.org\"; start-info=\"application/soap+xml\"; action=\"urn:ihe:iti:2007:ProvideAndRegisterDocumentSet-bResponse\"...\n\nWant to know how to validate XDR soap header & body ?\nAnd also what should be response format ?", "answer": "Please refer to the IHE specifications and wiki for how to create/validate the XDR messages.\nThere are a few threads in the google group that you can use for reference." }, { @@ -140,7 +132,7 @@ }, { "question": "To deliver disposition statuses asynchronously for XDR transmissions, is it expected that first a ResponseStatusType:Success is included by default in the synchronous reply or should it be omitted entirely?", - "answer": "Based on the underlying XDR work-flow, ResponseStatusType of Success would be expected to be sent back." + "answer": "Based on the underlying XDR workflow, ResponseStatusType of Success would be expected to be sent back." }, { "question": "To which endpoint should XDR MDN's be sent?", @@ -152,10 +144,10 @@ }, { "question": "When testing XDR sending, the user must click “RUN” on the tool (to generate the endpoint) and then send a message. When testing SMTP sending, it’s the opposite. The user must send the message and then click “RUN.” This inconsistency makes use of the tool more challenging.", - "answer": "This difference in the workflows is an artifact of the underlying technologies and unfortunately not something that would be easy to change on our UI. For XDR, the endpoints must be established ahead of time or else the systems wouldn't know where to send messages. The Direct From address must be known so that the message can be properly routed within the ETT to the correct test instance while the test case is running. The ETT and Toolkit are an active part of this communication. The ETT must be primed BEFORE the communication takes place.\nFor SMTP, the ETT only becomes involved after the mail has been sent and is sitting in a mailbox. The SMTP communication/commands must be completed before ETT can check, or else there is nothing for ETT to find. The ETT can only step in AFTER the SMTP communication akes place.\nThe underlying protocols themselves have different workflows." + "answer": "This difference in the workflows is an artifact of the underlying technologies and unfortunately not something that would be easy to change on our UI. For XDR, the endpoints must be established ahead of time or else the systems wouldn't know where to send messages. The Direct From address must be known so that the message can be properly routed within the ETT to the correct test instance while the test case is running. The ETT and Toolkit are an active part of this communication. The ETT must be primed BEFORE the communication takes place.\nFor SMTP, the ETT only becomes involved after the mail has been sent and is sitting in a mailbox. The SMTP communication/commands must be completed before ETT can check, or else there is nothing for ETT to find. The ETT can only step in AFTER the SMTP communication takes place.\nThe underlying protocols themselves have different workflows." }, { - "question": "I have sent my XDR message to the endpoint and receive a Registry Response. However, the ETT never moves past the Pending Refresh button no matter how many times I press it. How do I move forward?", + "question": "I have sent my XDR message to the endpoint and received a Registry Response. However, the ETT never moves past the Pending Refresh button no matter how many times I press it. How do I move forward?", "answer": "Verify that your XDR message contains the Direct Address Block and that Block is completely filled out and matches what was entered into the Test Case UI. The ETT uses the Direct Address block to route the messages; so if the values do not match, they will not be forwarded to the correct module within ETT." }, { @@ -199,7 +191,7 @@ "answer": "We found the logs for this test case :\n\nINFO 3888 --- [io-12080-exec-1] g.n.h.ttt.xdr.api.TLSClientImpl : Sending a request to the server using socket:\nSocket class: class sun.security.ssl.SSLSocketImpl\nRemote address = ych-qa-mu3.test.yourcareuniverse.net/52.73.115.17\nRemote port = 9283\nLocal socket address = /172.31.38.171:36196\nLocal address = /172.31.38.171\nLocal port = 36196\nNeed client authentication = false\nCipher suite = SSL_NULL_WITH_NULL_NULL\nProtocol = NONE\njavax.net.ssl.SSLException: Connection has been shutdown: javax.net.ssl.SSLHands\nhakeException: Received fatal alert: bad_certificate\n\nIt looks like you are using a bad certificate. Please re-download the certificate and re-install it." }, { - "question": "XDR Test 3 (Revieve): We believe we having an issue Receiving the Documents and saving to the EHR. We have code to check for a document ID which has been sent in the past. Is there a way we could set the unique document ID in the test so that we can import the files more easily?", + "question": "XDR Test 3 (Receive): We believe we having an issue Receiving the Documents and saving to the EHR. We have code to check for a document ID which has been sent in the past. Is there a way we could set the unique document ID in the test so that we can import the files more easily?", "answer": "The ETT uses a symbolic ID (\"Document01\") in the metadata to identify the attached document:\n\nIt is incumbent upon the receiving registry in a provide and register transaction to turn the symbolic ID into a UUID of their choosing. In IHE ITI, Volume 2b, see Section 3.42.4.1.3.7 UUIDs and Symbolic Ids:\n\"If a field is formatted as a symbolic Id in the Submission Request, the Document Registry shall replace it with newly generated, properly formatted UUIDs upon acceptance of the submission. If the same symbolic Id appears more than once in the Submission Request, it shall be replaced with the same generated UUID.\"\nThe receiving system must be able to handle symbolic IDs in accordance with the base IHE specifications." }, { @@ -219,16 +211,16 @@ "answer": "The X-DIRECT-FINAL-DESTINATION-DELIVERY header is what is checked for this test case.\nThe “DIRECT To:” is the direct address that is responded to and is registered/mapped in the Direct portion of the ETT.\nThe \"Outgoing (ETT --> SUT) Direct From Address\" refers to the value that we place in the element in all outgoing messages from ETT to the SUT. This may be optional for your system." }, { - "question": "Is there a sample you can provided for XDR MT TEST 30 so show what the header should look like?", + "question": "Is there a sample you can provide for XDR MT TEST 30 so show what the header should look like?", "answer": "The soap body could itself be inspected from the \"Log\" section of the test by clicking the \"Waiting for Validation\" or \"Logs\" button after running the test.. Please let us know if this is not what you are looking for.\n\nOf particular interest is this part true\ntrue\nFollow-up: In the 30 and 31, the proctor needs to verify that the headers are being processed and \"accept\" / \"reject\" based on the demonstration.\n\nFor 32 and above, the \"Logs\" button can be used to see the synchronous/asynchronous delivery notification produced by the System Under Test (SUT), however, the process of accept/reject still remains the same - a verification by the proctor as per IG for Delivery Notification 1.5.2.1.1.\n\n\"The proctor will read the logs for the synchronous communication or the asynchronous communication for an appropriate response. This test is part of the Implementation Guide for Delivery Notification test suite\"" }, { - "question": "Please confirm my understanding. The ETT for XDR MT TEST 30 sends a a VALID delivery notifications request to the system under test in the following format. The system under test then sends a Direct message based on the VALID delivery notifications request. If I am correct then is the format above that you sent me the complete VALID delivery notifications request? We are trying to give vendors a sample of what the VALID delivery notifications request looks like.", + "question": "Please confirm my understanding. The ETT for XDR MT TEST 30 sends a VALID delivery notifications request to the system under test in the following format. The system under test then sends a Direct message based on the VALID delivery notifications request. If I am correct then is the format above that you sent me the complete VALID delivery notifications request? We are trying to give vendors a sample of what the VALID delivery notifications request looks like.", "answer": "Yes, that is correct; the format is same - however the multi-part identifier and other ids and direct:from/to elements will differ based on the session." }, { "question": "The vendor does not understand XDR MT TEST 30, and I am not 100% sure of what role the Test Proctor has in the verification process. This is the message flow chart from page 11 of the IG for Delivery Notification. So...\n1. The ETT acts as the Receiving STA because the ETT sends a VALID delivery notifications request?\n2. The SUT is expected to receive this valid delivery notification, and send a success notification back to the ETT (with the ETT acting as the original sending Edge client?\nIf this is correct then what format of the success notification is the ETT looking for? And, what verification does the proctor do since it seems that some verification is done automatically by the ETT, and other verification seems like it is visual inspection by the proctor.", - "answer": "The complete positive delivery notification flow described is being tested in XDR MT TEST 38 (and in these cases, ETT only shows the logs and no automatic verification is being done, the proctor has to accept/reject based on visual inspection as per examples in the IG for Direct-Edge 1.5.2.1.1.)\nHowever, XDR MT TEST 30 tests only a part of it, namely that the XDR well formed header is being processed by SUT - this can be demonstrated by showing the SUT logs. Similarly XDR MT TEST 31 can be demonstrated by the SUT log message of an error in the malformed field. Technically speaking XDR MT TEST 38 subsumes XDR MT TEST 30.\nFor XDR MT TEST 38, you are right - ETT acts as a sending Edge by requesting the Delivery Notification through the header. And SUT is expected to receive this valid notification and send it back to the ETT (acting as the Edge XDR) - which can be viewed from the tool's log button - in this case, the example of the notifications are given in the document example from IG for Direct-Edge 1.5.2.1.1.\n \nmailto:entity1@direct.example.org\nsuccess\n\n(for negative cases, XDR MT TESTs 33-37, this would correspond to failure with an optional reason for failure as per the example)" + "answer": "The complete positive delivery notification flow described is being tested in XDR MT TEST 38 (and in these cases, ETT only shows the logs and no automatic verification is being done, the proctor has to accept/reject based on visual inspection as per examples in the IG for Direct-Edge 1.5.2.1.1.)\nHowever, XDR MT TEST 30 tests only a part of it, namely that the XDR well-formed header is being processed by SUT - this can be demonstrated by showing the SUT logs. Similarly XDR MT TEST 31 can be demonstrated by the SUT log message of an error in the malformed field. Technically speaking XDR MT TEST 38 subsumes XDR MT TEST 30.\nFor XDR MT TEST 38, you are right - ETT acts as a sending Edge by requesting the Delivery Notification through the header. And SUT is expected to receive this valid notification and send it back to the ETT (acting as the Edge XDR) - which can be viewed from the tool's log button - in this case, the example of the notifications is given in the document example from IG for Direct-Edge 1.5.2.1.1.\n \nmailto:entity1@direct.example.org\nsuccess\n\n(for negative cases, XDR MT TESTs 33-37, this would correspond to failure with an optional reason for failure as per the example)" }, { "question": "The SUT is never creating a notification in MT 38 since it just forwards a copy of the notification from the ETT back to the ETT?\n\n\"For XDR MT TEST 38, you are right - ETT acts as a sending Edge by requesting the Delivery Notification through the header. And SUT is expected to receive this valid notification and send it back to the ETT (acting as the Edge XDR)\"", @@ -249,8 +241,8 @@ "answer": "Check that the IHE_XDM and SUBSET01 directories have been explicitly created. When the ZIP file is viewed, there should be a specific entry for each directory listing.\n\n- Some examples for XDM:\nftp://ftp.ihe.net/Connectathon/samples/XDM_samples/\n\nA table showing the difference in optionality between the different XD* metadata is available in Vol 3 of IHE ITI:\nhttp://www.ihe.net/uploadedFiles/Documents/ITI/IHE_ITI_TF_Vol3.pdf See table \" 4.3.1-3: Sending Actor Metadata Attribute Optionality\" starting on page 109." }, { - "question": "I have tried to validate some XDM zip's, but it seems that the validator stops at point 'Decoding ZIP'.\nIt works with the sample found on ftp://ftp.ihe.net/Connectathon/samples/ITI-profiles/XDM_samples/ but if you try to decompress and compress it again, it does not work anymore.\n\nI've tried with different compressing algorithm like 'Deflate', 'Deflate64', 'LZMA', 'PPMd', 'BZIP2', but none of them works. If you try with those kind of algorithm, you will see java exceptions of the validator saying 'not supported algorithm...'\nSo my question now is how to compress files to create a valid XDM-Zip?", - "answer": "Tested working solutions are:\n\n* Using of 7-Zip (17.00 beta) -> But you have to set the 'relative path' again in the GUI.\n* Using of MacOS X line command, navigate to the folder that contains all XDM-files and use 'zip -r *'\n* Using WinRAR 5.40 (64bit) -> make shure that value 'Store relativ path' under tab 'Files' is selected\nThe IHE specification states to use the ZIP format, not the RAR format for compression." + "question": "I have tried to validate some XDM zips, but it seems that the validator stops at point 'Decoding ZIP'.\nIt works with the sample found on ftp://ftp.ihe.net/Connectathon/samples/ITI-profiles/XDM_samples/ but if you try to decompress and compress it again, it does not work anymore.\n\nI've tried with different compressing algorithm like 'Deflate', 'Deflate64', 'LZMA', 'PPMd', 'BZIP2', but none of them works. If you try with those kind of algorithm, you will see java exceptions of the validator saying 'not supported algorithm...'\nSo my question now is how to compress files to create a valid XDM-Zip?", + "answer": "Tested working solutions are:\n\n* Using of 7-Zip (17.00 beta) -> But you have to set the 'relative path' again in the GUI.\n* Using of MacOS X line command, navigate to the folder that contains all XDM-files and use 'zip -r *'\n* Using WinRAR 5.40 (64bit) -> make sure that value 'Store relative path' under tab 'Files' is selected\nThe IHE specification states to use the ZIP format, not the RAR format for compression." } ], "C-CDA": [ @@ -267,15 +259,15 @@ "answer": "There is an extension package that may be used.\nsdtc:raceCode\nsdtc:ethnicGroupCode\nSee the C-CDA companion guide for additional details:\nhttp://www.hl7.org/implement/standards/product_brief.cfm?product_id=447" }, { - "question": "I'm now trying to increase the debugging output from the validator, hopefully to see where my issues exist with the vocabulary. I've tried using a global DEBUG level in the logging.properties file and setting to ALL, but it appears that the validator doesn't look at the properties file to set it's level of logging.\n\nDoes anyone know where I can change that info level for the validator classes?\n\nThe logging that I do get appears to show the vocab/valuesystems loading just too quickly.\n\n08:27:14,748 INFO [VocabularyLoadRunner:92] Loading vocabularies at: /var/tomcat2/ValidatorConfig/CodeSystems...\n08:27:14,748 INFO [VocabularyLoadRunner:94] Vocabularies loaded...\n08:27:14,748 INFO [VocabularyLoadRunner:99] Loading value sets at: /var/tomcat2/ValidatorConfig/ValueSets...\n08:27:14,749 INFO [VocabularyLoadRunner:101] Value Sets loaded...\n08:27:14,749 INFO [VocabularyLoadRunner:105] !!!!*********** VOCABULARY DATABASE HAS FINISHED LOADING - SERVER WILL CONTINUE AND SHOULD BE DONE SHORTLY. ***********!!!!", - "answer": "15:02:46,635 INFO [VocabularyLoadRunner:92] Loading vocabularies at: C:\\CCDA\\Validator\\code_repository...\n15:02:46,636 INFO [VocabularyLoadRunner:44] Loading files in : CDT...\n15:02:47,758 INFO [VocabularyLoadRunner:44] Loading files in : ICD10CM...\n15:02:51,412 INFO [VocabularyLoadRunner:44] Loading files in : ICD10PCS...\n15:02:53,378 INFO [VocabularyLoadRunner:44] Loading files in : ICD9CM_DX...\n....\nC:\\CCDA\\Validator\\valueset_repository...\n15:04:37,554 INFO [VocabularyLoadRunner:44] Loading files in : VSAC...\n15:04:37,555 INFO [VsacLoader:30] Loading Value Set File: Ability.xlsx\n15:04:37,662 INFO [VsacLoader:30] Loading Value Set File: ActPriority.xlsx\n15:04:37,708 INFO [VsacLoader:30] Loading Value Set File: ActStatus.xlsx\n...\n\nHere are my config settings for the the codes set and value set files:\n \n\nThe localCodeRepositoryDir is the path to the folders (e.g. ICD9CM_DX, ICD9CM_SG, ICD10CM, etc...) that contain the code set files.\nThe localValueSetRepositoryDir is the path to the VSAC folder that contains all of the value set files.\nI believe the application is hard-coded to look for those particular folders in order to process the files within. That may be case-sensitive..." + "question": "I'm now trying to increase the debugging output from the validator, hopefully to see where my issues exist with the vocabulary. I've tried using a global DEBUG level in the logging.properties file and setting to ALL, but it appears that the validator doesn't look at the properties file to set its level of logging.\n\nDoes anyone know where I can change that info level for the validator classes?\n\nThe logging that I do get appears to show the vocab/valuesystems loading just too quickly.\n\n08:27:14,748 INFO [VocabularyLoadRunner:92] Loading vocabularies at: /var/tomcat2/ValidatorConfig/CodeSystems...\n08:27:14,748 INFO [VocabularyLoadRunner:94] Vocabularies loaded...\n08:27:14,748 INFO [VocabularyLoadRunner:99] Loading value sets at: /var/tomcat2/ValidatorConfig/ValueSets...\n08:27:14,749 INFO [VocabularyLoadRunner:101] Value Sets loaded...\n08:27:14,749 INFO [VocabularyLoadRunner:105] !!!!*********** VOCABULARY DATABASE HAS FINISHED LOADING - SERVER WILL CONTINUE AND SHOULD BE DONE SHORTLY. ***********!!!!", + "answer": "15:02:46,635 INFO [VocabularyLoadRunner:92] Loading vocabularies at: C:\\CCDA\\Validator\\code_repository...\n15:02:46,636 INFO [VocabularyLoadRunner:44] Loading files in : CDT...\n15:02:47,758 INFO [VocabularyLoadRunner:44] Loading files in : ICD10CM...\n15:02:51,412 INFO [VocabularyLoadRunner:44] Loading files in : ICD10PCS...\n15:02:53,378 INFO [VocabularyLoadRunner:44] Loading files in : ICD9CM_DX...\n....\nC:\\CCDA\\Validator\\valueset_repository...\n15:04:37,554 INFO [VocabularyLoadRunner:44] Loading files in : VSAC...\n15:04:37,555 INFO [VsacLoader:30] Loading Value Set File: Ability.xlsx\n15:04:37,662 INFO [VsacLoader:30] Loading Value Set File: ActPriority.xlsx\n15:04:37,708 INFO [VsacLoader:30] Loading Value Set File: ActStatus.xlsx\n...\n\nHere are my config settings for the codes set and value set files:\n \n\nThe localCodeRepositoryDir is the path to the folders (e.g. ICD9CM_DX, ICD9CM_SG, ICD10CM, etc...) that contain the code set files.\nThe localValueSetRepositoryDir is the path to the VSAC folder that contains all of the value set files.\nI believe the application is hard-coded to look for those particular folders in order to process the files within. That may be case-sensitive..." }, { "question": "Hello, were there any updates made to the tool? I'm getting this same error with problem codes for \"no known problems\".", "answer": "No specific updates were made to the tool to remove the validation for the “No Known Problems”.\nAre you following the HL7 example at http://hl7-c-cda-examples.herokuapp.com/examples/view/7353a215efda8dfe3fbacb19abbb90756ce14bab" }, { - "question": "Hoping to obtain some clarity around representing NDC codes for administered immunizations in the C-CDA.\n\nNDC is published as a standard for immunizations as a CCDS element and the ONC's CCDS companion guide cites that it's supported as a translational element accompanying the CVX code in CDA R2.1. Looking at the 2.1 IG, NDC is referenced within the Immunization Medication Information entry-level template with the Vaccine Clinical Drug value set stating: \"Value set intensionally defined from RXNORM (OID: 2.16.840.1.113883.6.88), comprised of those codes whose ingredients map to NDC codes that the CDC associates with CVX codes.\" Reading this and viewing the provided examples it appears as though the IG expects only RxNorm codes (not NDC) to be used as translational element entries mapping between an NDC and a CVX.\n\nNDC's are not actually included anywhere in the certification test data/validation for immunizations (only CVX), and I'm struggling to find any examples of their use for immunizations in C-CDA. Is there a specific expectation of how NDC codes are to be represented in C-CDA docs (if at all) to satisfy the ONC's CCDS standard?", + "question": "Hoping to obtain some clarity around representing NDC codes for administered immunizations in the C-CDA.\n\nNDC is published as a standard for immunizations as a CCDS element and the ONC's CCDS companion guide cites that it's supported as a translational element accompanying the CVX code in CDA R2.1. Looking at the 2.1 IG, NDC is referenced within the Immunization Medication Information entry-level template with the Vaccine Clinical Drug value set stating: \"Value set intentionally defined from RXNORM (OID: 2.16.840.1.113883.6.88), comprised of those codes whose ingredients map to NDC codes that the CDC associates with CVX codes.\" Reading this and viewing the provided examples it appears as though the IG expects only RxNorm codes (not NDC) to be used as translational element entries mapping between an NDC and a CVX.\n\nNDC's are not actually included anywhere in the certification test data/validation for immunizations (only CVX), and I'm struggling to find any examples of their use for immunizations in C-CDA. Is there a specific expectation of how NDC codes are to be represented in C-CDA docs (if at all) to satisfy the ONC's CCDS standard?", "answer": "The test data contains CVX because that is accompanied with a SHALL requirement in the C-CDA IG.\nSince the translation elements are having a conformance statement of “MAY” and not a “SHALL”, you could choose to use the NDC codes mapped to the CVX codes in the translation element as specified by the CCG." } ], @@ -305,11 +297,11 @@ "answer": "Negative Testing which is being performed as part of the b1 capability is to ensure that the System Under Test (SUT) is more robust and is able to receive and parse C-CDA’s more effectively including the CCDS data elements. All of the CCDS vocabulary requirements that are enforced for normal b1 sample XML files are valid for negative testing also." }, { - "question": "I tried to understand the certification criteria but got stuck with understanding 170.315(h)(1) & (h)(2). I don't understand why h1, looks like h2 is covering h1. I have couple of questions:\n1. What is a edge system how to implement it? DO I HAVE TO for this MU3 Certification?\n2. As of my knowledge, a HISP can be implemented by using Direct project JAVA RI, but I don't understand what an edge protocol is and how to implement?\n3. All documents in the \"Implementation Guide for Delivery Notification in Direct V 1.1\" and the \"Applicability Statement for Secure Health Transport V 1.2\" are talking about HISP-SMTP, Edge-SMTP, and SOAP protocol what is the deference?", + "question": "I tried to understand the certification criteria but got stuck with understanding 170.315(h)(1) & (h)(2). I don't understand why h1, looks like h2 is covering h1. I have a couple of questions:\n1. What is an edge system and how to implement it? DO I HAVE TO for this MU3 Certification?\n2. As of my knowledge, a HISP can be implemented by using Direct project JAVA RI, but I don't understand what an edge protocol is and how to implement?\n3. All documents in the \"Implementation Guide for Delivery Notification in Direct V 1.1\" and the \"Applicability Statement for Secure Health Transport V 1.2\" are talking about HISP-SMTP, Edge-SMTP, and SOAP protocol. What is the deference?", "answer": "The definition of an Edge system and Edge protocols are defined in Implementation Guide for Direct Edge Protocols Version 1.1 which is available here:\nhttps://www.healthit.gov/sites/default/files/implementationguidefordirectedgeprotocolsv1_1.pdf. \n Questions regarding the Certification Program should be addressed via email to: ONC.Certification@hhs.gov. You will also discover information at this link: https://www.healthit.gov/policy-researchers-implementers/2015-edition-test-method" }, { - "question": "Attached is an Edge sample file with UNKNOWN for patient race and ethnicity. I have tried but could not find UNKNOWN any where in OMB/CDC race and ethnicity value set codes. I have tried several ways, but it is giving data validation error. Please guide us from where i can find code for UNKNOWN race and ethnicity. Is there issue in sample file data ??", + "question": "Attached is an Edge sample file with UNKNOWN for patient race and ethnicity. I have tried but could not find UNKNOWN anywhere in OMB/CDC race and ethnicity value set codes. I have tried several ways, but it is giving data validation error. Please guide us from where i can find code for UNKNOWN race and ethnicity. Is there an issue in sample file data ??", "answer": "Please refer to the C-CDA IG for the appropriate value sets and the codes for Unknown." }, { @@ -318,15 +310,15 @@ }, { "question": "Do HTTPS end points work, it isn’t working for us.", - "answer": "Vendors need to be aware of the following when using HTTPS endpoints.\n1. SUT has to import the ETT published certificates to use for Mutual TLS.\n2. They need to provide these certs in the Mutual TLS handshake when using HTTPS.\n3. Sometimes when using the ETT certs, vendors encounter exceptions.\na. Here is an example:\ni. When attempting to run tests in the Edge Test Tool, we added the Certificates into our system. We noticed a critical failure and were unable to send a message due to an issue we found with the cert.pem file. There is a Core Java feature that requires the contents of the subject (CN) to match the host. What we expected was CN=ttedge.sitenv.org but what we saw was CN=test.\nTo fix the above problem identified in bullet #3, the vendor can use the approaches outlined below.\na. If there are issues with the CN name, it is possible to map the host file (1).\nb. Alternatively make a change programmatically per the IHE recommendation (2).\nIt is acceptable to use the hostname implementation approach or the approach identified by the IHE. Using the approach identified in the IHE wiki will allow you to interoperate with other Endpoints who follow the IHE spec and provide certificates which don’t have the right CN value.\nIf you did it programmatically, you set the address as the requirement; however, the host name approach is also fine as long as you know that in the real world might encounter the same issue and you will have add more host names.\n(1) Please see the post below for the host file mapping solution.\n“What we expected was CN=ttedge.sitenv.org but what we saw was CN=test.\nTo resolve this issue, we added a entry into our host file to map \"test\" to \"ttpedge.sitenv.org.\" Please confirm this approach is acceptable.”\nhttps://groups.google.com/d/msgid/edge-test-tool/1f1c6fdd-834e-44d9-856d-57d4a07ed391%40googlegroups.com. \n Please see the posts below for the IHE programming solution. How do I eliminate exceptions caused by Certificate mismatches with the server host name? The ATNA profile says nothing about whether the DNS name of the host must match the name specified in the certificate. It assumes that possession of the certificate with the private key is sufficient to protect the connection. By default, the Java HttpsURLConnection verifies the DNS name of the host against that of the certificate. If you want to disable this, the easiest way is to add the following lines of code somewhere in the initialization of your application. HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; } }); Alternatively, you can call setHostnameVerifier on your URLConnection. connection.setHostnameVerifier(new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; } }); http://wiki.ihe.net/index.php/ATNA_FAQ#How_do_I_eliminate_exceptions_caused_by_Certificate_mismatches_with_the_server_host_name.3F https://groups.google.com/d/msgid/edge-test-tool/068f7a3c-365b-4a30-bc86-d1a6eb794590%40googlegroups.com." + "answer": "Vendors need to be aware of the following when using HTTPS endpoints.\n1. SUT has to import the ETT published certificates to use for Mutual TLS.\n2. They need to provide these certs in the Mutual TLS handshake when using HTTPS.\n3. Sometimes when using the ETT certs, vendors encounter exceptions.\na. Here is an example:\ni. When attempting to run tests in the Edge Test Tool, we added the Certificates into our system. We noticed a critical failure and were unable to send a message due to an issue we found with the cert.pem file. There is a Core Java feature that requires the contents of the subject (CN) to match the host. What we expected was CN=ttedge.sitenv.org but what we saw was CN=test.\nTo fix the above problem identified in bullet #3, the vendor can use the approaches outlined below.\na. If there are issues with the CN name, it is possible to map the host file (1).\nb. Alternatively make a change programmatically per the IHE recommendation (2).\nIt is acceptable to use the hostname implementation approach or the approach identified by the IHE. Using the approach identified in the IHE wiki will allow you to interoperate with other Endpoints who follow the IHE spec and provide certificates which don’t have the right CN value.\nIf you did it programmatically, you set the address as the requirement; however, the host name approach is also fine as long as you know that in the real world might encounter the same issue and you will have added more host names.\n(1) Please see the post below for the host file mapping solution.\n“What we expected was CN=ttedge.sitenv.org but what we saw was CN=test.\nTo resolve this issue, we added an entry into our host file to map \"test\" to \"ttpedge.sitenv.org.\" Please confirm this approach is acceptable.”\nhttps://groups.google.com/d/msgid/edge-test-tool/1f1c6fdd-834e-44d9-856d-57d4a07ed391%40googlegroups.com. \n Please see the posts below for the IHE programming solution. How do I eliminate exceptions caused by Certificate mismatches with the server host name? The ATNA profile says nothing about whether the DNS name of the host must match the name specified in the certificate. It assumes that possession of the certificate with the private key is sufficient to protect the connection. By default, the Java HttpsURLConnection verifies the DNS name of the host against that of the certificate. If you want to disable this, the easiest way is to add the following lines of code somewhere in the initialization of your application. HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; } }); Alternatively, you can call setHostnameVerifier on your URLConnection. connection.setHostnameVerifier(new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; } }); http://wiki.ihe.net/index.php/ATNA_FAQ#How_do_I_eliminate_exceptions_caused_by_Certificate_mismatches_with_the_server_host_name.3F https://groups.google.com/d/msgid/edge-test-tool/068f7a3c-365b-4a30-bc86-d1a6eb794590%40googlegroups.com." }, { - "question": "Does ETT has added the following accredited trust bundle into the testing system?", + "question": "Does ETT have added the following accredited trust bundle into the testing system?", "answer": "The public certificate and trust anchors used by TTP/ETT are available here: https://ttpedge.sitenv.org/ttp/#/direct" }, { - "question": "From what I understood from the documentation, for successful execution of the test procedures for 170.315(h)(1), the direct mail server does not require STARTTLS. Can your ensure that my understanding is correct?", - "answer": "Yes, the Direct transport does not make use of STARTTLS; while, the mail-based Edge protocols do use STARTTLS." + "question": "From what I understood from the documentation, for successful execution of the test procedures for 170.315(h)(1), the direct mail server does not require STARTTLS. Can you ensure that my understanding is correct?", + "answer": "Yes, the Direct transport does not make use of STARTTLS; while the mail-based Edge protocols do use STARTTLS." }, { "question": "Where can ATLs download the latest release of the ETT for local installation? Today (September 28th) the NIST hosted tool (https://ttpedge.sitenv.org/ttp/#/home) and the release notes (https://ttpedge.sitenv.org/ttp/#/edge/releaseNotes) report the current version is 1.1.0. The latest version of the JAR file found at the Bit Bucket downloads link is ett.1.0.19.jar (https://bitbucket.org/jperugini/ett/downloads?tab=downloads), which has a release date of June 24th, 3 months prior to the date of the current release.", @@ -336,5 +328,6 @@ "question": "Does the ETT require the same unique message ID when the message is sent to multiple recipients, or is it acceptable to have different message IDs for each recipient?", "answer": "Message IDs must be unique." } - ] + ], + "Videos": [] } diff --git a/src/components/resources/data/faq.md b/src/components/archived/data/faq.md similarity index 100% rename from src/components/resources/data/faq.md rename to src/components/archived/data/faq.md diff --git a/src/components/c-cda/scorecard/ScorecardHome.tsx b/src/components/c-cda/scorecard/ScorecardHome.tsx index 48328f63..84312674 100644 --- a/src/components/c-cda/scorecard/ScorecardHome.tsx +++ b/src/components/c-cda/scorecard/ScorecardHome.tsx @@ -45,6 +45,7 @@ import { SectionNameEnum, SORT_ORDER_STARTING_VALUE, } from './types/ScorecardConstants' +import ErrorDisplayCard from '../validation/results/ErrorDisplay' export default function ScorecardHome() { const [resultsDialogState, setResultsDialogState] = useState(false) @@ -53,6 +54,8 @@ export default function ScorecardHome() { } const [isTryMeDemo, setIsTryMeDemo] = useState(false) + const [scorecardHomeError, setScorecardHomeError] = useState('') + const demoSampleOptions: { label: string; value: string }[] = [ { label: 'High Scoring Sample', @@ -82,11 +85,11 @@ export default function ScorecardHome() { value: 'sampleWithVocabularyErrors.json', }, { - label: 'Sample with Missing Section Data', + label: 'Sample with Empty Sections', value: 'sampleWithEmptySections.json', }, { - label: 'Sample with Missing Sections and Errors', + label: 'Sample with Empty Sections and Errors', value: 'sampleWithEmptySectionsAndErrors.json', }, ] @@ -168,8 +171,14 @@ export default function ScorecardHome() { const [isValidResults, errorMessage]: [boolean, string | null] = processResults(newScorecardResponseJson) displayResults(isValidResults, errorMessage, true) } catch (error) { - // TODO: In the extremely unlikely case this happens, we should produce a dialog error - console.error('Failed to run Try Me Demo in handleSubmitDemoStart(), unable to get demo sample: ', error) + const errorMessagePrefix = 'Error running Scorecard Demo' + console.error( + `${errorMessagePrefix} in handleSubmitDemoStart(): + Failed to run Scorecard Demo in handleSubmitDemoStart(), unable to get demo sample: `, + error + ) + setScorecardHomeError(`${errorMessagePrefix}: + ${error}. Please try again later.`) } if (typeof window.gtag === 'function') { @@ -380,8 +389,7 @@ export default function ScorecardHome() { } else { const finalErrorMessage = `Error: ${errorMessage ? errorMessage : 'Unknown error message'} ` console.error(finalErrorMessage) - // TODO: Replace wuth error dialog used in C-CDA Validator - alert(finalErrorMessage) + setScorecardHomeError(finalErrorMessage) } } @@ -585,6 +593,12 @@ export default function ScorecardHome() { vocabResults={vocabResults} sortFunction={sortResultsOrderByGradeTypeAndNumberOfIssues} > + + setScorecardHomeError('')} + response={{ error: scorecardHomeError }} + /> ) diff --git a/src/components/c-cda/scorecard/ScorecardResultsDialog.tsx b/src/components/c-cda/scorecard/ScorecardResultsDialog.tsx index 1d7d26f5..dbb729be 100644 --- a/src/components/c-cda/scorecard/ScorecardResultsDialog.tsx +++ b/src/components/c-cda/scorecard/ScorecardResultsDialog.tsx @@ -15,6 +15,8 @@ import ScorecardBaseCheckSummary from './summary/ScorecardBaseCheckSummary' import ScorecardBestPracticeSummary from './summary/ScorecardBestPracticeSummary' import ScorecardCompareChartSummary from './summary/ScorecardCompareChartSummary' import { HrefLinkValueEnum } from './types/ScorecardConstants' +import { useState } from 'react' +import ErrorDisplayCard from '../validation/results/ErrorDisplay' interface ScorecardResultsDialogProps { dialogState: boolean @@ -37,16 +39,81 @@ export default function ScorecardResultsDialog({ vocabResults, sortFunction, }: ScorecardResultsDialogProps) { + const [isSaveReportLoading, setIsSaveReportLoading] = useState(false) + const [saveReportError, setSaveReportError] = useState('') + const dividerPaddingStyle = { paddingTop: 2, } - const handleSaveReport = () => { + const handleSaveReport = async () => { console.log('Enter handleSaveReport()') + + setIsSaveReportLoading(true) + setSaveReportError('') + + try { + if (!json) { + throw new Error('json is undefined, there is no data to save') + } + + const scorecardApiUrl = process.env.NEXT_PUBLIC_SCORECARD_SAVESCORECARDSERVICE_API + if (!scorecardApiUrl) { + throw new Error('The Save Scorecard API URL is undefined, there is no valid endpoint to call') + } + + console.log('Client API call running for save scorecard service ') + // Note: CORS is configured on the server to allow trusted domains + const response = await fetch(scorecardApiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // prettier-ignore + 'Accept': 'application/pdf', + }, + body: JSON.stringify(json), + }) + + if (!response.ok) { + throw new Error(`HTTP error encountered while saving scorecard results. Status: ${response.status}`) + } + + console.log('POST request successful for save scorecard service, preparing to download file') + const pdfBlobResponse = await response.blob() + console.log('PDF blob response created', pdfBlobResponse) + + console.log('Downloading the file') + const url = window.URL.createObjectURL(pdfBlobResponse) + if (url) { + console.log('Valid URL created from PDF blob response', url) + const link = document.createElement('a') + link.href = url + link.download = json?.filename ? `SITE_C-CDA_Scorecard_${json.filename}.pdf` : 'scorecardReport.pdf' + document.body.appendChild(link) + console.log('Initiating download of scorecard report') + link.click() + link.remove() + } + } catch (error) { + const errorMessagePrefix = 'Error saving scorecard report' + console.error(`${errorMessagePrefix} in handleSaveReport():`, error) + setSaveReportError(`${errorMessagePrefix}: + ${error}. Please try again later.`) + } finally { + setIsSaveReportLoading(false) + } } const handleDownloadSampleDocument = () => { console.log('Enter handleDownloadSampleDocument()') + // Create an anchor element since next.js can use that cleanly to download a static asset in the public folder + const locationPrefix = '/c-cda/scorecard/samples/' + const link = document.createElement('a') + link.setAttribute('download', '') // Force download vs nav, no filenmae set so uses default url name + link.href = locationPrefix + (json?.filename ? json.filename : 'UnknownFilename.xml') + document.body.appendChild(link) + link.click() + link.remove() } const isShowSampleDownloadButton: boolean = isTryMeDemo @@ -78,7 +145,7 @@ export default function ScorecardResultsDialog({
- + @@ -102,9 +169,14 @@ export default function ScorecardResultsDialog({ }} > - + setSaveReportError('')} + response={{ error: saveReportError }} + /> {isShowSampleDownloadButton && ( {test.criteria && manualValidationCriteria.includes(test.criteria) && - (formattedLogs.length > 0 || criteriaMet.includes('FALSE')) && (criteriaMet.includes('TRUE') || criteriaMet.includes('FALSE')) && ( - + @@ -519,17 +562,21 @@ const TestCard = ({ test, receive }: TestCardProps) => { id={id} open={open} anchorEl={anchorEl} - onClose={handleClose} + onClose={handleClosePopover} anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ vertical: 'top', - horizontal: 'left', + horizontal: 'center', }} > {popoverMessage} )} - {requiresCCDADocument() && ( + {requiresCCDADocument() && !endpointsGenerated && ( { }} > CCDA Document Type - {documentDetails && Selected: {documentDetails.fileName}} )} - {showDocumentSelector && ( )} - {renderCriteriaMetIcon()} + - RUN + {endpointsGenerated ? 'REFRESH' : 'RUN'} +
@@ -575,11 +623,11 @@ const TestCard = ({ test, receive }: TestCardProps) => { LOGS {test.criteria && - testRequest && - (testRequest.length > 0 || criteriaMet.includes('FALSE') || criteriaMet.includes('ERROR')) && + criteriaMet && (criteriaMet.includes('TRUE') || criteriaMet.includes('FALSE') || criteriaMet.includes('ERROR') || + criteriaMet.includes('PASSED') || criteriaMet.includes('SUCCESS')) && ( - - - - Manage Your Contact Email Addresses - - {' '} - - - - - - {/* To-Do: List email addressess and delete email addresses with funtionality*/} - - Group Members - - - - + {status !== 'authenticated' ? ( + + + + ) : ( + <> + {isFetchingLoggedInUsersDirectEmailAdresses ? ( + + ) : ( + + + {isAddingDirectAddress || isDeletingDirectEmailGroup ? ( + + ) : ( + + <> + + + Add a valid Direct email address to get started. + + + addDirectAddress()} + disabled={directEmailFormatError || directEmailAddressRef.current?.value === ''} + title="Add a Direct email address to manage its contacts." + > + + + + ), + }} + /> + + {!_.isEmpty(directEmailAddressList) && ( + + + + + + + )} + + + )} + {!_.isEmpty(directEmailAddressList) && ( + <> + {isAddingContactAddress || isDeletingContactAddress || isFetchingContactAddresses ? ( + + ) : ( + + <> + + + Manage contacts for selected email + + + addContactAddress()} + title="Add a contact email address for the selected Direct email address." + disabled={contactEmailFormatError || contactEmailAddressRef.current?.value === ''} + > + + + + ), + }} + /> + + + {!_.isEmpty(contactEmailAdressList) && ( + + {contactEmailAdressList.map((x) => { + return ( + + + + deleteContactAddress(x)} + disabled={contactEmailFormatError} + color="error" + title="Delete this contact email address." + > + + + + + ) + })} + + )} + + + + )} + + )} + + {!_.isEmpty(errorMessage) && ( + setErrorMessage('')} + response={{ error: errorMessage }} + /> + )} + {!_.isEmpty(successMessage) && ( + setSuccessMessage('')} + /> + )} + + )} + + )} ) } diff --git a/src/components/direct/send-direct/SendDirect.tsx b/src/components/direct/send-direct/SendDirect.tsx index 728a7e46..f627bad6 100644 --- a/src/components/direct/send-direct/SendDirect.tsx +++ b/src/components/direct/send-direct/SendDirect.tsx @@ -1,4 +1,6 @@ 'use client' + +import { useEffect, useState } from 'react' import BannerBox from '@/components/shared/BannerBox' import Link from 'next/link' import styles from '@shared/styles.module.css' @@ -8,28 +10,44 @@ import Version12 from './Version12Tab' import MessageStatus from './message-status/MessageStatus' const SendDirect = () => { + const [selectedTab, setSelectedTab] = useState('VERSION V1.3') + const sendDirectTabs: TabInputs[] = [ { tabName: 'VERSION V1.3', tabIndex: 0, tabPanel: }, { tabName: 'VERSION V1.2', tabIndex: 1, tabPanel: }, { tabName: 'Message Status', tabIndex: 2, tabPanel: }, ] + + useEffect(() => { + const handleRouteChange = () => { + const hash = window.location.hash.replace('#', '').replace(/-/g, ' ').toLowerCase() + const tab = sendDirectTabs.find((t) => t.tabName.toLowerCase() === hash) + setSelectedTab(tab ? tab.tabName : 'VERSION V1.3') + } + + handleRouteChange() + window.addEventListener('hashchange', handleRouteChange) + + return () => { + window.removeEventListener('hashchange', handleRouteChange) + } + }, []) + return ( <> - {/* Global Header */} + Direct , - + Direct Message , ]} - heading={'Send Direct Message '} - description={<>Send a Direct message from this tool to a HISP of your choosing. Need more text here} + heading={'Send Direct Message'} + description="Send a Direct message from this tool to a HISP of your choosing. Need more text here" /> - {/* Main Content */} - + ) } diff --git a/src/components/direct/shared/AlertSnackbar.tsx b/src/components/direct/shared/AlertSnackbar.tsx new file mode 100644 index 00000000..de20380f --- /dev/null +++ b/src/components/direct/shared/AlertSnackbar.tsx @@ -0,0 +1,27 @@ +import { Snackbar, Alert } from '@mui/material' + +const AlertSnackbar = ({ + message, + severity, + open, + onClose, +}: { + message: string + severity: 'success' | 'error' | 'warning' | 'info' + open: boolean + onClose: () => void +}) => { + return ( + + + {message} + + + ) +} +export default AlertSnackbar diff --git a/src/components/direct/shared/LoadingButton.tsx b/src/components/direct/shared/LoadingButton.tsx index cfc64df2..815100f6 100644 --- a/src/components/direct/shared/LoadingButton.tsx +++ b/src/components/direct/shared/LoadingButton.tsx @@ -1,18 +1,64 @@ -import React, { ReactNode } from 'react' -import { Button, CircularProgress } from '@mui/material' +import React, { ReactNode, useState, useEffect } from 'react' +import { Button, CircularProgress, Box, Typography } from '@mui/material' import CheckIcon from '@mui/icons-material/Check' import { ButtonProps } from '@mui/material/Button' -interface LoadingButtonProps extends ButtonProps { +interface ExtendedLoadingButtonProps extends ButtonProps { loading: boolean done: boolean + progressive?: boolean + progressDuration?: number + finalLabel?: ReactNode children: ReactNode } -const LoadingButton: React.FC = ({ loading, done, children, ...props }) => { +const LoadingButton: React.FC = ({ + loading, + done, + progressive = false, + progressDuration = 3000, + finalLabel = 'Refresh', + children, + ...props +}) => { + const [progress, setProgress] = useState(0) + + useEffect(() => { + let interval: NodeJS.Timeout | null = null + if (progressive && loading && progress < 100) { + interval = setInterval(() => { + setProgress((oldProgress) => { + const diff = 100 - oldProgress + return Math.min(oldProgress + diff * 0.1, 100) + }) + }, progressDuration / 100) + } else if (!loading) { + setProgress(0) + } + + return () => { + if (interval) { + clearInterval(interval) + } + } + }, [progressive, loading, progressDuration]) + return ( ) } diff --git a/src/components/direct/shared/SubTabs.tsx b/src/components/direct/shared/SubTabs.tsx new file mode 100644 index 00000000..41cab44b --- /dev/null +++ b/src/components/direct/shared/SubTabs.tsx @@ -0,0 +1,84 @@ +'use client' +import palette from '@/styles/palette' +import { Box, Container, Tabs, Tab, styled } from '@mui/material' +import * as React from 'react' +import { useEffect } from 'react' + +export type TabInputs = { + tabName: string + tabIndex: number + tabPanel: React.ReactNode +} + +export interface TabsProps { + selectedTab: string + tabs: TabInputs[] +} + +interface TabPanelProps { + children?: React.ReactNode + index: number + value: number +} + +const SubTabsComponent = ({ selectedTab, tabs }: TabsProps) => { + const [value, setValue] = React.useState(0) + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setValue(newValue) + } + + useEffect(() => { + if (selectedTab !== '') { + const selectedTabIndex = tabs.filter((c) => c.tabName === selectedTab) + setValue(selectedTabIndex[0].tabIndex) + } + }, [selectedTab, tabs]) + + function CustomTabPanel(props: TabPanelProps) { + const { children, value, index } = props + + return
{value === index && <>{children}}
+ } + + const StyledTab = styled(Tab)(() => ({ + backgroundColor: palette.white, + color: palette.primary, + '&.Mui-selected, &:hover': { + backgroundColor: palette.white, + color: palette.primary, + }, + })) + + return ( + <> + + + + {tabs.map((tab) => ( + + ))} + + + + + {tabs.map((tab) => ( + + {tab.tabPanel} + + ))} + + ) +} +export default SubTabsComponent diff --git a/src/components/direct/test-by-criteria/B1Tab.tsx b/src/components/direct/test-by-criteria/B1Tab.tsx index b6c4f10c..729bcf98 100644 --- a/src/components/direct/test-by-criteria/B1Tab.tsx +++ b/src/components/direct/test-by-criteria/B1Tab.tsx @@ -39,10 +39,8 @@ const B1Component = () => { const handleChange = (event: SelectChangeEvent) => { const newOption = event.target.value as string - // Set showTestCard to false to remove old cards setShowTestCard(false) - // Delay the re-render of test cards to allow the old cards to be removed completely setTimeout(() => { setOption(newOption) setShowTestCard(true) diff --git a/src/components/direct/test-by-criteria/H1Tab.tsx b/src/components/direct/test-by-criteria/H1Tab.tsx index a74316bd..efaf294d 100644 --- a/src/components/direct/test-by-criteria/H1Tab.tsx +++ b/src/components/direct/test-by-criteria/H1Tab.tsx @@ -10,24 +10,60 @@ import { CardContent, Container, } from '@mui/material' +import React, { useState } from 'react' import Profile from '../shared/Profile' -import SMTPTestCard from '../shared/SMTPTestCard' import palette from '@/styles/palette' -import React, { useEffect } from 'react' -import criteria from '@/assets/Criteria' +import TestCard from '../hisp/TestCard' +import testCases from '../../../assets/SMTPTestCases' const H1Component = () => { - const [option, setOption] = React.useState('') - const [showTestCard, setShowTestCard] = React.useState(false) - const handleChange = (event: SelectChangeEvent) => { - setOption(event.target.value as string) + const [option, setOption] = useState('') + const [hostname, setHostname] = useState('') + const [email, setEmail] = useState('') + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [tlsRequired, setTlsRequired] = useState(false) + + const dropdownOptions = [ + { + value: 'directHome', + label: 'Criteria (i) Direct Home - Certificates', + link: '/direct#certification-download', + }, + { + value: 'certificateDiscovery', + label: 'Criteria (i) Certificate Discovery / Hosting', + link: '/direct/dcdt#hosting', + }, + { value: 'registerDirect', label: 'Criteria (i) Register Direct', link: '/direct/register' }, + { value: 'sendDirectMessage', label: 'Criteria (i) Send Direct Message', link: '/direct/senddirect' }, + { + value: 'receiveMessageStatus', + label: 'Criteria (i) Receive - Message Status', + link: '/direct/senddirect#message-status', + }, + { value: 'deliveryNotifications', label: 'Criteria (ii) Delivery Notifications', testCard: true }, + ] + + const h1Criteria = testCases.tests.filter((test) => test.criteria?.includes('h1-1')) + + const selectedTestCases = () => { + switch (option) { + default: + return h1Criteria + } } - const h1CriteriaList = criteria.filter((c) => c.testList === "['h1']") - /* useEffect(() => { - if (option !== '') { - setShowTestCard(true) + + const handleChange = (event: SelectChangeEvent) => { + const selectedOption = dropdownOptions.find((option) => option.value === event.target.value) + if (selectedOption?.testCard) { + setOption(event.target.value) + } else if (selectedOption?.link) { + window.location.href = selectedOption.link } - }, [option]) */ + } + + const showTestCard = option === 'deliveryNotifications' return ( @@ -49,9 +85,9 @@ const H1Component = () => { label="Choose a sub category" onChange={handleChange} > - {h1CriteriaList.map((option) => ( - - {option.name} + {dropdownOptions.map((item) => ( + + {item.label} ))} @@ -61,21 +97,35 @@ const H1Component = () => { - +
- {showTestCard && ( - - )} + {showTestCard && + selectedTestCases().map((test, i) => ( + + {' '} + + + ))}
) } + export default H1Component diff --git a/src/components/direct/test-by-criteria/ServerActions.ts b/src/components/direct/test-by-criteria/ServerActions.ts index e0645261..79dd3194 100644 --- a/src/components/direct/test-by-criteria/ServerActions.ts +++ b/src/components/direct/test-by-criteria/ServerActions.ts @@ -1,5 +1,9 @@ 'use server' import axios from 'axios' +import { authOptions } from '@/lib/auth' +import _ from 'lodash' +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' interface APICallData { testCaseNumber: number | string @@ -28,6 +32,13 @@ interface APICallData { attachmentType?: string } +export interface Documents { + [key: string]: { + dirs: Directory[] + files: FileDetail[] + } +} + interface XDRAPICallData { ip_address?: string port?: string @@ -69,6 +80,16 @@ interface XDRAPIResponse { criteriaMet: string testRequest: string testResponse: string + endpoint: string + endpointTLS: string +} + +interface StatusResponse { + criteriaMet: string + testRequest: string + testResponse: string + message: string + status: string } export async function handleAPICall(data: APICallData): Promise { @@ -100,6 +121,8 @@ export async function handleAPICall(data: APICallData): Promise { export async function handleXDRAPICall(data: XDRAPICallData): Promise { const apiUrl = process.env.XDR_TEST_BY_CRITERIA_ENDPOINT + data.id + '/run' + const session = await getServerSession(authOptions) + const jsessionid = session?.user?.jsessionid ?? '' const formattedData = { targetEndpointTLS: data.targetEndpointTLS, ip_address: data.ip_address, @@ -122,10 +145,9 @@ export async function handleXDRAPICall(data: XDRAPICallData): Promise { + const statusUrl = process.env.XDR_TEST_BY_CRITERIA_ENDPOINT + testCaseId + '/status' + const session = await getServerSession(authOptions) + const jsessionid = session?.user?.jsessionid ?? '' + try { + const response = await axios.get(statusUrl, { + headers: session + ? { 'Content-Type': 'application/json', Cookie: `JSESSIONID=${jsessionid}` } + : { 'Content-Type': 'application/json' }, + }) + const content = response.data + let testRequest = '' + let testResponse = '' + let criteriaMet = '' + if (content && content.content && content.content.value) { + testRequest = content.content.value.request || content.message + testResponse = content.content.value.response || content.message + criteriaMet = content.content.criteriaMet + } + console.log('Status fetched: ', content) + return { + criteriaMet: criteriaMet, + testRequest: testRequest, + testResponse: testResponse, + message: content.message, + status: content.status, + } + } catch (error) { + if (axios.isAxiosError(error)) { + console.error('Failed to fetch status:', error.response?.data) + throw new Error(`Failed to fetch status: ${error.message}`) + } else { + console.error('Unexpected error:', error) + throw new Error('An unexpected error occurred') + } + } +} + +export async function fetchCCDADocuments(receive: boolean): Promise { + const baseUrl = receive + ? process.env.CCDA_DOCUMENTS_XDR || 'https://ett.healthit.gov/ett/api/ccdadocuments?testCaseType=xdr' + : process.env.CCDA_DOCUMENTS || 'https://ett.healthit.gov/ett/api/ccdadocuments?testCaseType' + + const config = { + method: 'get', + url: baseUrl.toString(), + headers: { 'Content-Type': 'application/json' }, + } + + try { + const response = await axios(config) + return response.data } catch (error) { if (axios.isAxiosError(error) && error.response) { console.error('API Error Response:', error.response.data) diff --git a/src/components/direct/transport-tool/TransportTestToolContent.tsx b/src/components/direct/transport-tool/TransportTestToolContent.tsx index 2ac4a3a5..7ce5e6c7 100644 --- a/src/components/direct/transport-tool/TransportTestToolContent.tsx +++ b/src/components/direct/transport-tool/TransportTestToolContent.tsx @@ -55,7 +55,10 @@ const TransportTestToolContent = ({ sampleCCDAFiles, trustBundleDownloadUrl }: T > Publish your Direct certificates (Organization or Address bound) per the{' '} - Direct specifications. + + Direct specifications + + . diff --git a/src/components/direct/transport-tool/actions.ts b/src/components/direct/transport-tool/actions.ts index 8e93f8df..feb03b5a 100644 --- a/src/components/direct/transport-tool/actions.ts +++ b/src/components/direct/transport-tool/actions.ts @@ -1,4 +1,5 @@ 'use server' +import { GENERIC_ERROR_MESSAGE } from '@/constants/errorConstants' import axios from 'axios' export async function getSampleCCDAFiles(sampleCCDAFilesEndpoint: string) { @@ -29,7 +30,7 @@ export async function handleSendMessageWithAttachmentFilepath(prevState: object console.error(error.response?.data) return { response: { - error: 'They was an error connecting to the backend, Please try again later!', + error: GENERIC_ERROR_MESSAGE, errorStatus: error.response?.status, }, } @@ -60,7 +61,7 @@ export async function handleSendMessageWithAttachmentFile(prevState: object | un console.error(error.response?.data) return { response: { - error: 'They was an error connecting to the backend, Please try again later!', + error: GENERIC_ERROR_MESSAGE, errorStatus: error.response?.status, }, } @@ -91,7 +92,7 @@ export async function handleSearchSITEInbox(prevState: object | undefined, formD console.error(error.response?.data) return { response: { - error: 'They was an error connecting to the backend, Please try again later!', + error: GENERIC_ERROR_MESSAGE, errorStatus: error.response?.status, }, } @@ -120,7 +121,7 @@ export async function handleSearchHHSInbox(prevState: object | undefined, formDa console.error(error.response?.data) return { response: { - error: 'They was an error connecting to the backend, Please try again later!', + error: GENERIC_ERROR_MESSAGE, errorStatus: error.response?.status, }, } @@ -151,7 +152,7 @@ export async function handleUploadTrustAnchor(prevState: object | undefined, for console.error(error.response?.data) return { response: { - error: 'They was an error connecting to the backend, Please try again later!', + error: GENERIC_ERROR_MESSAGE, errorStatus: error.response?.status, }, } diff --git a/src/components/direct/validate/Validate.tsx b/src/components/direct/validators/DirectMessage.tsx similarity index 75% rename from src/components/direct/validate/Validate.tsx rename to src/components/direct/validators/DirectMessage.tsx index e49f5721..10a9ef33 100644 --- a/src/components/direct/validate/Validate.tsx +++ b/src/components/direct/validators/DirectMessage.tsx @@ -1,4 +1,4 @@ -'use client' +/* 'use client' import BannerBox from '@/components/shared/BannerBox' import styles from '@/components/shared/styles.module.css' import { Box, Button, Container, Stack, TextField, Tooltip, Typography } from '@mui/material' @@ -10,26 +10,7 @@ import palette from '@/styles/palette' const Validate = () => { return ( <> - {/* Global Header */} - - Direct - , - - Validate - , - ]} - heading={'Validate Direct Message'} - description={ - <> - Ensure that your direct messaging system complies with legal and regulatory requirements, including HIPAA - regulations in the United States. Stay updated with any changes in the regulations that might impact the way - direct messages need to be handled and secured. - - } - /> - {/* Main Content */} + @@ -87,3 +68,4 @@ const Validate = () => { } export default Validate + */ diff --git a/src/components/direct/validators/ValidatorHome.tsx b/src/components/direct/validators/ValidatorHome.tsx new file mode 100644 index 00000000..487702bf --- /dev/null +++ b/src/components/direct/validators/ValidatorHome.tsx @@ -0,0 +1,36 @@ +/* 'use client' +import BannerBox from '@/components/shared/BannerBox' +import Link from 'next/link' +import styles from '@shared/styles.module.css' +import TabsComponent, { TabInputs } from '@/components/shared/TabsComponent' +import DirectMessage from './DirectMessage' +import XDM from './XDMValidator' + +const ValidatorHome = () => { + const validatorTabs: TabInputs[] = [ + { tabName: 'XDM VALIDATOR', tabIndex: 0, tabPanel: }, + { tabName: 'DIRECT MESSAGE', tabIndex: 1, tabPanel: }, + ] + return ( + <> + + + Direct + , + + Message Validators + , + ]} + heading={'Message Validators '} + description={<>} + /> + + + + ) +} + +export default ValidatorHome + */ diff --git a/src/components/direct/validators/XDMResults.tsx b/src/components/direct/validators/XDMResults.tsx new file mode 100644 index 00000000..b8ea65f3 --- /dev/null +++ b/src/components/direct/validators/XDMResults.tsx @@ -0,0 +1,105 @@ +import { Box, Button, Divider, Typography } from '@mui/material' +import _ from 'lodash' + +import ErrorDisplayCard from '@/components/c-cda/validation/results/ErrorDisplay' +import { useFormStatus } from 'react-dom' +import { useState, useEffect } from 'react' +import { CircularProgress } from '@mui/material' +import palette from '@/styles/palette' +import { Check } from '@mui/icons-material' +import ErrorIcon from '@mui/icons-material/Error' +interface ResultsComponentProps { + response: ContentProps + disabled?: boolean + buttonTitle: string +} +type ContentProps = { + pass: boolean + report: string + uploadedTrustAnchorFileName: string + error?: string + errorStatus?: number +} +const XDMResults = ({ response, buttonTitle }: ResultsComponentProps) => { + const [errorOpen, setErrorOpen] = useState(false) + const { pending } = useFormStatus() + + const handleErrorClose = () => { + setErrorOpen(false) + } + + useEffect(() => { + if (!pending && _.has(response, 'error')) { + setErrorOpen(true) + } + }, [pending, response]) + + return ( + <> + + + {!pending && _.has(response, 'error') && ( + + )} + + {!pending && !_.has(response, 'error') && !_.isEmpty(response) && ( + + + + Validator Results + + {response.pass && ( + + + + Validation Passed + + +
{response.report}
+
+
+ )} + {!response.pass && ( + + + + Validation Failed + + +
{response.report}
+
+
+ )} +
+ )} + + ) +} + +export default XDMResults diff --git a/src/components/direct/validators/XDMValidator.tsx b/src/components/direct/validators/XDMValidator.tsx new file mode 100644 index 00000000..7ba4707c --- /dev/null +++ b/src/components/direct/validators/XDMValidator.tsx @@ -0,0 +1,73 @@ +'use client' +import BannerBox from '@/components/shared/BannerBox' +import styles from '@/components/shared/styles.module.css' +import { Box, Container, Stack, Typography } from '@mui/material' +import Link from 'next/link' +import DragandDropFile from '@components/shared/DragandDropFile' +import { handleXDMUpload } from './actions' +import { useFormState } from 'react-dom' +import { useEffect, useState } from 'react' +import XDMResults from './XDMResults' + +const XDM = () => { + const [base64, setBase64] = useState(null) + const [data, handleSubmit] = useFormState(handleXDMUpload, { response: {} }) + const [fileContent, setFileContent] = useState(null) + + const formData = new FormData() + formData.append('zip', base64 || '') + const handleSubmitwithBase64 = handleSubmit.bind(null, formData) + + useEffect(() => { + const reader = new FileReader() + reader.onloadend = () => { + const result = reader.result as string + const base64String = result.split(',')[1] // Extract base64 part + setBase64(base64String) + } + if (fileContent) { + reader.readAsDataURL(fileContent) + } + }, [fileContent]) + + const getFileName = (data: File[]) => { + if (data !== undefined) { + setFileContent(data[0]) + } + } + + return ( + <> + {/* Global Header */} + + Direct + , + + XDM Validator + , + ]} + heading={'XDM Validator'} + description={<>Upload your XDM file to validate.} + /> + + {/* Main Content */} + +
+ + + + Upload your XDM file: + + + + + + +
+ + ) +} + +export default XDM diff --git a/src/components/direct/validators/actions.ts b/src/components/direct/validators/actions.ts new file mode 100644 index 00000000..5d15fb49 --- /dev/null +++ b/src/components/direct/validators/actions.ts @@ -0,0 +1,48 @@ +'use server' +import axios from 'axios' +import _ from 'lodash' + +export async function handleXDMUpload(prevState: object | undefined, formData: FormData) { + const Api = process.env.XDM_VALIDATOR_API + const uploadFile = formData.get('zip') + // console.log('uploaded File', uploadFile) + if (_.isEmpty(uploadFile)) { + return { + response: { + error: 'Please upload a file', + errorStatus: 400, + }, + } + } else { + const body = JSON.stringify({ + zip: uploadFile, + }) + const config = { + method: 'post', + url: Api, + data: body, + headers: { + 'Content-Type': 'application/json', + }, + } + console.log('Submitted data XDM Validator ', config) + try { + const response = await axios.request(config) + console.log('XDM Validator response status', response.status) + //console.log('XDM Validator response status', response.data) + return { response: response.data } + } catch (error) { + if (axios.isAxiosError(error)) { + console.error(error.response?.data) + return { + response: { + error: 'There was an error completing the request, Please try again later!', + errorStatus: error.response?.status, + }, + } + } else { + console.error(error) + } + } + } +} diff --git a/src/components/direct/xdr-edge/SendTab.tsx b/src/components/direct/xdr-edge/SendTab.tsx new file mode 100644 index 00000000..ad6a34a5 --- /dev/null +++ b/src/components/direct/xdr-edge/SendTab.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react' +import SubTabsComponent, { TabInputs } from '../shared/SubTabs' +import Template from './send/TemplateTab' +import Content from './send/ContentTab' +import bulletedList from '../shared/BulletList' +import { Container, Typography, List, ListItem } from '@mui/material' + +const SendTab = () => { + const [selectedTab, setSelectedTab] = useState('Choose template') + + const sendTabs: TabInputs[] = [ + { tabName: 'Choose template', tabIndex: 0, tabPanel: