diff --git a/.env b/.env
index c27226af..94c96aee 100644
--- a/.env
+++ b/.env
@@ -7,5 +7,10 @@ NEXT_PUBLIC_CCDA_VALIDATOR_CURES_DOWNLOAD_URL=https://codeload.github.com/onc-he
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
-# TODO: Allow for this to be dynamic per environment file and delete from here. See: https://phase.dev/blog/nextjs-public-runtime-variables/
+# TODO: Allow for these to be dynamic per environment file and delete from here.
+# For now though, these are STATIC as this file is injected in the build,
+# and NEXT_PUBLIC env vars are not overridden by other env file definitions
+# See: https://phase.dev/blog/nextjs-public-runtime-variables/
+NEXT_PUBLIC_IS_DEBUG_MODE=false
+NEXT_PUBLIC_IS_EVENT_TRACKING=true
NEXT_PUBLIC_SCORECARD_SAVESCORECARDSERVICE_API=https://ccda.healthit.gov/scorecard/savescorecardservice
diff --git a/package-lock.json b/package-lock.json
index 29a67fa1..48cbc452 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,7 +21,7 @@
"dompurify": "^3.1.6",
"lodash": "^4.17.21",
"marked": "^12.0.2",
- "next": "14.2.3",
+ "next": "14.2.13",
"next-auth": "^4.24.7",
"nookies": "^2.5.2",
"react": "^18",
@@ -1048,9 +1048,9 @@
}
},
"node_modules/@next/env": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz",
- "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA=="
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.13.tgz",
+ "integrity": "sha512-s3lh6K8cbW1h5Nga7NNeXrbe0+2jIIYK9YaA9T7IufDWnZpozdFUp6Hf0d5rNWUKu4fEuSX2rCKlGjCrtylfDw=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "14.0.4",
@@ -1062,9 +1062,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz",
- "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==",
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.13.tgz",
+ "integrity": "sha512-IkAmQEa2Htq+wHACBxOsslt+jMoV3msvxCn0WFSfJSkv/scy+i/EukBKNad36grRxywaXUYJc9mxEGkeIs8Bzg==",
"cpu": [
"arm64"
],
@@ -1077,9 +1077,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz",
- "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==",
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.13.tgz",
+ "integrity": "sha512-Dv1RBGs2TTjkwEnFMVL5XIfJEavnLqqwYSD6LXgTPdEy/u6FlSrLBSSfe1pcfqhFEXRAgVL3Wpjibe5wXJzWog==",
"cpu": [
"x64"
],
@@ -1092,9 +1092,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz",
- "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==",
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.13.tgz",
+ "integrity": "sha512-yB1tYEFFqo4ZNWkwrJultbsw7NPAAxlPXURXioRl9SdW6aIefOLS+0TEsKrWBtbJ9moTDgU3HRILL6QBQnMevg==",
"cpu": [
"arm64"
],
@@ -1107,9 +1107,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz",
- "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==",
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.13.tgz",
+ "integrity": "sha512-v5jZ/FV/eHGoWhMKYrsAweQ7CWb8xsWGM/8m1mwwZQ/sutJjoFaXchwK4pX8NqwImILEvQmZWyb8pPTcP7htWg==",
"cpu": [
"arm64"
],
@@ -1122,9 +1122,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz",
- "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==",
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.13.tgz",
+ "integrity": "sha512-aVc7m4YL7ViiRv7SOXK3RplXzOEe/qQzRA5R2vpXboHABs3w8vtFslGTz+5tKiQzWUmTmBNVW0UQdhkKRORmGA==",
"cpu": [
"x64"
],
@@ -1137,9 +1137,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz",
- "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==",
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.13.tgz",
+ "integrity": "sha512-4wWY7/OsSaJOOKvMsu1Teylku7vKyTuocvDLTZQq0TYv9OjiYYWt63PiE1nTuZnqQ4RPvME7Xai+9enoiN0Wrg==",
"cpu": [
"x64"
],
@@ -1152,9 +1152,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz",
- "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==",
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.13.tgz",
+ "integrity": "sha512-uP1XkqCqV2NVH9+g2sC7qIw+w2tRbcMiXFEbMihkQ8B1+V6m28sshBwAB0SDmOe0u44ne1vFU66+gx/28RsBVQ==",
"cpu": [
"arm64"
],
@@ -1167,9 +1167,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz",
- "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==",
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.13.tgz",
+ "integrity": "sha512-V26ezyjPqQpDBV4lcWIh8B/QICQ4v+M5Bo9ykLN+sqeKKBxJVDpEc6biDVyluTXTC40f5IqCU0ttth7Es2ZuMw==",
"cpu": [
"ia32"
],
@@ -1182,9 +1182,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz",
- "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==",
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.13.tgz",
+ "integrity": "sha512-WwzOEAFBGhlDHE5Z73mNU8CO8mqMNLqaG+AO9ETmzdCQlJhVtWZnOl2+rqgVQS+YHunjOWptdFmNfbpwcUuEsw==",
"cpu": [
"x64"
],
@@ -6337,11 +6337,11 @@
"dev": true
},
"node_modules/next": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz",
- "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==",
+ "version": "14.2.13",
+ "resolved": "https://registry.npmjs.org/next/-/next-14.2.13.tgz",
+ "integrity": "sha512-BseY9YNw8QJSwLYD7hlZzl6QVDoSFHL/URN5K64kVEVpCsSOWeyjbIGK+dZUaRViHTaMQX8aqmnn0PHBbGZezg==",
"dependencies": {
- "@next/env": "14.2.3",
+ "@next/env": "14.2.13",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
@@ -6356,15 +6356,15 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "14.2.3",
- "@next/swc-darwin-x64": "14.2.3",
- "@next/swc-linux-arm64-gnu": "14.2.3",
- "@next/swc-linux-arm64-musl": "14.2.3",
- "@next/swc-linux-x64-gnu": "14.2.3",
- "@next/swc-linux-x64-musl": "14.2.3",
- "@next/swc-win32-arm64-msvc": "14.2.3",
- "@next/swc-win32-ia32-msvc": "14.2.3",
- "@next/swc-win32-x64-msvc": "14.2.3"
+ "@next/swc-darwin-arm64": "14.2.13",
+ "@next/swc-darwin-x64": "14.2.13",
+ "@next/swc-linux-arm64-gnu": "14.2.13",
+ "@next/swc-linux-arm64-musl": "14.2.13",
+ "@next/swc-linux-x64-gnu": "14.2.13",
+ "@next/swc-linux-x64-musl": "14.2.13",
+ "@next/swc-win32-arm64-msvc": "14.2.13",
+ "@next/swc-win32-ia32-msvc": "14.2.13",
+ "@next/swc-win32-x64-msvc": "14.2.13"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
diff --git a/package.json b/package.json
index f354e741..c3b36d7d 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"dompurify": "^3.1.6",
"lodash": "^4.17.21",
"marked": "^12.0.2",
- "next": "14.2.3",
+ "next": "14.2.13",
"next-auth": "^4.24.7",
"nookies": "^2.5.2",
"react": "^18",
diff --git a/public/certificates/xdr-tls/keyAndCert.zip b/public/certificates/xdr-tls/keyAndCert.zip
new file mode 100644
index 00000000..3923e70c
Binary files /dev/null and b/public/certificates/xdr-tls/keyAndCert.zip differ
diff --git a/public/shared/LogoBackgroundImage.png b/public/shared/LogoBackgroundImage.png
new file mode 100644
index 00000000..9853742e
Binary files /dev/null and b/public/shared/LogoBackgroundImage.png differ
diff --git a/public/shared/ONCLogo-backgroundImage.png b/public/shared/ONCLogo-backgroundImage.png
deleted file mode 100644
index f4a77fab..00000000
Binary files a/public/shared/ONCLogo-backgroundImage.png and /dev/null differ
diff --git a/public/shared/SITEWhiteLogo.svg b/public/shared/SITEWhiteLogo.svg
index 0e2ede48..6d6416e1 100644
--- a/public/shared/SITEWhiteLogo.svg
+++ b/public/shared/SITEWhiteLogo.svg
@@ -1,10 +1,36 @@
-
+
+
\ No newline at end of file
diff --git a/public/shared/site-nav-logo.svg b/public/shared/site-nav-logo.svg
index 2c0123c4..d41a71e9 100644
--- a/public/shared/site-nav-logo.svg
+++ b/public/shared/site-nav-logo.svg
@@ -1,9 +1,36 @@
-
+
+
\ No newline at end of file
diff --git a/src/app/account/changepassword/page.tsx b/src/app/account/changepassword/page.tsx
new file mode 100644
index 00000000..ecb2629e
--- /dev/null
+++ b/src/app/account/changepassword/page.tsx
@@ -0,0 +1,12 @@
+import React from 'react'
+import ChangePasswordHome from '@/components/account/ChangePasswordHome'
+
+const ChangePassword = () => {
+ return (
+ <>
+
+ >
+ )
+}
+
+export default ChangePassword
diff --git a/src/app/account/info/page.tsx b/src/app/account/info/page.tsx
new file mode 100644
index 00000000..e3f4ea88
--- /dev/null
+++ b/src/app/account/info/page.tsx
@@ -0,0 +1,12 @@
+import React from 'react'
+import InfoHome from '@/components/account/InfoHome'
+
+const AccountInfo = () => {
+ return (
+ <>
+
+ >
+ )
+}
+
+export default AccountInfo
diff --git a/src/assets/SMTPTestCases.tsx b/src/assets/SMTPTestCases.tsx
index e5707db1..ee3cbcfa 100644
--- a/src/assets/SMTPTestCases.tsx
+++ b/src/assets/SMTPTestCases.tsx
@@ -2468,6 +2468,7 @@ const testCases = {
sutHisp: true,
criteria: "['h2-6']",
sutEdge: false,
+ ccdaFileRequired: true,
fields: [
{
label: 'C-CDA Document Type',
@@ -2475,7 +2476,6 @@ const testCases = {
datatype: 'CCDAWidget',
value: 'ccdaReferenceFilename',
readOnly: false,
- ccdaFileRequired: true,
display: true,
},
],
diff --git a/src/components/account/ChangePasswordHome.tsx b/src/components/account/ChangePasswordHome.tsx
new file mode 100644
index 00000000..786be094
--- /dev/null
+++ b/src/components/account/ChangePasswordHome.tsx
@@ -0,0 +1,189 @@
+'use client'
+import PageAlertBox from '@/components/shared/PageAlertBox'
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ Container,
+ Divider,
+ Grid,
+ IconButton,
+ InputAdornment,
+ LinearProgress,
+ TextField,
+ Typography,
+ CardContent,
+} from '@mui/material'
+import { useSession } from 'next-auth/react'
+import { useState } from 'react'
+import { changePassword } from './actions'
+import _ from 'lodash'
+import { VisibilityOff, Visibility } from '@mui/icons-material'
+
+const LoginButtonStyle = {
+ padding: '10px 0',
+ width: '100%',
+ margin: 1,
+}
+
+const ChangePasswordHome = () => {
+ const { data: session, status } = useSession()
+ const [isLoading, setIsLoading] = useState(false)
+ const [oldPassword, setOldPassword] = useState('')
+ const [password, setPassword] = useState('')
+ const [repeatPassword, setRepeatPassword] = useState('')
+ const [showPassword, setShowPassword] = useState(false)
+ const [message, setMessage] = useState({ message: '', severity: 'info' })
+
+ const handleChangePassword = async (e: React.FormEvent) => {
+ setIsLoading(true)
+ e.preventDefault()
+ if (password === repeatPassword) {
+ changePassword(oldPassword, password).then((data) => {
+ if (data === true) {
+ setMessage({ message: 'Password updated', severity: 'success' })
+ setOldPassword('')
+ setPassword('')
+ setRepeatPassword('')
+ } else {
+ setMessage({ message: `${data}`, severity: 'error' })
+ setOldPassword('')
+ setPassword('')
+ setRepeatPassword('')
+ }
+ })
+ } else {
+ setMessage({ message: 'Passwords do not match', severity: 'error' })
+ setOldPassword('')
+ setPassword('')
+ setRepeatPassword('')
+ }
+ setIsLoading(false)
+ }
+
+ const handleClickShowPassword = () => {
+ setShowPassword((prev) => !prev)
+ }
+
+ const ChangePasswordGrid = (
+ handleChangePassword(e)} sx={{ mt: 5 }}>
+
+
+ setOldPassword(e.target.value)}
+ InputProps={{
+ endAdornment: (
+
+
+ {showPassword ? : }
+
+
+ ),
+ }}
+ />
+
+
+ setPassword(e.target.value)}
+ InputProps={{
+ endAdornment: (
+
+
+ {showPassword ? : }
+
+
+ ),
+ }}
+ />
+
+
+ setRepeatPassword(e.target.value)}
+ InputProps={{
+ endAdornment: (
+
+
+ {showPassword ? : }
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+
+
+ )
+
+ return status !== 'authenticated' ? (
+
+
+
+ ) : (
+ <>
+ {isLoading ? (
+
+ ) : (
+ <>
+
+
+
+
+ Change Password
+
+
+
+ To keep your account secure, please enter your current password and your new password. Make sure your
+ new password includes a mix of letters, numbers, and symbols.
+
+
+ Choose a password that you haven't used before. Once you've filled in the fields, click{' '}
+ "Save" to update your password. If you need assistance, please contact our
+ support team. Stay secure!
+
+ {ChangePasswordGrid}
+
+
+ {!_.isEmpty(message.message) && (
+ setMessage({ message: '', severity: 'info' })}
+ >
+ {message.message}
+
+ )}
+
+ >
+ )}
+ >
+ )
+}
+
+export default ChangePasswordHome
diff --git a/src/components/account/InfoHome.tsx b/src/components/account/InfoHome.tsx
new file mode 100644
index 00000000..9c304bee
--- /dev/null
+++ b/src/components/account/InfoHome.tsx
@@ -0,0 +1,108 @@
+'use client'
+import PageAlertBox from '@/components/shared/PageAlertBox'
+import { Box, Card, Container, Divider, LinearProgress, List, ListItem, ListItemText, Typography } from '@mui/material'
+import { useSession } from 'next-auth/react'
+import { useEffect, useState } from 'react'
+import { fetchAccountInfo } from './actions'
+import Profile from '../direct/shared/Profile'
+import BannerBox from '../shared/BannerBox'
+import { AccountCircleOutlined, EmailOutlined } from '@mui/icons-material'
+
+const InfoHome = () => {
+ const { data: session, status } = useSession()
+ const [directList, setDirectList] = useState([])
+ const [smtpProfiles, setSmtpProfiles] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+
+ useEffect(() => {
+ async function fetchLoggedInUsersAccountInfo() {
+ const usersAccountInfo = await fetchAccountInfo()
+ if (usersAccountInfo && typeof usersAccountInfo !== 'string') {
+ setDirectList(usersAccountInfo.directList || [])
+ setSmtpProfiles(usersAccountInfo.smtpProfiles || [])
+ }
+ }
+ if (status === 'authenticated') {
+ setIsLoading(true)
+ fetchLoggedInUsersAccountInfo().then(() => setIsLoading(false))
+ }
+ }, [session, status])
+
+ return status !== 'authenticated' ? (
+
+
+
+ ) : (
+ <>
+
+ Welcome to your Account Information page! Here, you can view your direct email address and SMTP account
+ details. Ensure these settings are correct for optimal email delivery. Please review this information
+ regularly to keep your account functioning smoothly. If you have any questions, feel free to reach out to
+ our support team.
+ >
+ }
+ />
+
+ {isLoading ? (
+
+ ) : (
+
+
+ My Account Info
+
+
+
+
+
+
+ My Direct Email Addresses
+
+
+
+ {directList.map((directEmail, index) => {
+ return (
+
+
+
+ )
+ })}
+
+
+
+
+
+
+ My SMTP Profiles
+
+
+
+ {smtpProfiles.map((profile, index) => {
+ return (
+
+
+
+ )
+ })}
+
+
+
+
+ )}
+
+ >
+ )
+}
+
+export default InfoHome
diff --git a/src/components/account/actions.ts b/src/components/account/actions.ts
new file mode 100644
index 00000000..7874ee18
--- /dev/null
+++ b/src/components/account/actions.ts
@@ -0,0 +1,120 @@
+'use server'
+import { authOptions } from '@/lib/auth'
+import { getServerSession } from 'next-auth'
+import Profile from '../direct/shared/Profile'
+
+const ETT_API_URL = process.env.ETT_API_URL
+
+export interface AccountInfo {
+ smtpProfiles?: Profile[]
+ directList?: string[]
+}
+
+export async function changePassword(oldPassword: string, newPassword: string) {
+ const session = await getServerSession(authOptions)
+ const jsessionid = session?.user?.jsessionid ?? ''
+ const ettAPIUrl = `${ETT_API_URL}/passwordManager/change`
+ try {
+ const response = await fetch(ettAPIUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Cookie: `JSESSIONID=${jsessionid}`,
+ },
+ body: JSON.stringify({ newPassword: newPassword, oldPassword: oldPassword }),
+ })
+ const data = await response.json()
+ if (!response.ok) {
+ console.log(`Error: ${JSON.stringify(data)}`)
+ return data.message
+ }
+ return data
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ return JSON.stringify({
+ status: 500,
+ success: false,
+ error: error.message || 'An error occurred',
+ })
+ }
+ }
+}
+
+export async function fetchAccountInfo() {
+ const session = await getServerSession(authOptions)
+ const jsessionid = session?.user?.jsessionid ?? ''
+ try {
+ const accountInfo: AccountInfo = {}
+ return Promise.allSettled([fetchProfiles(jsessionid), fetchDirectEmails(jsessionid)]).then(
+ ([profiles, directEmails]) => {
+ if (profiles.status === 'fulfilled') {
+ accountInfo.smtpProfiles = profiles.value.filter((profile: Profile) => profile.profileName !== null)
+ }
+ if (directEmails.status === 'fulfilled') {
+ accountInfo.directList = directEmails.value
+ }
+ return accountInfo
+ }
+ )
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ return JSON.stringify({
+ status: 500,
+ success: false,
+ error: error.message || 'An error occurred',
+ })
+ }
+ }
+}
+
+async function fetchProfiles(jsessionid: string) {
+ const ettAPIUrl = `${ETT_API_URL}/smtpProfile`
+ try {
+ const response = await fetch(ettAPIUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Cookie: `JSESSIONID=${jsessionid}`,
+ },
+ })
+ const data = await response.json()
+ if (!response.ok) {
+ console.log(`Error: ${data}`)
+ }
+ return data
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ return JSON.stringify({
+ status: 500,
+ success: false,
+ error: error.message || 'An error occurred',
+ })
+ }
+ }
+}
+
+async function fetchDirectEmails(jsessionid: string) {
+ const ettAPIUrl = `${ETT_API_URL}/registration/direct`
+ try {
+ const response = await fetch(ettAPIUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Cookie: `JSESSIONID=${jsessionid}`,
+ },
+ })
+ const data = await response.json()
+ if (!response.ok) {
+ console.log(`Error: ${data}`)
+ }
+ return data
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ return JSON.stringify({
+ status: 500,
+ success: false,
+ error: error.message || 'An error occurred',
+ })
+ }
+ }
+}
diff --git a/src/components/archived/ArchiveHome.tsx b/src/components/archived/ArchiveHome.tsx
index 27c0c33e..2025394d 100644
--- a/src/components/archived/ArchiveHome.tsx
+++ b/src/components/archived/ArchiveHome.tsx
@@ -8,6 +8,7 @@ import FAQCard from './ArchiveCard'
import faq from './data/FAQ.json'
import VideoItem from '../resources/VideoItem'
import ForwardToInboxOutlinedIcon from '@mui/icons-material/ForwardToInboxOutlined'
+import eventTrack from '@/services/analytics'
export default function ArchiveHome() {
const menuItems: menuProps[] = [
@@ -26,13 +27,18 @@ export default function ArchiveHome() {
icon: ,
},
]
+
function trackMenuItemClick(heading: string) {
- if (typeof window.gtag === 'function') {
- window.gtag('event', 'Click FAQs sub menu', {
- event_category: 'Navigation',
- event_label: heading,
- })
- }
+ eventTrack('Sub Menu Anchor Link Click', 'Archived', `Navigated to ${heading} via sub menu in Archived page`)
+ // TODO: Decided if the new format for eventType, eventCategory, and eventLabel is appropriate.
+ // Remove the following code either way, but, if new format is not preferred,
+ // copy the old string labels into event track before deletion
+ // if (typeof window.gtag === 'function') {
+ // window.gtag('event', 'Click FAQs sub menu', {
+ // event_category: 'Navigation',
+ // event_label: heading,
+ // })
+ // }
}
return (
diff --git a/src/components/c-cda/scorecard/ScorecardHome.tsx b/src/components/c-cda/scorecard/ScorecardHome.tsx
index 84312674..50214091 100644
--- a/src/components/c-cda/scorecard/ScorecardHome.tsx
+++ b/src/components/c-cda/scorecard/ScorecardHome.tsx
@@ -29,9 +29,13 @@ import BannerBox from '@shared/BannerBox'
import SectionHeader from '@shared/SectionHeader'
import styles from '@shared/styles.module.css'
import Link from 'next/link'
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
+import { useFormState } from 'react-dom'
+import ErrorDisplayCard from '../validation/results/ErrorDisplay'
+import ValidatorLoadingCard from '../validation/results/ResultsLoading'
import ScorecardResultsDialog from './ScorecardResultsDialog'
-import { getDemoSample } from './serverside/demoSampleService'
+import { postToScorecardForValidation } from './serverside/actions'
+import { allSampleOptions, allSampleOptionsExceptDebug, getDemoSample } from './serverside/demoSampleService'
import {
getDefaultReferenceResult,
getFailingSectionSpecificErrorCount,
@@ -45,80 +49,18 @@ import {
SectionNameEnum,
SORT_ORDER_STARTING_VALUE,
} from './types/ScorecardConstants'
-import ErrorDisplayCard from '../validation/results/ErrorDisplay'
+import eventTrack from '@/services/analytics'
export default function ScorecardHome() {
const [resultsDialogState, setResultsDialogState] = useState(false)
const handleCloseResultsDialog = () => {
setResultsDialogState(false)
}
- const [isTryMeDemo, setIsTryMeDemo] = useState(false)
- const [scorecardHomeError, setScorecardHomeError] = useState('')
-
- const demoSampleOptions: { label: string; value: string }[] = [
- {
- label: 'High Scoring Sample',
- value: 'highScoringSample.json',
- },
- {
- label: 'Low Scoring Sample (C-CDA R2.1)',
- value: 'lowScoringSample_r21.json',
- },
- {
- label: 'Low Scoring Sample (C-CDA R1.0)',
- value: 'lowScoringSample_r11.json',
- },
- {
- label: 'Sample With Errors',
- value: 'sampleWithErrors.json',
- },
- ]
+ const [isTryMeDemo, setIsTryMeDemo] = useState(false)
- const newDemoSampleOptions: { label: string; value: string }[] = [
- {
- label: 'Sample with IG Errors',
- value: 'sampleWithIGErrors.json',
- },
- {
- label: 'Sample with Vocabulary Errors',
- value: 'sampleWithVocabularyErrors.json',
- },
- {
- label: 'Sample with Empty Sections',
- value: 'sampleWithEmptySections.json',
- },
- {
- label: 'Sample with Empty Sections and Errors',
- value: 'sampleWithEmptySectionsAndErrors.json',
- },
- ]
- demoSampleOptions.push(...newDemoSampleOptions)
-
- const debugSampleOptions: { label: string; value: string }[] = [
- {
- label: 'Schema Errors',
- value: 'sampleWithSchemaErrors.json',
- },
- {
- label: 'No Content',
- value: 'sampleWithoutAnyContent.json',
- },
- {
- label: 'SITE 3 High Scoring Sample',
- value: 'site3-highScoringSample.json',
- },
- {
- label: 'SITE 3 Low Scoring Sample',
- value: 'site3-lowScoringSample.json',
- },
- {
- label: 'SITE 3 Sample With Errors',
- value: 'site3-sampleWithErrors.json',
- },
- ]
- // TODO: Tie this to a debug mode env var (if true, push, otherwise maybe don't as may not want in production)
- demoSampleOptions.push(...debugSampleOptions)
+ const IS_DEBUG_MODE: boolean = process.env.NEXT_PUBLIC_IS_DEBUG_MODE === 'true'
+ const demoSampleOptions = IS_DEBUG_MODE ? allSampleOptions : allSampleOptionsExceptDebug
const [demoSampleOption, setDemoSampleOption] = useState(demoSampleOptions[0].value)
const [scorecardResponseJson, setScorecardResponseJson] = useState()
@@ -130,20 +72,68 @@ export default function ScorecardHome() {
getDefaultReferenceResult(ReferenceInstanceEnum.VOCAB)
)
+ const [scorecardHomeError, setScorecardHomeError] = useState('')
+ const [isDisableStartButton, setIsDisableStartButton] = useState(true)
+ const formRef = useRef(null)
+ const [formState, formAction] = useFormState(postToScorecardForValidation, { response: null })
+ const [fileName, setFileName] = useState('')
+ // TODO: Consider setting this based off of file size?
+ // Right now, it is the regular validator's estimate for IG + Vocab (15), just like this, but it adds best practice
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [estimatedValidationTime, setEstimatedValidationTime] = useState(20)
+ const [isValidating, setIsValidating] = useState(false)
+
+ /* Set API response when updated */
useEffect(() => {
- if (scorecardResponseJson) {
- console.log('Updated scorecardResponseJson:', scorecardResponseJson)
+ if (formState) {
+ if (formState.error) {
+ setScorecardHomeError(
+ formState.error + (formState.errorStatus ? ` Status Number: ${formState.errorStatus}` : '')
+ )
+ setIsValidating(false)
+ } else if (formState.response) {
+ const newScorecardResponseJson: ScorecardJsonResponseType = formState.response
+ setScorecardResponseJson(newScorecardResponseJson)
+ }
}
- if (scResults) {
- console.log('Updated scResults:', scResults)
+ }, [formState])
+
+ /* Handle results display after API call results returned */
+ useEffect(() => {
+ if (scorecardResponseJson) {
+ const [isValidResults, errorMessage] = processResults(scorecardResponseJson)
+ setIsValidating(false)
+ displayResults(isValidResults, errorMessage)
}
- if (igResults) {
- console.log('Updated igResults', igResults)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [scorecardResponseJson])
+
+ /* Only enable START button if file is selected */
+ useEffect(() => {
+ if (!fileName) {
+ setIsDisableStartButton(true)
+ } else {
+ setIsDisableStartButton(false)
}
- if (vocabResults) {
- console.log('Updated vocabResults', vocabResults)
+ }, [fileName])
+
+ /* Debug logs */
+ useEffect(() => {
+ if (IS_DEBUG_MODE) {
+ if (scorecardResponseJson) {
+ console.log('Updated scorecardResponseJson:', scorecardResponseJson)
+ }
+ if (scResults) {
+ console.log('Updated scResults:', scResults)
+ }
+ if (igResults) {
+ console.log('Updated igResults', igResults)
+ }
+ if (vocabResults) {
+ console.log('Updated vocabResults', vocabResults)
+ }
}
- }, [scorecardResponseJson, scResults, igResults, vocabResults])
+ }, [scorecardResponseJson, scResults, igResults, vocabResults, IS_DEBUG_MODE])
const handleDemoSampleChange = (e: SelectChangeEvent) => {
console.log('handleDemoSampleChange(e), event:', e)
@@ -151,12 +141,7 @@ export default function ScorecardHome() {
console.log(`Selected ${demoSampleSelected}`)
setDemoSampleOption(demoSampleSelected)
- if (typeof window.gtag === 'function') {
- window.gtag('event', 'Select demo dropdown', {
- event_category: 'dropdown',
- event_label: `Selected ${demoSampleSelected}`,
- })
- }
+ eventTrack('Dropdown Selection', 'Scorecard', `Selected ${demoSampleSelected} sample within Try Me Demo dropdown`)
}
const handleSubmitDemoStart = (e: React.FormEvent) => {
@@ -164,12 +149,11 @@ export default function ScorecardHome() {
console.log('handleSubmitDemoStart(e), event: ', e)
console.log('Starting demo with sample: ' + demoSampleOption)
+ setIsTryMeDemo(true)
+
try {
const newScorecardResponseJson: ScorecardJsonResponseType = getDemoSample(demoSampleOption)
setScorecardResponseJson(newScorecardResponseJson)
-
- const [isValidResults, errorMessage]: [boolean, string | null] = processResults(newScorecardResponseJson)
- displayResults(isValidResults, errorMessage, true)
} catch (error) {
const errorMessagePrefix = 'Error running Scorecard Demo'
console.error(
@@ -181,29 +165,59 @@ export default function ScorecardHome() {
${error}. Please try again later.`)
}
- if (typeof window.gtag === 'function') {
- window.gtag('event', 'Try Me', {
- event_category: 'Button',
- event_label: 'Score card try me demo',
- })
+ eventTrack('Button Click Form Submission', 'Scorecard', 'Run the Try Me Demo with selected file and view results')
+ }
+
+ const getFileName = (data: File[]) => {
+ console.log(data[0]?.name)
+ if (data) {
+ setFileName(data[0]?.name)
+ } else {
+ console.log('SC Filename is undefined...')
}
}
+ const resetResultsData = () => {
+ setScorecardResponseJson(undefined)
+ setScResults(undefined)
+ setIgResults(getDefaultReferenceResult(ReferenceInstanceEnum.IG_CONFORMANCE))
+ setVocabResults(getDefaultReferenceResult(ReferenceInstanceEnum.VOCAB))
+ setIsTryMeDemo(false)
+ }
+
const handleSubmitScorecardStart = (e: React.FormEvent) => {
e.preventDefault()
console.log('handleSubmitScorecardStart(e), event: ', e)
+ console.log('Starting C-CDA Scorecard validation with submitted C-CDA file: ' + fileName)
+
+ resetResultsData()
+ setIsValidating(true)
- // TODO: Support POST response from API
- // const newScorecardResponseJson: ScorecardJsonResponseType = POST response from API
- // setScorecardResponseJson(newScorecardResponseJson)
- //
- // const [isValidResults, errorMessage]: [boolean, string | null] = processResults(newScorecardResponseJson)
- // displayResults(isValidResults, errorMessage, false)
+ try {
+ if (formRef.current) {
+ const formData = new FormData(formRef.current)
+ formAction(formData)
+ if (formState.error) {
+ throw new Error(formState.error)
+ }
+ }
+ } catch (error) {
+ const errorMessagePrefix = 'Error running C-CDA Scorecard validation'
+ console.error(
+ `${errorMessagePrefix} in handleSubmitScorecardStart():
+ Failed to run C-CDA Scorecard validation in handleSubmitScorecardStart(), unable to load submitted file: `,
+ error
+ )
+ setScorecardHomeError(`${errorMessagePrefix}: ${error instanceof Error ? error.message : String(error)}.`)
+ setIsValidating(false)
+ }
+
+ eventTrack('Button Click Form Submission', 'Scorecard', 'Run SC validation with selected file and view results')
}
const processResults = (newJson: ScorecardJsonResponseType): [boolean, string | null] => {
if (newJson) {
- if (newJson.success == false) {
+ if (!newJson.success) {
// Handle valid JSON but with an error returned from the server
const error = newJson.errorMessage
const file = newJson.filename
@@ -302,7 +316,7 @@ export default function ScorecardHome() {
*/
const sortResultsOrderByGradeTypeAndNumberOfIssues = (
results: ScorecardResultsType | undefined,
- isisAscending: boolean
+ isAscending: boolean
) => {
if (results?.categoryList) {
const gradeOrder: { [key: string]: number } = {
@@ -320,20 +334,20 @@ export default function ScorecardHome() {
const nullFlavorNIComparison: number = compareNullFlavorNI(a, b)
if (nullFlavorNIComparison !== 0) return nullFlavorNIComparison
- const conformanceComparison: number = compareConformance(a, b, isisAscending)
+ const conformanceComparison: number = compareConformance(a, b, isAscending)
if (conformanceComparison !== 0) return conformanceComparison
- const vocabularyComparison: number = compareVocabulary(a, b, isisAscending)
+ const vocabularyComparison: number = compareVocabulary(a, b, isAscending)
if (vocabularyComparison !== 0) return vocabularyComparison
const gradeComparison: number = compareGrades(
gradeOrder,
- isisAscending ? (b.categoryGrade as GradeEnum) : (a.categoryGrade as GradeEnum),
- isisAscending ? (a.categoryGrade as GradeEnum) : (b.categoryGrade as GradeEnum)
+ isAscending ? (b.categoryGrade as GradeEnum) : (a.categoryGrade as GradeEnum),
+ isAscending ? (a.categoryGrade as GradeEnum) : (b.categoryGrade as GradeEnum)
)
if (gradeComparison !== 0) return gradeComparison
- const numberOfIssuesComparison: number = compareNumberOfIssues(a, b, isisAscending)
+ const numberOfIssuesComparison: number = compareNumberOfIssues(a, b, isAscending)
return numberOfIssuesComparison // No check for 0 on last comparison because we have to return something
})
@@ -382,9 +396,8 @@ export default function ScorecardHome() {
: (a.numberOfIssues ?? 0) - (b.numberOfIssues ?? 0)
}
- const displayResults = (isValidResults: boolean, errorMessage: string | null, isTryMeButtonClick: boolean) => {
+ const displayResults = (isValidResults: boolean, errorMessage: string | null) => {
if (isValidResults) {
- setIsTryMeDemo(isTryMeButtonClick)
setResultsDialogState(true)
} else {
const finalErrorMessage = `Error: ${errorMessage ? errorMessage : 'Unknown error message'} `
@@ -393,6 +406,7 @@ export default function ScorecardHome() {
}
}
+ // TODO: Separate out as much modal data and logic as possible into it's own component for cleaner code
const modalUrls = [
'https://raw.githubusercontent.com/onc-healthit/site-content/master/CCDAScorecardIntroduction.md',
'https://raw.githubusercontent.com/onc-healthit/site-content/master/CCDAScorecardResultsInterpretation.md',
@@ -443,9 +457,9 @@ export default function ScorecardHome() {
{/* Main Content */}
- {/* Actual Scorecard Validation */}
-
+ {/* Actual Scorecard Validation */}
+
-
{/* Scorecard User File Upload */}
-
+
-
{/* Scorecard Validation Submit */}
-
@@ -583,6 +595,10 @@ export default function ScorecardHome() {
+ {isValidating && (
+
+ )}
+
{
const demoSample = demoSampleMap[optionValue]
+ console.log('Value of demoSample: ', demoSample)
if (!demoSample) {
throw new Error('Invalid option value selected or sent from the Scorecard Try Me demo dropdown')
}
return demoSample
}
+
+const currentDemoSampleOptions: { label: string; value: string }[] = [
+ {
+ label: 'High Scoring Sample',
+ value: HIGH_SCORING,
+ },
+ {
+ label: 'Low Scoring Sample (C-CDA R2.1)',
+ value: LOW_SCORING,
+ },
+ {
+ label: 'Low Scoring Sample (C-CDA R1.0)',
+ value: LOW_SCORING_R11,
+ },
+ {
+ label: 'Sample With Errors',
+ value: WITH_ERRORS,
+ },
+]
+
+const newDemoSampleOptions: { label: string; value: string }[] = [
+ {
+ label: 'Sample with IG Errors',
+ value: WITH_IG_ERRORS,
+ },
+ {
+ label: 'Sample with Vocabulary Errors',
+ value: WITH_VOCAB_ERRORS,
+ },
+ {
+ label: 'Sample with Empty Sections',
+ value: WITH_EMPTY_SECTIONS,
+ },
+ {
+ label: 'Sample with Empty Sections and Errors',
+ value: WITH_EMPTY_SECTIONS_AND_ERRORS,
+ },
+]
+
+const oldDemoSampleOptions: { label: string; value: string }[] = [
+ {
+ label: 'SITE 3 High Scoring Sample',
+ value: SITE3_HIGH_SCORING,
+ },
+ {
+ label: 'SITE 3 Low Scoring Sample',
+ value: SITE3_LOW_SCORING,
+ },
+ {
+ label: 'SITE 3 Sample With Errors',
+ value: SITE3_WITH_ERRORS,
+ },
+]
+
+export const debugSampleOptions: { label: string; value: string }[] = [
+ {
+ label: 'Schema Errors',
+ value: WITH_SCHEMA_ERRORS,
+ },
+ {
+ label: 'No Content',
+ value: WITHOUT_CONTENT,
+ },
+]
+
+export const allSampleOptionsExceptDebug: { label: string; value: string }[] = [
+ ...currentDemoSampleOptions,
+ ...newDemoSampleOptions,
+ ...oldDemoSampleOptions,
+]
+
+export const allSampleOptions: { label: string; value: string }[] = [
+ ...currentDemoSampleOptions,
+ ...newDemoSampleOptions,
+ ...oldDemoSampleOptions,
+ ...debugSampleOptions,
+]
diff --git a/src/components/c-cda/scorecard/summary/ScorecardCompareChartSummary.tsx b/src/components/c-cda/scorecard/summary/ScorecardCompareChartSummary.tsx
index f28df6f9..ec48044b 100644
--- a/src/components/c-cda/scorecard/summary/ScorecardCompareChartSummary.tsx
+++ b/src/components/c-cda/scorecard/summary/ScorecardCompareChartSummary.tsx
@@ -1,4 +1,4 @@
-import SwitchWithLabel from '@/components/shared/SwitchWIthLabel'
+import SwitchWithLabel from '@/components/shared/SwitchWithLabel'
import { Box, Typography } from '@mui/material'
import { BarChart } from '@mui/x-charts/BarChart'
import { useState } from 'react'
diff --git a/src/components/c-cda/scorecard/types/ScorecardJsonResponseType.ts b/src/components/c-cda/scorecard/types/ScorecardJsonResponseType.ts
index 281b3ec3..2d617937 100644
--- a/src/components/c-cda/scorecard/types/ScorecardJsonResponseType.ts
+++ b/src/components/c-cda/scorecard/types/ScorecardJsonResponseType.ts
@@ -4,7 +4,6 @@ export interface ScorecardJsonResponseType {
ccdaDocumentType: string | null
results: ScorecardResultsType | null
referenceResults: ScorecardReferenceResultType[] | []
- // referenceResults: ScorecardReferenceResultType[]
schemaErrorList: SchemaErrorList[] | null
schemaErrors: boolean
success: boolean
diff --git a/src/components/c-cda/validation/ValidatorForm.tsx b/src/components/c-cda/validation/ValidatorForm.tsx
index b794a781..e5ac1ae5 100644
--- a/src/components/c-cda/validation/ValidatorForm.tsx
+++ b/src/components/c-cda/validation/ValidatorForm.tsx
@@ -202,6 +202,7 @@ export default function ValidatorForm({
{/* Buttons */}
+ {/* ValidationComponent returns the validate button, a loading dialog, and the validation results */}
void
+ handleClose?: () => void
estimatedValidationTime: number
fileName: string
}
const ValidatorLoadingCard: FC = ({
open,
+ // TODO: Implement handleClose to maybe cancel the API call, or in some other way, or remove it
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
handleClose,
estimatedValidationTime,
fileName,
diff --git a/src/components/c-cda/validation/results/ValidationResultsSummary.tsx b/src/components/c-cda/validation/results/ValidationResultsSummary.tsx
index 3f49bdd6..f17a5507 100644
--- a/src/components/c-cda/validation/results/ValidationResultsSummary.tsx
+++ b/src/components/c-cda/validation/results/ValidationResultsSummary.tsx
@@ -107,7 +107,6 @@ const ValidationResults = ({
const ccdaMDHTConformanceValidationResults = ccdaValidationResults.filter((result) =>
result?.type.includes('C-CDA MDHT Conformance')
)
-
const sccVocabularyValidationResults = ccdaValidationResults.filter((result) =>
result?.type.includes('S&CC Vocabulary Validation Conformance')
)
diff --git a/src/components/direct/dcdt/DCDTCertificates.tsx b/src/components/direct/dcdt/DCDTCertificates.tsx
index d321a5fc..c5bf1808 100644
--- a/src/components/direct/dcdt/DCDTCertificates.tsx
+++ b/src/components/direct/dcdt/DCDTCertificates.tsx
@@ -57,7 +57,9 @@ const DCDTCertificates = () => {
{"Download the Testing Tool's trust anchor."}{' '}
- Download Trust Anchor
+ Download Trust Anchor
diff --git a/src/components/direct/hisp/HISPPortal.tsx b/src/components/direct/hisp/HISPPortal.tsx
index 83f6c73e..9592003f 100644
--- a/src/components/direct/hisp/HISPPortal.tsx
+++ b/src/components/direct/hisp/HISPPortal.tsx
@@ -9,6 +9,7 @@ import IMAP from './IMAPTab'
import POP3 from './POP3Tab'
import XDR from './XDRTab'
import ValidationResults from './ValidationResultsTab'
+import ProfileProvider from './provider'
const HISPPortal = () => {
const hispTabs: TabInputs[] = [
@@ -41,7 +42,9 @@ const HISPPortal = () => {
}
/>
{/* Main Content */}
-
+
+
+
>
)
}
diff --git a/src/components/direct/hisp/IMAPTab.tsx b/src/components/direct/hisp/IMAPTab.tsx
index 1b91b10e..f3bacdd0 100644
--- a/src/components/direct/hisp/IMAPTab.tsx
+++ b/src/components/direct/hisp/IMAPTab.tsx
@@ -17,12 +17,15 @@ import * as React from 'react'
import testCases from '@/assets/SMTPTestCases'
import _ from 'lodash'
import TestFilter from './TestFilter'
+import { useContext } from 'react'
+import { ProfileContext } from './context'
const IMAP = () => {
const [option, setOption] = React.useState('')
const imapTestCases = testCases.tests.filter((test) => test.protocol === 'imap')
const imapTestCasesSender = imapTestCases.filter((test) => test.sutRole === 'sender' && test.sutHisp)
const imapTestCasesReceiver = imapTestCases.filter((test) => test.sutRole === 'receiver' && test.sutHisp)
+ const { hostname, email, password, tls, username } = useContext(ProfileContext)
const handleChange = (event: SelectChangeEvent) => {
setOption(event.target.value as string)
}
@@ -45,7 +48,7 @@ const IMAP = () => {
Select sender or receiver for your system & fill in the additional fields to get started.{' '}
-
+
@@ -86,7 +89,14 @@ const IMAP = () => {
{imapTestCasesSender.map((test, i) => {
return (
-
+
)
})}
@@ -98,7 +108,15 @@ const IMAP = () => {
{imapTestCasesReceiver.map((test, i) => {
return (
-
+
)
})}
diff --git a/src/components/direct/hisp/MessageTrackingTab.tsx b/src/components/direct/hisp/MessageTrackingTab.tsx
index d57e1b6e..febae4e0 100644
--- a/src/components/direct/hisp/MessageTrackingTab.tsx
+++ b/src/components/direct/hisp/MessageTrackingTab.tsx
@@ -20,12 +20,15 @@ import testCases from '@/assets/SMTPTestCases'
import DragandDropFile from '@/components/shared/DragandDropFile'
import HelpIcon from '@mui/icons-material/Help'
import TestFilter from './TestFilter'
+import { useContext } from 'react'
+import { ProfileContext } from './context'
const MessageTracking = () => {
const [option, setOption] = React.useState('')
const mu2TestCases = testCases.tests.filter((test) => test.protocol === 'mu2')
const mu2TestCasesSender = mu2TestCases.filter((test) => test.sutRole === 'sender' && test.sutHisp)
const mu2TestCasesReceiver = mu2TestCases.filter((test) => test.sutRole === 'receiver' && test.sutHisp)
+ const { hostname, email, password, tls, username } = useContext(ProfileContext)
const handleChange = (event: SelectChangeEvent) => {
setOption(event.target.value as string)
}
@@ -48,7 +51,7 @@ const MessageTracking = () => {
Select sender or receiver for your system & fill in the additional fields to get started.{' '}
-
+
@@ -102,7 +105,14 @@ const MessageTracking = () => {
{mu2TestCasesSender.map((test, i) => {
return (
-
+
)
})}
@@ -113,7 +123,15 @@ const MessageTracking = () => {
{mu2TestCasesReceiver.map((test, i) => {
return (
-
+
)
})}
diff --git a/src/components/direct/hisp/POP3Tab.tsx b/src/components/direct/hisp/POP3Tab.tsx
index 55ff0709..595140e5 100644
--- a/src/components/direct/hisp/POP3Tab.tsx
+++ b/src/components/direct/hisp/POP3Tab.tsx
@@ -17,12 +17,16 @@ import * as React from 'react'
import testCases from '@/assets/SMTPTestCases'
import _ from 'lodash'
import TestFilter from './TestFilter'
+import { useContext } from 'react'
+import { ProfileContext } from './context'
const POP3 = () => {
const [option, setOption] = React.useState('')
const popTestCases = testCases.tests.filter((test) => test.protocol === 'pop')
const popTestCasesSender = popTestCases.filter((test) => test.sutRole === 'sender' && test.sutHisp)
const popTestCasesReceiver = popTestCases.filter((test) => test.sutRole === 'receiver' && test.sutHisp)
+ const { hostname, email, password, tls, username } = useContext(ProfileContext)
+
const handleChange = (event: SelectChangeEvent) => {
setOption(event.target.value as string)
}
@@ -45,7 +49,7 @@ const POP3 = () => {
Select sender or receiver for your system & fill in the additional fields to get started.{' '}
-
+
@@ -86,7 +90,14 @@ const POP3 = () => {
{popTestCasesSender.map((test, i) => {
return (
-
+
)
})}
@@ -98,7 +109,15 @@ const POP3 = () => {
{popTestCasesReceiver.map((test, i) => {
return (
-
+
)
})}
diff --git a/src/components/direct/hisp/ProfilesCard.tsx b/src/components/direct/hisp/ProfilesCard.tsx
index 6a559ba3..809e2a48 100644
--- a/src/components/direct/hisp/ProfilesCard.tsx
+++ b/src/components/direct/hisp/ProfilesCard.tsx
@@ -5,8 +5,9 @@ interface DocsCardProps {
smtpAddress: string
emailAddress: string
header: string
+ getProfileReport: (profileName: string) => void
}
-const ProfilesCard: React.FC = ({ smtpAddress, emailAddress, header }) => {
+const ProfilesCard: React.FC = ({ smtpAddress, emailAddress, header, getProfileReport }) => {
return (
= ({ smtpAddress, emailAddress, head
{header}
-
- SUTE SMTP Address:
+ Vendor Hostname/IP:
{smtpAddress}
- SUTE Email Address:
+ Vendor Direct Email Address:
{emailAddress}
-
+ getProfileReport(header)}>
Show Report
diff --git a/src/components/direct/hisp/SMTPTab.tsx b/src/components/direct/hisp/SMTPTab.tsx
index 2c7c8126..40f96935 100644
--- a/src/components/direct/hisp/SMTPTab.tsx
+++ b/src/components/direct/hisp/SMTPTab.tsx
@@ -16,12 +16,15 @@ import palette from '@/styles/palette'
import * as React from 'react'
import testCases from '@/assets/SMTPTestCases'
import TestFilter from './TestFilter'
+import { useContext } from 'react'
+import { ProfileContext } from './context'
const SMTP = () => {
const [option, setOption] = React.useState('')
const smtpTestCases = testCases.tests.filter((test) => test.protocol === 'smtp')
const smtpTestCasesSender = smtpTestCases.filter((test) => test.sutRole === 'sender' && test.sutHisp)
const smtpTestCasesReceiver = smtpTestCases.filter((test) => test.sutRole === 'receiver' && test.sutHisp)
+ const { hostname, email, password, tls, username } = useContext(ProfileContext)
const handleChange = (event: SelectChangeEvent) => {
setOption(event.target.value as string)
}
@@ -44,7 +47,7 @@ const SMTP = () => {
Select sender or receiver for your system & fill in the additional fields to get started.{' '}
-
+
@@ -85,7 +88,14 @@ const SMTP = () => {
{smtpTestCasesSender.map((test, i) => {
return (
-
+
)
})}
@@ -96,7 +106,15 @@ const SMTP = () => {
{smtpTestCasesReceiver.map((test, i) => {
return (
-
+
)
})}
diff --git a/src/components/direct/hisp/TestCard.tsx b/src/components/direct/hisp/TestCard.tsx
index d3b6cf20..06efdf3e 100644
--- a/src/components/direct/hisp/TestCard.tsx
+++ b/src/components/direct/hisp/TestCard.tsx
@@ -6,6 +6,7 @@ import { handleAPICall } from '../test-by-criteria/ServerActions'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import CancelIcon from '@mui/icons-material/Cancel'
import LoadingButton from '../shared/LoadingButton'
+import { APICallData, APICallResponse, TestRequestResponses } from '../test-by-criteria/ServerActions'
import {
Box,
Button,
@@ -22,8 +23,8 @@ import {
Checkbox,
TextField,
SelectChangeEvent,
- Popover,
} from '@mui/material'
+import AlertSnackbar from '../shared/AlertSnackbar'
export type TestCaseFields = {
name: string
@@ -98,12 +99,7 @@ const TestCard = ({
username = 'defaultUsername',
password = 'defaultPassword',
tlsRequired = false,
- receive,
}: TestCardProps) => {
- const [popoverAnchorEl, setPopoverAnchorEl] = useState(null)
- const popoverOpen = Boolean(popoverAnchorEl)
- const popoverId = popoverOpen ? 'ccda-file-required-popover' : undefined
- const [autoCloseTimer, setAutoCloseTimer] = useState(null)
const attachmentTypeTestIDs = [231, 331]
const manualValidationCriteria = [
"['b1-5']",
@@ -114,15 +110,24 @@ const TestCard = ({
"['b1-4','su1-4']",
"['b1-4']",
]
+ const mdnTestIds = ['mu2']
+ const clearButtonVisibleOnCriteriaSet = new Set(['TRUE', 'FALSE', 'ERROR', 'PASSED', 'PENDING', 'SUCCESS', 'STEP2'])
+ const [currentStep, setCurrentStep] = useState(1)
+ const [previousResult, setPreviousResult] = useState(null)
+
const [showDetail, setShowDetail] = useState(false)
const [criteriaMet, setCriteriaMet] = useState('')
- const [testRequestResponses, setTestRequestResponses] = useState('')
+ const [testRequestResponses, setTestRequestResponses] = useState({})
const [showLogs, setShowLogs] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isFinished, setIsFinished] = useState(false)
const [apiError, setApiError] = useState(false)
const [attachmentType, setAttachmentType] = useState('')
+ const [alertOpen, setAlertOpen] = useState(false)
+ const [alertMessage, setAlertMessage] = useState('')
+ const [alertSeverity, setAlertSeverity] = useState<'success' | 'error' | 'warning' | 'info'>('error')
+
const handleDocumentConfirm = (selectedData: SelectedDocument) => {
console.log('Confirmed Document', selectedData)
setDocumentDetails(selectedData)
@@ -146,12 +151,14 @@ const TestCard = ({
}
const handleClearTest = () => {
+ setCurrentStep(1)
setCriteriaMet('')
- setTestRequestResponses('')
+ setTestRequestResponses({})
setIsFinished(false)
setShowLogs(false)
setDocumentDetails(null)
setApiError(false)
+ setPreviousResult(null)
}
const handleAttachmentTypeChange = (event: SelectChangeEvent) => {
@@ -163,12 +170,52 @@ const TestCard = ({
fileName: string
fileLink: string
} | null>(null)
+ const baseRequestData: APICallData = {
+ testCaseNumber: test.id,
+ sutSmtpAddress: hostname,
+ sutEmailAddress: email,
+ useTLS: tlsRequired,
+ sutCommandTimeoutInSeconds: 0,
+ sutUserName: username,
+ sutPassword: password,
+ tttUserName: '',
+ tttPassword: '',
+ startTlsPort: 0,
+ status: '',
+ ccdaReferenceFilename: documentDetails ? documentDetails.fileName : '',
+ ccdaValidationObjective: test.criteria || '',
+ ccdaFileLink: documentDetails ? documentDetails.fileLink : '',
+ cures: true,
+ year: '2021',
+ hostingcase: 'YES',
+ attachmentType: attachmentType,
+ previousResult: undefined,
+ }
+
+ const createRequestData = (step: number, prevResult?: APICallResponse | null): APICallData => {
+ const requestData = { ...baseRequestData }
+
+ if (step === 1) {
+ requestData.status = 'na'
+ } else if (step === 2 && prevResult) {
+ requestData.status = 'fetching'
+ requestData.previousResult = prevResult
+ } else {
+ requestData.status = ''
+ }
+ return requestData
+ }
- const formattedLogs = Object.entries(testRequestResponses).map(([key, value]) => (
-
- {value}
-
- ))
+ const formattedLogs = Object.entries(testRequestResponses).map(([key, value]) => {
+ const cleanedKey = key.trim()
+ const cleanedValue = value.trim()
+
+ return (
+
+ {`${cleanedKey}: ${cleanedValue}`}
+
+ )
+ })
const [formData, setFormData] = useState<{ [key: string]: FieldValue }>(() => {
const initialData: { [key: string]: FieldValue } = {}
@@ -197,68 +244,73 @@ const TestCard = ({
setFormData((prev) => ({ ...prev, [name]: value }))
}
- const handleClosePopover = () => {
- if (autoCloseTimer) {
- clearTimeout(autoCloseTimer)
- setAutoCloseTimer(null)
+ const handleRunTest = async () => {
+ const isMDNTest = test.protocol && mdnTestIds.includes(test.protocol)
+
+ if (test.ccdaFileRequired && !documentDetails && !test.name.includes('MT')) {
+ setAlertMessage(
+ 'This test requires a CCDA document to be selected. Please select a document before running the test.'
+ )
+ setAlertSeverity('error')
+ setAlertOpen(true)
+ return
}
- setPopoverAnchorEl(null)
- }
+ try {
+ setIsLoading(true)
+ setIsFinished(false)
+ setCriteriaMet('')
+
+ if (isMDNTest) {
+ const requestData = createRequestData(currentStep, previousResult)
+
+ const response = await handleAPICall(requestData)
+ const result = response[0]
+
+ setIsFinished(true)
+ setCriteriaMet(result.criteriaMet)
+ setTestRequestResponses(result.testRequestResponses)
+
+ if (currentStep === 1) {
+ setPreviousResult(result)
+ if (result.criteriaMet.includes('STEP2')) {
+ setCurrentStep(2)
+ }
+ } else if (currentStep === 2) {
+ setPreviousResult(null)
+ setCurrentStep(1)
+ }
+ } else {
+ const requestData = createRequestData(0)
+ const response = await handleAPICall(requestData)
+ const result = response[0]
- const handleRunTest = async () => {
- if (test.ccdaFileRequired && !documentDetails) {
- setPopoverAnchorEl(document.activeElement as HTMLButtonElement)
- const timer = setTimeout(() => {
- handleClosePopover()
- }, 2500)
- setAutoCloseTimer(timer)
- } else {
- try {
- setIsLoading(true)
- setIsFinished(false)
- setCriteriaMet('')
- const response = await handleAPICall({
- testCaseNumber: test.id,
- sutSmtpAddress: hostname,
- sutEmailAddress: email,
- useTLS: tlsRequired,
- sutCommandTimeoutInSeconds: 0,
- sutUserName: username,
- sutPassword: password,
- tttUserName: '',
- tttPassword: '',
- startTlsPort: 0,
- status: '',
- ccdaReferenceFilename: documentDetails ? documentDetails.fileName : '',
- ccdaValidationObjective: documentDetails ? documentDetails.directory : '',
- ccdaFileLink: documentDetails ? documentDetails.fileLink : '',
- cures: true,
- year: '2021',
- hostingcase: 'YES',
- attachmentType: attachmentType,
- })
setIsFinished(true)
- setCriteriaMet(response.criteriaMet)
- setTestRequestResponses(response.testRequestResponses)
- console.log('Criteria met: ', response.criteriaMet)
- console.log('Test Request Responses:', response.testRequestResponses)
- } catch (error) {
- console.error('Failed to run test:', error)
- setApiError(true)
- setCriteriaMet('FALSE')
- } finally {
- setIsLoading(false)
- if (test.criteria && !manualValidationCriteria.includes(test.criteria)) {
- setTimeout(() => {
- setIsFinished(false)
- }, 100)
+ setCriteriaMet(result.criteriaMet)
+ setTestRequestResponses(result.testRequestResponses)
+
+ if (result.criteriaMet.includes('STEP2')) {
+ setCurrentStep(2)
}
}
+ } catch (error) {
+ console.error('Failed to run test:', error)
+ setApiError(true)
+ setAlertMessage('An error occurred while running the test.')
+ setAlertSeverity('error')
+ setAlertOpen(true)
+ setCriteriaMet('FALSE')
+ } finally {
+ setIsLoading(false)
+ if (test.criteria && !manualValidationCriteria.includes(test.criteria)) {
+ setTimeout(() => {
+ setIsFinished(false)
+ }, 100)
+ }
}
}
const renderCriteriaMetIcon = () => {
- if (criteriaMet === 'TRUE') {
+ if (criteriaMet === 'TRUE' || criteriaMet === 'PASSED') {
return
} else if (criteriaMet === 'FALSE') {
return
@@ -278,6 +330,10 @@ const TestCard = ({
setShowDetail((prev) => !prev)
}
+ const handleAlertClose = () => {
+ setAlertOpen(false)
+ }
+
const renderAttachmentTypeDropdown = () => {
if (test.fields) {
const field = test.fields.find((f) => f.name === 'attachmentType')
@@ -309,25 +365,9 @@ const TestCard = ({
- {}
-
-
- This test requires a CCDA document to be selected. Please select a document before running the test.
-
-
+
+
+
{showDetail ? (
<>
@@ -491,7 +531,19 @@ const TestCard = ({
variant="contained"
color="primary"
>
- RUN
+
+ {test.protocol && mdnTestIds.includes(test.protocol)
+ ? currentStep === 1
+ ? 'RUN'
+ : 'CHECK MDN'
+ : 'RUN'}
+
MORE INFO
@@ -499,15 +551,16 @@ const TestCard = ({
LOGS
- {test.criteria &&
- manualValidationCriteria.includes(test.criteria) &&
- (criteriaMet.includes('TRUE') || criteriaMet.includes('FALSE')) && (
-
-
- Clear
-
-
- )}
+ {((test.criteria &&
+ criteriaMet &&
+ Array.from(clearButtonVisibleOnCriteriaSet).some((status) => criteriaMet.includes(status))) ||
+ isFinished) && (
+
+
+ Clear
+
+
+ )}
{test.criteria && manualValidationCriteria.includes(test.criteria) && !apiError && isFinished && (
Waiting Validation
)}
diff --git a/src/components/direct/hisp/ValidationResultsTab.tsx b/src/components/direct/hisp/ValidationResultsTab.tsx
index 59015da7..d8617757 100644
--- a/src/components/direct/hisp/ValidationResultsTab.tsx
+++ b/src/components/direct/hisp/ValidationResultsTab.tsx
@@ -1,41 +1,192 @@
import * as React from 'react'
-import { Box, Typography, Container } from '@mui/material'
+import {
+ Box,
+ Typography,
+ Container,
+ LinearProgress,
+ Button,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Tooltip,
+} from '@mui/material'
+import CheckCircleIcon from '@mui/icons-material/CheckCircle'
+import CancelIcon from '@mui/icons-material/Cancel'
+import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace'
import ProfilesCard from './ProfilesCard'
+import { useSession } from 'next-auth/react'
+import PageAlertBox from '@/components/shared/PageAlertBox'
+import { useEffect } from 'react'
+import { fetchProfileReport, fetchProfiles } from './actions'
+import Profile from '../shared/Profile'
+import _ from 'lodash'
+
+const removeProfilesWithNullProfileName = (profiles: Profile[]) => {
+ return profiles.filter((profile) => profile.profileName !== null)
+}
+
+interface ProfileReport {
+ smtpEdgeLogID: number
+ testCaseNumber: string
+ timestamp: number
+ criteriaMet: boolean
+}
const ValidationResults = () => {
+ const { data: session, status } = useSession()
+ const [profiles, setProfiles] = React.useState([])
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [profileReport, setProfileReport] = React.useState([])
+ const [profileName, setProfileName] = React.useState('')
+ const [open, setOpen] = React.useState(false)
+
+ useEffect(() => {
+ async function fetchLoggedInUsersProfiles() {
+ setIsLoading(true)
+ const loggedInUsersProfiles = await fetchProfiles()
+ const filteredProfiles = removeProfilesWithNullProfileName(loggedInUsersProfiles)
+ if (!_.isEmpty(filteredProfiles)) {
+ setProfiles(filteredProfiles)
+ }
+ setIsLoading(false)
+ }
+ if (status === 'authenticated') {
+ fetchLoggedInUsersProfiles()
+ }
+ }, [status, session])
+
+ const handleGetProfileReport = async (profileName: string) => {
+ setIsLoading(true)
+ const profileReport = await fetchProfileReport(profileName)
+ setProfileReport(profileReport)
+ setProfileName(profileName)
+ setOpen(true)
+ setIsLoading(false)
+ }
+
+ const handleCloseProfileReport = () => {
+ setProfileName('')
+ setProfileReport([])
+ setOpen(false)
+ }
+
+ const convertDate = (date: number) => {
+ const dateInt = _.toNumber(date)
+ return new Date(dateInt).toLocaleString()
+ }
+
return (
-
-
-
- Below are the different profiles you are aligned to. Select one to see the validation results.
-
-
-
-
-
-
-
-
-
-
-
+ <>
+ {status !== 'authenticated' ? (
+
+
+
+ ) : (
+
+ {isLoading ? (
+
+ ) : (
+
+ {open && (
+
+
+
+ Validation Report for {profileName}
+
+ }
+ onClick={() => handleCloseProfileReport()}
+ aria-label="close"
+ size="small"
+ >
+ Go back to profiles
+
+
+
+
+
+
+
+ Test Case
+
+ Timestamp
+ Result
+
+
+ {!_.isEmpty(profileReport) ? (
+
+ {profileReport.map((row: ProfileReport) => (
+
+
+ {row.testCaseNumber}
+
+ {convertDate(row.timestamp)}
+
+ {row.criteriaMet ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+
+
+
+ No validation results found for this profile.
+
+
+
+ )}
+
+
+
+ )}
+ {!open && (
+ <>
+
+ Below are your saved profiles. Select one to see the validation results.
+
+
+ {profiles.map((profile, index) => {
+ return (
+
+ )
+ })}
+
+ >
+ )}
+
+ )}
+
+ )}
+ >
)
}
diff --git a/src/components/direct/hisp/XDRTab.tsx b/src/components/direct/hisp/XDRTab.tsx
index 8e612915..70842d03 100644
--- a/src/components/direct/hisp/XDRTab.tsx
+++ b/src/components/direct/hisp/XDRTab.tsx
@@ -2,7 +2,6 @@ import React, { useState } from 'react'
import {
Box,
- Button,
Container,
Card,
CardContent,
@@ -14,9 +13,9 @@ import {
SelectChangeEvent,
} from '@mui/material'
import XDRTestCard from '@components/direct/hisp/XDRTestCard'
-import { Download } from '@mui/icons-material'
import TestFilter from './TestFilter'
import testCases from '@/assets/XDRTestCases'
+import DownloadXDRCert from '../shared/DownloadXDRCert'
const XDR = () => {
const [option, setOption] = useState('')
@@ -46,7 +45,7 @@ const XDR = () => {
{
-
-
-
- You can also download XDR TLS certificates.
- }>
- Download
-
-
-
-
+
{option !== '' && (
diff --git a/src/components/direct/hisp/XDRTestCard.tsx b/src/components/direct/hisp/XDRTestCard.tsx
index 8d89518f..9450005f 100644
--- a/src/components/direct/hisp/XDRTestCard.tsx
+++ b/src/components/direct/hisp/XDRTestCard.tsx
@@ -9,11 +9,10 @@ import {
Tooltip,
Typography,
FormControl,
- Popover,
} from '@mui/material'
import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo'
import _ from 'lodash'
-import React, { useState, useEffect, useRef } from 'react'
+import React, { useState, useRef } from 'react'
import DynamicTable from './DynamicTable'
import { handleXDRAPICall, GetStatus } from '../test-by-criteria/ServerActions'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
@@ -22,7 +21,9 @@ import LoadingButton from '../shared/LoadingButton'
import DocumentSelector from './DocumentSelector'
import { useSession } from 'next-auth/react'
import XMLDisplay from '../shared/colorizeXML'
-import ValidatorMenu from '@/components/c-cda/validation/results/ValidationMenu'
+import ValidatorResultsSummary from '@/components/c-cda/validation/results/ValidationResultsSummary'
+import AlertSnackbar from '../shared/AlertSnackbar'
+
export type TestCaseFields = {
name?: string
id: string | number
@@ -82,34 +83,26 @@ interface StepTextProps {
inputs: InputFields[]
role?: string
endpointsGenerated: boolean
+ criteriaMet: string
}
-const senderText = 'Hit Run to generate your endpoint.'
-const receiverText = 'Hit Run to send a XDR message.'
-const StepText = ({ inputs, role, endpointsGenerated }: StepTextProps) => {
- if (endpointsGenerated) {
- return (
-
- Step 2: Send XDR message to endpoint and refresh to check status.
-
- )
- }
+interface ValidationResults {
+ resultsMetaData: ResultsMetaData
+ ccdaValidationResults: CCDAValidationResult[]
+}
- return (
- <>
-
- Step 1: Provide your{' '}
- {inputs.map((input, i) => (
-
- {input.name}
- {inputs.length - 1 === i ? '. ' : ', '}
-
- ))}
- {role === 'sender' ? senderText : receiverText}
-
- >
- )
+interface ResultsMetaData {
+ documentType: string
+}
+
+interface CCDAValidationResult {
+ errorType: string
+ messageId: string
}
+
+const senderText = 'Hit Run to generate your endpoint.'
+const receiverText = 'Hit Run to send a XDR message.'
+
export type FieldValue = boolean | string | number
export type ExtraFields = {
label: string
@@ -133,7 +126,8 @@ interface SelectedDocument {
fileName: string
fileLink: string
}
-const TestCard = ({ test, receive }: TestCardProps) => {
+
+const TestCard = ({ test }: TestCardProps) => {
const defaultEndpoint =
process.env.XDR_ENDPOINT_PREFIX || 'http://ett.healthit.gov:11084/xdstools/sim/edge-ttp__' + test.id + '/rep/xdrpr'
const defaultEndpointTLS =
@@ -151,15 +145,27 @@ const TestCard = ({ test, receive }: TestCardProps) => {
const [endpointsGenerated, setEndpointsGenerated] = useState(false)
const [endpoint, setEndpoint] = useState(defaultEndpoint)
const [endpointTLS, setEndpointTLS] = useState(defaultEndpointTLS)
+ const [validationResults, setValidationResults] = useState(null)
+
+ const scrollRef = useRef(null)
+ const summaryRef = useRef(null)
+ const mdhtErrorRef = useRef(null)
+ const mdhtWarningRef = useRef(null)
+ const mdhtInfoRef = useRef(null)
+ const vocabularyErrorRef = useRef(null)
+ const vocabularyWarningRef = useRef(null)
+ const vocabularyInfoRef = useRef(null)
+ const referenceErrorRef = useRef(null)
+ const referenceWarningRef = useRef(null)
+ const referenceInfoRef = useRef(null)
+ const originalCCDARef = useRef(null)
- const [anchorEl, setAnchorEl] = useState(null)
- const [popoverMessage, setPopoverMessage] = useState('')
- const [autoCloseTimer, setAutoCloseTimer] = useState(null)
- const open = Boolean(anchorEl)
- const id = open ? 'simple-popover' : undefined
- const hiddenAnchorRef = useRef(null)
- const [logType, setLogType] = useState<'request' | 'response'>('request')
- const manualValidationCriteria = ["['b1-3']", "['b1-3','su1-3']"]
+ const [alertOpen, setAlertOpen] = useState(false)
+ const [alertMessage, setAlertMessage] = useState('')
+ const [alertSeverity, setAlertSeverity] = useState<'success' | 'error' | 'warning' | 'info'>('info')
+
+ const [logType, setLogType] = useState<'request' | 'response' | 'ccdaValidation'>('request')
+ const manualValidationIDs = ['4a', '4b', '20amu2', '20bmu2']
const { data: session } = useSession()
const subHeader = 'Description'
const subDesc = test['Purpose/Description']
@@ -171,32 +177,18 @@ const TestCard = ({ test, receive }: TestCardProps) => {
const shouldDisplayInput = (input: InputFields) => {
return !(input.key === 'payload' && input.type?.includes('CCDAWidget'))
}
+
const handleClick = (event: React.MouseEvent, link: string) => {
navigator.clipboard.writeText(link)
- showPopover('Copied to clipboard!', event.currentTarget)
- }
- const showPopover = (message: string, anchor: HTMLButtonElement | null) => {
- setPopoverMessage(message)
- setAnchorEl(anchor || hiddenAnchorRef.current)
- if (autoCloseTimer) clearTimeout(autoCloseTimer)
- const timer = setTimeout(() => {
- handleClosePopover()
- }, 3000)
- setAutoCloseTimer(timer)
+ setAlertMessage('Copied to clipboard!')
+ setAlertSeverity('success')
+ setAlertOpen(true)
}
- useEffect(() => {
- return () => {
- if (autoCloseTimer) clearTimeout(autoCloseTimer)
- }
- }, [autoCloseTimer, anchorEl])
- const handleClosePopover = () => {
- if (autoCloseTimer) {
- clearTimeout(autoCloseTimer)
- setAutoCloseTimer(null)
- }
- setAnchorEl(null)
+
+ const handleAlertClose = () => {
+ setAlertOpen(false)
}
- const toggleLogType = (type: 'request' | 'response') => {
+ const toggleLogType = (type: 'request' | 'response' | 'ccdaValidation') => {
setLogType(type)
}
const endpointTestIds = [
@@ -216,7 +208,45 @@ const TestCard = ({ test, receive }: TestCardProps) => {
'44mu2',
]
const ccdaRequiredTestIds = ['1', '2', '3add']
+ const sendEdgeTestsCriteria = ['b1-1']
const isCCDADocumentRequired = ccdaRequiredTestIds.includes(test.id.toString())
+ const StepText = ({ inputs, role, endpointsGenerated, criteriaMet }: StepTextProps) => {
+ if (manualValidationIDs.includes(test.id.toString()) && isFinished) {
+ if (test.id == '20amu2' || test.id == '20bmu2') {
+ console.log('abc')
+ testRequest == 'Check your SUT logs and accept or reject'
+ testResponse == 'Check your SUT logs and accept or reject'
+ }
+ return (
+
+ Step 3: Check the logs to accept/reject the response
+
+ )
+ }
+
+ if (endpointsGenerated) {
+ return (
+
+ Step 2: Send XDR message to endpoint and refresh to check status.
+
+ )
+ }
+
+ return (
+ <>
+
+ Step 1: Provide your{' '}
+ {inputs.map((input, i) => (
+
+ {input.name}
+ {inputs.length - 1 === i ? '. ' : ', '}
+
+ ))}
+ {role === 'sender' ? senderText : receiverText}
+
+ >
+ )
+ }
const [formData] = useState<{ [key: string]: FieldValue }>(() => {
const initialData: { [key: string]: FieldValue } = {}
test.moreInfo?.fields?.forEach((field) => {
@@ -232,92 +262,118 @@ const TestCard = ({ test, receive }: TestCardProps) => {
}))
}
}
+
+ const fixEndpoint = (url: string): string => {
+ if (url && !url.startsWith('http://') && !url.startsWith('https://')) {
+ return 'https://' + url
+ }
+ return url
+ }
+
const handleRunTest = async () => {
if (!session) {
- showPopover('You must be logged in and have a valid session to perform this action.', null)
+ setAlertMessage('You must be logged in and have a valid session to perform this action.')
+ setAlertSeverity('error')
+ setAlertOpen(true)
return
}
- console.log('ccda required' + test.ccdaFileRequired)
- console.log('doc details ' + documentDetails)
+
if (isCCDADocumentRequired && !documentDetails) {
- showPopover(
- 'This test requires a CCDA document to be selected. Please select a document before running the test.',
- null
+ setAlertMessage(
+ 'This test requires a CCDA document to be selected. Please select a document before running the test.'
)
+ setAlertSeverity('error')
+ setAlertOpen(true)
return
- } else {
- const ip_address = fieldValues['ip_address'] || ''
- const port = fieldValues['port'] || ''
- const direct_to = fieldValues['direct_to'] || ''
- const direct_from = fieldValues['direct_from'] || ''
- const targetEndpointTLS = fieldValues['targetEndpointTLS'] || ''
- const outgoing_from = fieldValues['outgoing_from'] || ''
- try {
- setIsLoading(true)
- setIsFinished(false)
- setCriteriaMet('')
- if (endpointsGenerated) {
- const status = await GetStatus(test.id.toString())
- console.log('Test status:', status)
- setTestRequestRequest(status.testRequest)
- setTestRequestResponse(status.testResponse)
- setCriteriaMet(status.criteriaMet)
+ }
+ const ip_address = fieldValues['ip_address'] || ''
+ const port = fieldValues['port'] || ''
+ const direct_to = fieldValues['direct_to'] || ''
+ const direct_from = fieldValues['direct_from'] || ''
+ const targetEndpointTLS = fieldValues['targetEndpointTLS'] || ''
+ const outgoing_from = fieldValues['outgoing_from'] || ''
+ try {
+ setIsLoading(true)
+ setIsFinished(false)
+ setCriteriaMet('')
+ if (endpointsGenerated) {
+ const status = await GetStatus(test.id.toString())
+ console.log('Test status:', status)
+ setTestRequestRequest(status.testRequest)
+ setTestRequestResponse(status.testResponse)
+ setCriteriaMet(status.criteriaMet)
+ setIsFinished(true)
+ if (status.results) {
+ setValidationResults(status.results)
+ setEndpointsGenerated(false)
+ }
+ } else {
+ const response = await handleXDRAPICall({
+ ip_address: ip_address,
+ port: port,
+ direct_to: direct_to,
+ direct_from: direct_from,
+ targetEndpointTLS: targetEndpointTLS,
+ outgoing_from: outgoing_from,
+ name: documentDetails ? documentDetails.fileName : '',
+ path: documentDetails ? documentDetails.directory : '',
+ link: documentDetails ? documentDetails.fileLink : '',
+ id: test.id.toString(),
+ jsession: session.user.jsessionid,
+ cures: false,
+ itemNumber: '12',
+ selected: true,
+ svap: false,
+ uscdiv3: false,
+ })
+ setTimeout(() => {
setIsFinished(true)
- console.log('criteriamet:', criteriaMet)
- } else {
- const response = await handleXDRAPICall({
- ip_address: ip_address,
- port: port,
- direct_to: direct_to,
- direct_from: direct_from,
- targetEndpointTLS: targetEndpointTLS,
- outgoing_from: outgoing_from,
- name: documentDetails ? documentDetails.fileName : '',
- path: documentDetails ? documentDetails.directory : '',
- link: documentDetails ? documentDetails.fileLink : '',
- id: test.id.toString(),
- jsession: session.user.jsessionid,
- cures: false,
- itemNumber: '12',
- selected: true,
- svap: false,
- uscdiv3: false,
- })
- setTimeout(() => {
- setIsFinished(true)
- if (test.criteria && !manualValidationCriteria.includes(test.criteria)) {
- setCriteriaMet(response.criteriaMet)
+ if (test.criteria && !manualValidationIDs.includes(test.id.toString())) {
+ setCriteriaMet(response.criteriaMet)
+ }
+ if (!endpointTestIds.includes(test.id.toString())) {
+ let endpointSet = false
+ if (response.endpoint && response.endpoint.length > 10) {
+ setEndpoint(fixEndpoint(response.endpoint))
+ endpointSet = true
}
- if (
- !endpointTestIds.includes(test.id.toString()) &&
- (response.endpoint.length > 10 || response.endpointTLS.length > 10)
- ) {
- setEndpointsGenerated(true)
- setEndpoint(response.endpoint || defaultEndpoint)
- setEndpointTLS(response.endpointTLS || defaultEndpointTLS)
+ if (response.endpointTLS && response.endpointTLS.length > 10) {
+ setEndpointTLS(fixEndpoint(response.endpointTLS))
+ endpointSet = true
}
- setTestRequestRequest(response.testRequest)
- setTestRequestResponse(response.testResponse)
- if (!testRequest && !testResponse && test.criteria && !manualValidationCriteria.includes(test.criteria)) {
- setCriteriaMet('FALSE')
+ if (endpointSet) {
+ console.log('setting endpoints generated')
+ setEndpointsGenerated(true)
}
- console.log('Criteria met: ', response.criteriaMet)
- console.log('Test Request Responses:', response.testResponse)
- }, 10)
- }
- } catch (error) {
- console.error('Failed to run test:', error)
- setApiError(true)
- if (test.criteria && !manualValidationCriteria.includes(test.criteria)) {
- setCriteriaMet('FALSE')
- }
- } finally {
- setIsLoading(false)
- if (test.criteria && !manualValidationCriteria.includes(test.criteria)) {
- setTimeout(() => {
- setIsFinished(false)
- }, 100)
- }
+ }
+ setTestRequestRequest(response.testRequest)
+ setTestRequestResponse(response.testResponse)
+ if (
+ !testRequest &&
+ !testResponse &&
+ test.criteria &&
+ !manualValidationIDs.includes(test.id.toString()) &&
+ criteriaMet
+ ) {
+ console.log('Response null, setting criteria met false')
+ setCriteriaMet('FALSE')
+ }
+ console.log('Criteria met: ', response.criteriaMet)
+ console.log('Test Request Responses:', response.testResponse)
+ }, 10)
+ }
+ } catch (error) {
+ console.error('Failed to run test:', error)
+ setApiError(true)
+ if (test.criteria && !manualValidationIDs.includes(test.id.toString())) {
+ setCriteriaMet('FALSE')
+ }
+ } finally {
+ setIsLoading(false)
+ if (test.criteria && !manualValidationIDs.includes(test.id.toString())) {
+ setTimeout(() => {
+ setIsFinished(false)
+ }, 100)
}
}
}
@@ -342,12 +398,13 @@ const TestCard = ({ test, receive }: TestCardProps) => {
setEndpoint(defaultEndpoint)
setEndpointTLS(defaultEndpointTLS)
setApiError(false)
+ setValidationResults(null)
}
+
const renderCriteriaMetIcon = () => {
- if (endpointsGenerated && criteriaMet != 'PASSED') {
+ if (endpointsGenerated) {
return Pending
- }
- if (criteriaMet === 'TRUE' || criteriaMet === 'PASSED') {
+ } else if (criteriaMet === 'TRUE' || criteriaMet === 'PASSED' || criteriaMet === 'SUCCESS') {
return
} else if (criteriaMet === 'FALSE' || criteriaMet === 'ERROR') {
return
@@ -378,9 +435,40 @@ const TestCard = ({ test, receive }: TestCardProps) => {
} | null>(null)
const [showDocumentSelector, setShowDocumentSelector] = useState(false)
const renderLogs = () => {
- const content = logType === 'request' ? testRequest : testResponse
- return
+ if (logType === 'ccdaValidation') {
+ if (validationResults) {
+ return (
+
+ )
+ } else {
+ return No C-CDA Validation results available.
+ }
+ } else {
+ let content = logType === 'request' ? testRequest : testResponse
+
+ if ((test.id === '20amu2' || test.id === '20bmu2') && isFinished && (!testRequest || !testResponse)) {
+ content = 'Check your SUT logs and accept or reject'
+ }
+
+ return
+ }
}
+
const renderMoreInfo = () => {
const { moreInfo } = test
return (
@@ -432,22 +520,7 @@ const TestCard = ({ test, receive }: TestCardProps) => {
-
- {popoverMessage}
-
+
{showDetail ? (
renderMoreInfo()
) : showLogs ? (
@@ -468,27 +541,33 @@ const TestCard = ({ test, receive }: TestCardProps) => {
>
Response
+ toggleLogType('ccdaValidation')}
+ color={logType === 'ccdaValidation' ? 'primary' : 'inherit'}
+ disabled={!validationResults}
+ >
+ C-CDA Validation
+
Close Logs
+
{renderLogs()}
- {test.criteria &&
- manualValidationCriteria.includes(test.criteria) &&
- testRequest &&
- testRequest.length > 0 && (
-
-
- Accept
-
-
- Reject
-
-
- )}
+ {test.criteria && manualValidationIDs.includes(test.id.toString()) && isFinished && (
+
+
+ Accept
+
+
+ Reject
+
+
+ )}
Close Logs
@@ -501,14 +580,25 @@ const TestCard = ({ test, receive }: TestCardProps) => {
{test.desc}
{_.isEqual(test.sutRole, 'receiver') && _.has(test, 'inputs') && test.inputs !== undefined && (
-
+
)}
{_.isEqual(test.sutRole, 'sender') && _.has(test, 'inputs') && test.inputs !== undefined && (
-
+
)}
{_.has(test, 'inputs') &&
test.inputs &&
!endpointsGenerated &&
+ !isFinished &&
test.inputs.filter(shouldDisplayInput).map((input) => (
@@ -548,32 +638,22 @@ const TestCard = ({ test, receive }: TestCardProps) => {
Endpoint
-
- }
- onClick={(e) => handleClick(e, endpointsGenerated ? endpointTLS : `${defaultEndpointTLS}`)}
+ {test.id != 7 && (
+
- Endpoint TLS
-
-
-
- {popoverMessage}
-
+ }
+ onClick={(e) => handleClick(e, endpointsGenerated ? endpointTLS : `${defaultEndpointTLS}`)}
+ >
+ Endpoint TLS
+
+
+ )}
)}
{requiresCCDADocument() && !endpointsGenerated && (
@@ -615,31 +695,22 @@ const TestCard = ({ test, receive }: TestCardProps) => {
>
{endpointsGenerated ? 'REFRESH' : 'RUN'}
-
MORE INFO
LOGS
- {test.criteria &&
- criteriaMet &&
- (criteriaMet.includes('TRUE') ||
- criteriaMet.includes('FALSE') ||
- criteriaMet.includes('ERROR') ||
- criteriaMet.includes('PASSED') ||
- criteriaMet.includes('SUCCESS')) && (
-
-
- Clear
-
-
- )}
- {test.criteria &&
- manualValidationCriteria.includes(test.criteria) &&
- (testRequest || testResponse) &&
- isFinished &&
- !apiError && Waiting Validation}
+ {((test.criteria && criteriaMet) || isFinished) && (
+
+
+ Clear
+
+
+ )}
+ {test.criteria && manualValidationIDs.includes(test.id.toString()) && isFinished && !apiError && (
+ Waiting Validation
+ )}
>
diff --git a/src/components/direct/hisp/actions.ts b/src/components/direct/hisp/actions.ts
new file mode 100644
index 00000000..5edc486a
--- /dev/null
+++ b/src/components/direct/hisp/actions.ts
@@ -0,0 +1,139 @@
+'use server'
+
+import { authOptions } from '@/lib/auth'
+import { getServerSession } from 'next-auth'
+
+const ETT_API_URL = process.env.ETT_API_URL
+export interface Profile {
+ hostname: string
+ email: string
+ username: string
+ password: string
+ istls: boolean
+ profilename: string
+ profileid: string
+}
+
+export async function saveProfile(data: Profile) {
+ console.log(`'Saving profile:', ${JSON.stringify(data)}`)
+ const { hostname, email, username, password, istls, profilename } = data
+ const session = await getServerSession(authOptions)
+ const jsessionid = session?.user?.jsessionid ?? ''
+ const ettAPIUrl = `${ETT_API_URL}/smtpProfile`
+ try {
+ const response = await fetch(ettAPIUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Cookie: `JSESSIONID=${jsessionid}`,
+ },
+ body: JSON.stringify({
+ sutSMTPAddress: hostname,
+ sutEmailAddress: email,
+ sutUsername: username,
+ sutPassword: password,
+ useTLS: istls,
+ profileName: profilename,
+ username: session?.user?.name,
+ }),
+ })
+ const data = await response.json()
+ if (!response.ok) {
+ console.log(`Error: ${data}`)
+ }
+ return data
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ return JSON.stringify({
+ status: 500,
+ success: false,
+ error: error.message || 'An error occurred',
+ })
+ }
+ }
+}
+
+export async function deleteProfile(profilename: string) {
+ console.log(`deleting profile: ${profilename}`)
+ const session = await getServerSession(authOptions)
+ const jsessionid = session?.user?.jsessionid ?? ''
+ const ettAPIUrl = `${ETT_API_URL}/smtpProfile/${profilename}`
+ try {
+ const response = await fetch(ettAPIUrl, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ Cookie: `JSESSIONID=${jsessionid}`,
+ },
+ })
+ const data = await response.json()
+ if (!response.ok) {
+ console.log(`Error: ${data}`)
+ }
+ return data
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ return JSON.stringify({
+ status: 500,
+ success: false,
+ error: error.message || 'An error occurred',
+ })
+ }
+ }
+}
+
+export async function fetchProfiles() {
+ const session = await getServerSession(authOptions)
+ const jsessionid = session?.user?.jsessionid ?? ''
+ const ettAPIUrl = `${ETT_API_URL}/smtpProfile`
+ try {
+ const response = await fetch(ettAPIUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Cookie: `JSESSIONID=${jsessionid}`,
+ },
+ })
+ const data = await response.json()
+ if (!response.ok) {
+ console.log(`Error: ${data}`)
+ }
+ return data
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ return JSON.stringify({
+ status: 500,
+ success: false,
+ error: error.message || 'An error occurred',
+ })
+ }
+ }
+}
+
+export async function fetchProfileReport(profilename: string) {
+ const session = await getServerSession(authOptions)
+ const jsessionid = session?.user?.jsessionid ?? ''
+ const ettAPIUrl = `${ETT_API_URL}/smtpLog/${profilename}`
+ try {
+ const response = await fetch(ettAPIUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Cookie: `JSESSIONID=${jsessionid}`,
+ },
+ })
+ const data = await response.json()
+ if (!response.ok) {
+ console.log(`Error: ${data}`)
+ }
+ return data
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ return JSON.stringify({
+ status: 500,
+ success: false,
+ error: error.message || 'An error occurred',
+ })
+ }
+ }
+}
diff --git a/src/components/direct/hisp/context/index.tsx b/src/components/direct/hisp/context/index.tsx
new file mode 100644
index 00000000..db49f4a8
--- /dev/null
+++ b/src/components/direct/hisp/context/index.tsx
@@ -0,0 +1,36 @@
+import { createContext } from 'react'
+
+interface ProfileContextType {
+ profileid?: string
+ profilename: string
+ hostname: string
+ email: string
+ username: string
+ password: string
+ tls: boolean
+ setProfileid: (profileid: string) => void
+ setHostname: (hostname: string) => void
+ setEmail: (email: string) => void
+ setUsername: (username: string) => void
+ setPassword: (password: string) => void
+ setTls: (tls: boolean) => void
+ setProfilename: (profilename: string) => void
+}
+const defaultValue: ProfileContextType = {
+ profileid: '',
+ hostname: '',
+ email: '',
+ username: '',
+ password: '',
+ tls: false,
+ profilename: 'Default Profile',
+ setProfileid: () => {},
+ setHostname: () => {},
+ setEmail: () => {},
+ setUsername: () => {},
+ setPassword: () => {},
+ setTls: () => {},
+ setProfilename: () => {},
+}
+
+export const ProfileContext = createContext(defaultValue)
diff --git a/src/components/direct/hisp/provider/index.tsx b/src/components/direct/hisp/provider/index.tsx
new file mode 100644
index 00000000..e1885519
--- /dev/null
+++ b/src/components/direct/hisp/provider/index.tsx
@@ -0,0 +1,42 @@
+'use client'
+import { useState } from 'react'
+import { ProfileContext } from '../context'
+
+import { ReactNode } from 'react'
+
+interface ProfileProviderProps {
+ children: ReactNode
+}
+
+export default function ProfileProvider({ children }: ProfileProviderProps) {
+ const [profileid, setProfileid] = useState('')
+ const [hostname, setHostname] = useState('')
+ const [email, setEmail] = useState('')
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+ const [tls, setTls] = useState(false)
+ const [profilename, setProfilename] = useState('')
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/direct/send-direct/DirectForm.tsx b/src/components/direct/send-direct/DirectForm.tsx
index 57b35dc3..fd92e69e 100644
--- a/src/components/direct/send-direct/DirectForm.tsx
+++ b/src/components/direct/send-direct/DirectForm.tsx
@@ -1,6 +1,8 @@
+import SwitchWithLabel from '@/components/shared/SwitchWithLabel'
+import DragandDropFile from '@components/shared/DragandDropFile'
+import HelpIcon from '@mui/icons-material/Help'
import {
Box,
- Button,
Container,
Divider,
FormControlLabel,
@@ -13,10 +15,11 @@ import {
Tooltip,
Typography,
} from '@mui/material'
-import DragandDropFile from '@components/shared/DragandDropFile'
-import HelpIcon from '@mui/icons-material/Help'
-import React, { useEffect, useState } from 'react'
import _ from 'lodash'
+import React, { useEffect, useState } from 'react'
+import { useFormState } from 'react-dom'
+import { handleSendDirectMessage } from './actions'
+import SendDirectResults from './SendDirectResults'
const documentDropdown = [
{
@@ -51,32 +54,36 @@ export interface DirectFormProps {
version: string
certificateDropdown: certProps[]
algorithmDropdown: algorithmProps[]
+ domainName: string
}
-const DirectForm = ({ version, certificateDropdown, algorithmDropdown }: DirectFormProps) => {
- const [formErrors, setFormErrors] = useState({})
- const [formValues, setFormValues] = useState({})
- const [disableSendButton, setDisableSendButton] = React.useState(true)
- /* TO-DO: Form submission, this would change when we work on functionality */
- const handleSubmit = (e: React.FormEvent) => {
- const { name, value } = e.currentTarget
- setFormValues({
- ...formValues,
- [name]: value,
- })
- console.log(formValues)
- }
-
- /* Validation*/
+const DirectForm = ({ version, certificateDropdown, algorithmDropdown, domainName }: DirectFormProps) => {
+ const [formValues, setFormValues] = useState<{ [key: string]: string }>({})
+ const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({})
+ const [disableSendButton, setDisableSendButton] = useState(true)
+ const [selectedDocument, setSelectedDocument] = useState('')
+ const [selectedCertificate, setSelectedCertificate] = useState('')
+ const [selectedAlgorithm, setSelectedAlgorithm] = useState('')
+ const [isMessageWrapped, setIsMessageWrapped] = useState(true)
+ const [isInvalidDigest, setIsInvalidDigest] = useState(false)
+ const [data, handleSubmit] = useFormState(handleSendDirectMessage, { response: {} })
+ const [algorithmDropdownM, setAlgorithmDropdownM] = useState(algorithmDropdown)
+
+ //Validation
const handleValidation = (e: React.ChangeEvent) => {
- let errors = {}
+ const errors = { ...formErrors }
const { name, value } = e.target
if (value === '') {
- errors = { ...errors, [name]: 'Email Address is required' }
- }
- if (!/^[a-zA-Z0-9._:$!%-]+@[a-zA-Z0-9.-]+.[a-zA-Z]$/.test(value)) {
- errors = { ...errors, [name]: 'Please enter a valid email' }
+ errors[name] = 'This field is required'
+ } else if (name === 'toAddress') {
+ if (!/^[a-zA-Z0-9._:$!%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]+$/.test(value)) {
+ errors[name] = 'Please enter a valid email'
+ } else {
+ delete errors[name]
+ }
+ } else {
+ delete errors[name]
}
setFormValues({
...formValues,
@@ -86,207 +93,238 @@ const DirectForm = ({ version, certificateDropdown, algorithmDropdown }: DirectF
}
useEffect(() => {
- if (_.has(formValues, 'fromEmail') && _.has(formValues, 'toEmail') && _.isEmpty(formErrors)) {
+ if (_.has(formValues, 'fromAddress') && _.has(formValues, 'toAddress') && _.isEmpty(formErrors)) {
setDisableSendButton(false)
}
- }, [disableSendButton, formErrors, formValues])
-
+ if (isInvalidDigest) {
+ setSelectedCertificate('')
+ }
+ if (_.isEqual(selectedCertificate, 'GOOD_ECDSA_CERT')) {
+ const filteredAlgo = _.filter(algorithmDropdown, function (o) {
+ return o.label.includes('ECDSA')
+ })
+ setAlgorithmDropdownM(filteredAlgo)
+ } else {
+ setAlgorithmDropdownM(algorithmDropdown)
+ }
+ }, [algorithmDropdown, formErrors, formValues, isInvalidDigest, selectedCertificate])
+ const handleChangeDocument = (e: React.ChangeEvent) => {
+ const { value } = e.target
+ setSelectedDocument(value)
+ }
+ const handleChangeCertificate = (e: React.ChangeEvent) => {
+ const { value } = e.target
+ setSelectedCertificate(value)
+ setIsInvalidDigest(false)
+ }
+ const handleMessageWrappedChange = (e: React.ChangeEvent) => {
+ setIsMessageWrapped(e.target.checked)
+ }
+ const handleInvalidDigestChange = (e: React.ChangeEvent) => {
+ setIsInvalidDigest(e.target.checked)
+ }
return (
-
-
- Step 1
-
-
- Enter the fields below to send your direct message
-
+
)
}
diff --git a/src/components/direct/send-direct/SendDirect.tsx b/src/components/direct/send-direct/SendDirect.tsx
index f627bad6..2dce5c25 100644
--- a/src/components/direct/send-direct/SendDirect.tsx
+++ b/src/components/direct/send-direct/SendDirect.tsx
@@ -1,37 +1,20 @@
-'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'
-import TabsComponent, { TabInputs } from '@/components/shared/TabsComponent'
-import Version13 from './Version13Tab'
-import Version12 from './Version12Tab'
-import MessageStatus from './message-status/MessageStatus'
+import SendDirectTabs from './SendDirectTabs'
+function getDomainName(url: string) {
+ try {
+ const parsedUrl = new URL(url)
+ return parsedUrl.hostname
+ } catch (error) {
+ console.error('Invalid URL:', error)
+ return null
+ }
+}
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)
- }
- }, [])
+ const apiUrl = process.env.SEND_DIRECT_API || 'https://ett.healthit.gov/ett/api'
+ const domainName = getDomainName(apiUrl)
return (
<>
@@ -41,13 +24,13 @@ const SendDirect = () => {
Direct
,
- Direct Message
+ Send Direct
,
]}
heading={'Send Direct Message'}
- description="Send a Direct message from this tool to a HISP of your choosing. Need more text here"
+ description="Send a Direct message from this tool to a HISP of your choosing."
/>
-
+
>
)
}
diff --git a/src/components/direct/send-direct/SendDirectResults.tsx b/src/components/direct/send-direct/SendDirectResults.tsx
new file mode 100644
index 00000000..d898ac83
--- /dev/null
+++ b/src/components/direct/send-direct/SendDirectResults.tsx
@@ -0,0 +1,99 @@
+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 = {
+ result?: boolean
+ message?: string
+ error?: string
+ errorStatus?: number
+}
+const SendDirectResults = ({ response, buttonTitle, disabled }: 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 ? : buttonTitle}
+
+
+ {!pending && _.has(response, 'error') && (
+
+ )}
+
+ {!pending && !_.has(response, 'error') && !_.isEmpty(response) && (
+
+
+
+ {response.result && (
+
+
+
+ Message successfully sent!
+
+
+ )}
+ {response.result === undefined && (
+
+
+
+ Message failed to be sent!
+
+
+ {response.message}
+
+
+ )}
+
+ )}
+ >
+ )
+}
+
+export default SendDirectResults
diff --git a/src/components/direct/send-direct/SendDirectTabs.tsx b/src/components/direct/send-direct/SendDirectTabs.tsx
new file mode 100644
index 00000000..bb0afe8d
--- /dev/null
+++ b/src/components/direct/send-direct/SendDirectTabs.tsx
@@ -0,0 +1,38 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import TabsComponent, { TabInputs } from '@/components/shared/TabsComponent'
+import Version13 from './Version13Tab'
+import Version12 from './Version12Tab'
+import MessageStatus from './message-status/MessageStatus'
+export interface SendDirectTabsProps {
+ domainName: string
+}
+const SendDirectTabs = ({ domainName }: SendDirectTabsProps) => {
+ 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
+}
+
+export default SendDirectTabs
diff --git a/src/components/direct/send-direct/Version12Tab.tsx b/src/components/direct/send-direct/Version12Tab.tsx
index e1162df0..d0d7f29f 100644
--- a/src/components/direct/send-direct/Version12Tab.tsx
+++ b/src/components/direct/send-direct/Version12Tab.tsx
@@ -1,5 +1,5 @@
import DirectForm, { algorithmProps, certProps } from './DirectForm'
-
+import { SendDirectTabsProps } from './SendDirectTabs'
const certificateDropdownV12: certProps[] = [
{ value: 'GOOD', label: 'GOOD_CERT' },
{ value: 'INVALID', label: 'INVALID_CERT' },
@@ -12,9 +12,14 @@ const algorithmDropdownV12: algorithmProps[] = [
{ value: 'sha1', label: 'SHA-1' },
{ value: 'sha256', label: 'SHA-256' },
]
-const Version12 = () => {
+const Version12 = ({ domainName }: SendDirectTabsProps) => {
return (
-
+
)
}
diff --git a/src/components/direct/send-direct/Version13Tab.tsx b/src/components/direct/send-direct/Version13Tab.tsx
index 2b6dc3f7..fc48322e 100644
--- a/src/components/direct/send-direct/Version13Tab.tsx
+++ b/src/components/direct/send-direct/Version13Tab.tsx
@@ -1,39 +1,47 @@
import React from 'react'
import DirectForm, { algorithmProps, certProps } from './DirectForm'
-
+import { SendDirectTabsProps } from './SendDirectTabs'
const certificateDropdown: certProps[] = [
- { value: 'GOOD_CERT', label: 'GOOD_CERT' },
- { value: 'INVALID_CERT', label: 'INVALID_CERT' },
- { value: 'EXPIRED_CERT', label: 'EXPIRED_CERT' },
- { value: 'DIFFERENT_TRUST_ANCHOR', label: 'DIFFERENT_TRUST_ANCHOR' },
- { value: 'BAD_AIA', label: 'BAD_AIA' },
- { value: 'WILD_CARD_DOMAIN_CERT', label: 'WILD_CARD_DOMAIN_CERT' },
- { value: 'CERT_WITH_EMAIL_ADDRESS', label: 'CERT_WITH_EMAIL_ADDRESS' },
- { value: 'CERT_LESS_THAN_2048_BITS', label: 'CERT_LESS_THAN_2048_BITS' },
- { value: 'CERT_WITH_NO_CRL', label: 'CERT_WITH_NO_CRL' },
- { value: 'CERT_WITH_NO_NOTBEFORE_ATTR', label: 'CERT_WITH_NO_NOTBEFORE_ATTR' },
- { value: 'CERT_WITH_NO_NOTAFTER_ATT', label: 'CERT_WITH_NO_NOTAFTER_ATT' },
- { value: 'CERT_WITH_3072_BITS', label: 'CERT_WITH_3072_BITS' },
- { value: 'CERT_WITH_4096_BITS', label: 'CERT_WITH_4096_BITS' },
+ { value: 'GOOD', label: 'GOOD_CERT' },
+ { value: 'GOOD_ECDSA_CERT', label: 'GOOD_ECDSA_CERT' },
+ { value: 'INVALID', label: 'INVALID_CERT' },
+ { value: 'EXPIRED', label: 'EXPIRED_CERT' },
+ { value: 'DIFF', label: 'DIFFERENT_TRUST_ANCHOR' },
+ { value: 'AIA', label: 'BAD_AIA' },
+ { value: 'WILD_CARD', label: 'WILD_CARD_DOMAIN_CERT' },
+ { value: 'EMAIL', label: 'CERT_WITH_EMAIL_ADDRESS' },
+ { value: 'LESS_2048', label: 'CERT_LESS_THAN_2048_BITS' },
+ { value: 'NO_CRL', label: 'CERT_WITH_NO_CRL' },
+ { value: 'NO_NOTBEFORE', label: 'CERT_WITH_NO_NOTBEFORE_ATTR' },
+ { value: 'NO_NOTAFTER', label: 'CERT_WITH_NO_NOTAFTER_ATT' },
+ { value: 'CERT_3072', label: 'CERT_WITH_3072_BITS' },
+ { value: 'CERT_4096', label: 'CERT_WITH_4096_BITS' },
]
const algorithmDropdown: algorithmProps[] = [
- { value: 'SHA-256', label: 'SHA-256' },
- { value: 'SHA-384', label: 'SHA-384' },
- { value: 'SHA-512', label: 'SHA-512' },
+ { value: 'sha256', label: 'SHA-256' },
+ { value: 'sha384', label: 'SHA-384' },
+ { value: 'sha512', label: 'SHA-512' },
{
- value: 'Optimal Asymmetric Encryption Padding (OAEP) for RSA encryption and decryption',
+ value: 'OAEP-RSA',
label: 'Optimal Asymmetric Encryption Padding (OAEP) for RSA encryption and decryption',
},
- { value: 'ECDSA with P-256', label: 'ECDSA with P-256' },
- { value: 'ECDSA with SHA-256', label: 'ECDSA with SHA-256' },
- { value: 'ECDSA with P-384', label: 'ECDSA with P-384' },
- { value: 'ECDSA with SHA-384', label: 'ECDSA with SHA-384' },
- { value: 'AES with CBC', label: 'AES with CBC' },
- { value: 'AES with GCM', label: 'AES with GCM' },
+ { value: 'edsap256', label: 'ECDSA with P-256' },
+ { value: 'edsasha256', label: 'ECDSA with SHA-256' },
+ { value: 'edsap384', label: 'ECDSA with P-384' },
+ { value: 'edsasha384', label: 'ECDSA with SHA-384' },
+ { value: 'aescbc', label: 'AES with CBC' },
+ { value: 'aesgcm', label: 'AES with GCM' },
]
-const Version13 = () => {
- return
+const Version13 = ({ domainName }: SendDirectTabsProps) => {
+ return (
+
+ )
}
export default Version13
diff --git a/src/components/direct/send-direct/actions.ts b/src/components/direct/send-direct/actions.ts
new file mode 100644
index 00000000..9ea626de
--- /dev/null
+++ b/src/components/direct/send-direct/actions.ts
@@ -0,0 +1,124 @@
+'use server'
+import { GENERIC_ERROR_MESSAGE } from '@/constants/errorConstants'
+import axios from 'axios'
+
+export async function postTempUpload(file: File) {
+ const Api = process.env.ETT_TEMP_UPLOAD_API
+ const formData = new FormData()
+ formData.append('file', file)
+ formData.append('flowRelativePath', file.name)
+ formData.append('flowTotalSize', file.size.toString())
+ formData.append('flowFilename', file.name)
+ const config = {
+ method: 'post',
+ url: Api,
+ data: formData,
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ }
+ try {
+ const response = await axios.request(config)
+ console.log('Temp Upload response status', response.status)
+ return { response: response.data.flowRelativePath }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ console.error(error.response?.data)
+ return {
+ response: {
+ error: GENERIC_ERROR_MESSAGE,
+ errorStatus: error.response?.status,
+ },
+ }
+ } else {
+ console.error(error)
+ }
+ }
+}
+
+// Send Direct Message API Post Call
+export async function handleSendDirectMessage(prevState: object | undefined, formData: FormData) {
+ const sendDirectApi = process.env.SEND_DIRECT_API
+ const domainName = formData.get('domainName') as string
+ const version = formData.get('version')
+ const fromAddress = (formData.get('fromAddress') as string) + '@' + domainName
+ const toAddress = formData.get('toAddress') as string
+ const subject = formData.get('subject') !== '' ? (formData.get('subject') as string) : 'Test Message'
+ const textMessage = formData.get('textMessage') !== '' ? (formData.get('textMessage') as string) : 'Test Message'
+ const attachmentFile =
+ formData.get('attachmentFile') !== '' ? (formData.get('attachmentFile') as string) : 'CCDA_Ambulatory.xml'
+ const wrapped = version === 'v13' ? true : formData.get('wrapped') === 'on' ? true : false
+ const invalidDigest = formData.get('invalidDigest') === 'on' ? true : false
+ const signingCert = formData.get('signingCert') !== '' ? (formData.get('signingCert') as string) : ''
+ const signingCertPassword = ''
+
+ const digestAlgo =
+ version === 'v13'
+ ? formData.get('digestAlgo') !== ''
+ ? (formData.get('digestAlgo') as string)
+ : 'sha256'
+ : formData.get('digestAlgo') !== ''
+ ? (formData.get('digestAlgo') as string)
+ : 'sha1'
+ const ownCcdaAttachment = formData.get('ownCcdaAttachment') as File
+ const encryptionCert = formData.get('encryptionCert') as File
+
+ let ownCcdaAttachmentTempPath = ''
+ if (ownCcdaAttachment.size !== 0) {
+ const tempPath = await postTempUpload(ownCcdaAttachment)
+ ownCcdaAttachmentTempPath = tempPath?.response || ''
+ console.log('ownCcdaAttachmentTempPath', ownCcdaAttachmentTempPath)
+ }
+ let encryptionCertTempPath = ''
+ if (encryptionCert.size !== 0) {
+ const tempPath = await postTempUpload(encryptionCert)
+ encryptionCertTempPath = tempPath?.response || ''
+ console.log('encryptionCertTempPath', encryptionCertTempPath)
+ }
+ const data = JSON.stringify({
+ fromAddress: fromAddress,
+ toAddress: toAddress,
+ subject: subject,
+ textMessage: textMessage,
+ attachmentFile: attachmentFile,
+ wrapped: wrapped,
+ invalidDigest: invalidDigest,
+ signingCert: signingCert,
+ signingCertPassword: signingCertPassword,
+ directVersion: version,
+ digestAlgo: digestAlgo,
+ ownCcdaAttachment: ownCcdaAttachmentTempPath,
+ encryptionCert: encryptionCertTempPath,
+ })
+ const config = {
+ method: 'post',
+ url: sendDirectApi,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ data: data,
+ }
+ console.log('Submitted data for Send Direct ', config)
+ try {
+ const response = await axios.request(config)
+ console.log(JSON.stringify(response.data))
+ return { response: response.data }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ console.error(error.response?.data)
+ return {
+ response: {
+ error: error.response?.data.message ? error.response?.data.message : GENERIC_ERROR_MESSAGE,
+ errorStatus: error.response?.status,
+ },
+ }
+ } else {
+ console.error(error)
+ return {
+ response: {
+ error: GENERIC_ERROR_MESSAGE,
+ },
+ }
+ }
+ }
+}
diff --git a/src/components/direct/shared/AlertSnackbar.tsx b/src/components/direct/shared/AlertSnackbar.tsx
index de20380f..e7961b32 100644
--- a/src/components/direct/shared/AlertSnackbar.tsx
+++ b/src/components/direct/shared/AlertSnackbar.tsx
@@ -16,9 +16,9 @@ const AlertSnackbar = ({
open={open}
autoHideDuration={6000}
onClose={onClose}
- anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
-
+
{message}
diff --git a/src/components/direct/shared/DownloadXDRCert.tsx b/src/components/direct/shared/DownloadXDRCert.tsx
new file mode 100644
index 00000000..e291cdbc
--- /dev/null
+++ b/src/components/direct/shared/DownloadXDRCert.tsx
@@ -0,0 +1,25 @@
+import { Box, Button, Card, CardContent, Typography } from '@mui/material'
+import { Download } from '@mui/icons-material'
+
+const DownloadXDRCert = () => {
+ return (
+
+
+
+ Download XDR TLS certificates.
+ }
+ href="/certificates/xdr-tls/keyAndCert.zip"
+ download
+ >
+ Download
+
+
+
+
+ )
+}
+
+export default DownloadXDRCert
diff --git a/src/components/direct/shared/Profile.tsx b/src/components/direct/shared/Profile.tsx
index 9b6cab7b..ba9083e2 100644
--- a/src/components/direct/shared/Profile.tsx
+++ b/src/components/direct/shared/Profile.tsx
@@ -1,110 +1,301 @@
import palette from '@/styles/palette'
-import { Box, TextField, Button, MenuItem, FormGroup, FormControlLabel, Switch } from '@mui/material'
+import {
+ Box,
+ TextField,
+ Button,
+ FormGroup,
+ FormControlLabel,
+ Switch,
+ Select,
+ MenuItem,
+ SelectChangeEvent,
+ LinearProgress,
+} from '@mui/material'
+import { useContext, useEffect, useState } from 'react'
+import { ProfileContext } from '../hisp/context'
+import { deleteProfile, fetchProfiles, saveProfile } from '../hisp/actions'
+import { useSession } from 'next-auth/react'
+import AlertSnackbar from './AlertSnackbar'
+import _ from 'lodash'
-const dropdown = [
- {
- value: 'Default Profile',
- label: 'Default Profile',
- },
-]
+interface Profile {
+ profileName: string
+ sutSMTPAddress: string
+ sutEmailAddress: string
+ sutUsername: string
+ sutPassword: string
+ useTLS: boolean
+ smtpEdgeProfileID: string
+}
-interface ProfileProps {
- setHostname?: (hostname: string) => void
- setEmail?: (email: string) => void
- setUsername?: (username: string) => void
- setPassword?: (password: string) => void
- setTls?: (tls: boolean) => void
+const NEWPROFILENAME = '__new__'
+const removeProfilesWithNullProfileName = (profiles: Profile[]) => {
+ return profiles.filter((profile) => profile.profileName !== null)
}
+const Profile = () => {
+ const {
+ setProfileid,
+ setProfilename,
+ setHostname,
+ setEmail,
+ setUsername,
+ setPassword,
+ setTls,
+ profileid,
+ profilename,
+ hostname,
+ email,
+ password,
+ tls,
+ username,
+ } = useContext(ProfileContext)
+ const { data: session, status } = useSession()
+
+ const [profiles, setProfiles] = useState([])
+ const [selectedProfileName, setSelectedProfileName] = useState(NEWPROFILENAME)
+ const [isLoading, setIsLoading] = useState(false)
+ interface Message {
+ text: string
+ severity: 'info' | 'error' | 'success' | 'warning'
+ }
+
+ const [message, setMessage] = useState(null)
+
+ useEffect(() => {
+ async function fetchLoggedInUsersProfiles() {
+ setIsLoading(true)
+ const loggedInUsersProfiles = await fetchProfiles()
+ const filteredProfiles = removeProfilesWithNullProfileName(loggedInUsersProfiles)
+ if (!_.isEmpty(filteredProfiles)) {
+ setProfiles(filteredProfiles)
+ setSelectedProfileName(filteredProfiles[0].profileName)
+ setProfilename(filteredProfiles[0].profileName)
+ setHostname(filteredProfiles[0].sutSMTPAddress)
+ setEmail(filteredProfiles[0].sutEmailAddress)
+ setUsername(filteredProfiles[0].sutUsername)
+ setPassword(filteredProfiles[0].sutPassword)
+ setTls(filteredProfiles[0].useTLS)
+ setProfileid(filteredProfiles[0].smtpEdgeProfileID)
+ }
+ setIsLoading(false)
+ }
+ if (status === 'authenticated') {
+ fetchLoggedInUsersProfiles()
+ }
+ }, [status, session])
+
+ const handleSaveProfile = () => {
+ setIsLoading(true)
+ saveProfile({
+ profileid: profileid || '',
+ hostname: hostname,
+ email: email,
+ username: username,
+ password: password,
+ istls: tls,
+ profilename: profilename,
+ }).then(async (response) => {
+ if (response) {
+ setMessage({ text: `${profilename} saved`, severity: 'success' })
+ const loggedInUsersProfiles = await fetchProfiles()
+ const filteredProfiles = removeProfilesWithNullProfileName(loggedInUsersProfiles)
+ setProfiles(filteredProfiles)
+ const savedProfile = filteredProfiles.filter((profile) => profile.profileName === profilename).pop()
+ if (savedProfile) {
+ setSelectedProfileName(savedProfile.profileName)
+ setProfilename(savedProfile.profileName)
+ setHostname(savedProfile.sutSMTPAddress)
+ setEmail(savedProfile.sutEmailAddress)
+ setUsername(savedProfile.sutUsername)
+ setPassword(savedProfile.sutPassword)
+ setTls(savedProfile.useTLS)
+ setProfileid(savedProfile.smtpEdgeProfileID)
+ }
+ } else {
+ setMessage({ text: `Failed to save ${profilename}`, severity: 'error' })
+ }
+ setIsLoading(false)
+ })
+ }
-const noop = () => {}
+ const handleDeleteProfile = () => {
+ setIsLoading(true)
+ const profileNameToDelete = selectedProfileName
+ deleteProfile(profileNameToDelete).then(async (response) => {
+ if (response) {
+ setMessage({ text: `${profileNameToDelete} removed`, severity: 'success' })
+ const loggedInUsersProfiles = await fetchProfiles()
+ const filteredProfiles = removeProfilesWithNullProfileName(loggedInUsersProfiles)
+ setProfiles(filteredProfiles || [{ smtpEdgeProfileID: NEWPROFILENAME } as Profile])
+ const lastProfile = _.last(filteredProfiles)
+ setSelectedProfileName(lastProfile?.profileName || NEWPROFILENAME)
+ setProfilename(lastProfile?.profileName || '')
+ setHostname(lastProfile?.sutSMTPAddress || '')
+ setEmail(lastProfile?.sutEmailAddress || '')
+ setUsername(lastProfile?.sutUsername || '')
+ setPassword(lastProfile?.sutPassword || '')
+ setTls(lastProfile?.useTLS || false)
+ setProfileid(lastProfile?.smtpEdgeProfileID || NEWPROFILENAME)
+ } else {
+ setMessage({ text: `Failed to remove ${profileNameToDelete}`, severity: 'error' })
+ }
+ setIsLoading(false)
+ })
+ }
+
+ const handleProfileChange = async (event: SelectChangeEvent) => {
+ const selectedProfileName = event.target.value
+ if (selectedProfileName === NEWPROFILENAME) {
+ console.log('New Profile')
+ setSelectedProfileName(selectedProfileName)
+ setProfilename('')
+ setHostname('')
+ setEmail('')
+ setUsername('')
+ setPassword('')
+ setTls(false)
+ setProfileid('')
+ return
+ }
+ const selectedProfile = profiles.find((profile) => profile.profileName === selectedProfileName)
+ if (selectedProfile) {
+ setSelectedProfileName(selectedProfile.profileName)
+ setProfilename(selectedProfile.profileName)
+ setHostname(selectedProfile.sutSMTPAddress)
+ setEmail(selectedProfile.sutEmailAddress)
+ setUsername(selectedProfile.sutUsername)
+ setPassword(selectedProfile.sutPassword)
+ setTls(selectedProfile.useTLS)
+ setProfileid(selectedProfile.smtpEdgeProfileID)
+ }
+ }
-const Profile: React.FC = ({
- setHostname = noop,
- setEmail = noop,
- setUsername = noop,
- setPassword = noop,
- setTls = noop,
-}) => {
return (
-
-
- {dropdown.map((option) => (
-
- ))}
-
-
-
- setHostname(e.target.value)}
- />
- setEmail(e.target.value)}
- />
-
-
- setUsername(e.target.value)}
- />
- setPassword(e.target.value)}
- />
-
-
- setTls(e.target.checked)} />}
- label="TLS REQUIRED"
- name="tlsRequired"
- />
-
-
-
-
-
- Make Profile
-
-
- Save
-
+ <>
+ {isLoading ? (
+
+ ) : (
+
+ {!_.isEmpty(profiles) && (
+ <>
+
+ >
+ )}
+
+
+ setHostname(e.target.value)}
+ />
+ setEmail(e.target.value)}
+ />
+
+
+ setUsername(e.target.value)}
+ />
+ setPassword(e.target.value)}
+ />
+
+
+ setTls(e.target.checked)} />}
+ label="TLS REQUIRED"
+ name="tlsRequired"
+ />
+
+
+
+
+ {status === 'authenticated' && (
+ <>
+ setProfilename(e.target.value)}
+ />
+
+
+ Save
+
+ handleDeleteProfile()}
+ disabled={profilename === '' || profilename !== selectedProfileName}
+ >
+ Remove
+
+
+ >
+ )}
+
+ {!_.isEmpty(message) && (
+ setMessage(null)}
+ />
+ )}
-
- Remove
-
-
-
+ )}
+ >
)
}
export default Profile
diff --git a/src/components/direct/test-by-criteria/B1Tab.tsx b/src/components/direct/test-by-criteria/B1Tab.tsx
index 729bcf98..6415c746 100644
--- a/src/components/direct/test-by-criteria/B1Tab.tsx
+++ b/src/components/direct/test-by-criteria/B1Tab.tsx
@@ -17,15 +17,15 @@ import palette from '@/styles/palette'
import React, { useState } from 'react'
import testCases from '../../../assets/SMTPTestCases'
import xdrTestCases from '../../../assets/XDRTestCases'
+import { useContext } from 'react'
+import { ProfileContext } from '../hisp/context'
+import DownloadXDRCert from '../shared/DownloadXDRCert'
const B1Component = () => {
const [option, setOption] = useState('')
const [showTestCard, setShowTestCard] = useState(false)
- const [hostname, setHostname] = useState('')
- const [email, setEmail] = useState('')
- const [username, setUsername] = useState('')
- const [password, setPassword] = useState('')
- const [tlsRequired, setTlsRequired] = useState(false)
+ const { hostname, email, password, tls, username } = useContext(ProfileContext)
+ const [isXDR, setIsXDR] = React.useState(false)
const criteriaA = xdrTestCases.filter((testXdr) => testXdr.criteria?.includes('b1-1'))
const criteriaB = testCases.tests.filter((test) => test.criteria?.includes('b1-8'))
@@ -40,6 +40,11 @@ const B1Component = () => {
const newOption = event.target.value as string
setShowTestCard(false)
+ if (newOption === 'A' || newOption === 'D') {
+ setIsXDR(true)
+ } else {
+ setIsXDR(false)
+ }
setTimeout(() => {
setOption(newOption)
@@ -123,15 +128,12 @@ const B1Component = () => {
-
-
-
+ {!isXDR && (
+
+
+
+ )}
+ {isXDR && }
{showTestCard &&
@@ -144,7 +146,7 @@ const B1Component = () => {
email={email}
username={username}
password={password}
- tlsRequired={tlsRequired}
+ tlsRequired={tls}
receive={isReceiveOption()}
/>
diff --git a/src/components/direct/test-by-criteria/H1Tab.tsx b/src/components/direct/test-by-criteria/H1Tab.tsx
index efaf294d..75792521 100644
--- a/src/components/direct/test-by-criteria/H1Tab.tsx
+++ b/src/components/direct/test-by-criteria/H1Tab.tsx
@@ -15,14 +15,12 @@ import Profile from '../shared/Profile'
import palette from '@/styles/palette'
import TestCard from '../hisp/TestCard'
import testCases from '../../../assets/SMTPTestCases'
+import { useContext } from 'react'
+import { ProfileContext } from '../hisp/context'
const H1Component = () => {
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 { hostname, email, password, tls, username } = useContext(ProfileContext)
const dropdownOptions = [
{
@@ -97,13 +95,7 @@ const H1Component = () => {
-
+
@@ -117,7 +109,7 @@ const H1Component = () => {
email={email}
username={username}
password={password}
- tlsRequired={tlsRequired}
+ tlsRequired={tls}
receive={false}
/>
diff --git a/src/components/direct/test-by-criteria/H2Tab.tsx b/src/components/direct/test-by-criteria/H2Tab.tsx
index 96816dc5..1bd1a550 100644
--- a/src/components/direct/test-by-criteria/H2Tab.tsx
+++ b/src/components/direct/test-by-criteria/H2Tab.tsx
@@ -2,55 +2,298 @@ import {
Box,
Typography,
MenuItem,
- Card,
- CardContent,
- Container,
FormControl,
InputLabel,
Select,
SelectChangeEvent,
+ Card,
+ CardContent,
+ Container,
} from '@mui/material'
+import React, { useState, useContext } from 'react'
import Profile from '../shared/Profile'
-import SMTPTestCard from '../shared/SMTPTestCard'
import palette from '@/styles/palette'
-import * as React from 'react'
-import { useEffect } from 'react'
-import criteria from '@/assets/Criteria'
-import _ from 'lodash'
+import TestCard from '../hisp/TestCard'
+import XDRTestCard from '../hisp/XDRTestCard'
+import testCases from '../../../assets/SMTPTestCases'
+import xdrTestCases from '../../../assets/XDRTestCases'
+import { ProfileContext } from '../hisp/context'
+import DownloadXDRCert from '../shared/DownloadXDRCert'
+
+interface TestCase {
+ id: number
+ name: string
+ criteria?: string | string[]
+ protocol: string
+ description?: string
+}
+
+interface XDRTestCase {
+ id: number
+ name: string
+ criteria?: string | string[]
+}
+
+interface Subcategory {
+ value: string
+ label: string
+ categories: string[]
+ link?: string
+ testCard?: boolean
+ criteria?: string[]
+ testSources?: string[]
+ receive?: boolean
+}
const H2Component = () => {
- const h2CriteriaList = criteria.filter((c) => c.testList === "['h2']")
- const [firstDropdownSelectedOption, setFirstDropdownSelectedOption] = React.useState('All')
- const [criteriaOptions, setCriteriaOptions] = React.useState(h2CriteriaList)
- const [selectedCriteria, setSelectedCriteria] = React.useState('')
- const [showTestCard, setShowTestCard] = React.useState(false)
-
- const h2FirstDropdown = [
- { name: 'All', testList: ['h2', 'sc2'], selectOption: 'ALL' },
- { name: 'Setup', testList: ['h2', 'sc2'], selectOption: 'A' },
- { name: 'Send', testList: ['h2', 'sc2'], selectOption: 'B' },
- { name: 'Send - Delivery Notification for Direct', testList: ['h2', 'sc2'], selectOption: '9' },
- { name: 'Send using Direct+XDM', testList: ['h2', 'sc2'], selectOption: '2' },
- { name: 'Send conversion XDR', testList: ['h2', 'sc2'], selectOption: '3' },
- { name: 'Send using Edge Protocol', testList: ['h2', 'sc2'], selectOption: '4' },
- { name: 'Receive', testList: ['h2', 'sc2'], selectOption: '5' },
- { name: 'Receive - Delivery Notification in Direct', testList: ['h2', 'sc2'], selectOption: '10' },
- { name: 'Receive using Direct+XDM', testList: ['h2', 'sc2'], selectOption: '6' },
- { name: 'Receive conversion XDR', testList: ['h2', 'sc2'], selectOption: '7' },
- { name: 'Receive using Edge Protocol', testList: ['h2', 'sc2'], selectOption: '8' },
+ const [selectedCategory, setSelectedCategory] = useState('')
+ const [selectedSubcategory, setSelectedSubcategory] = useState('')
+ const { hostname, email, password, tls, username } = useContext(ProfileContext)
+ const [isXDR, setIsXDR] = React.useState(false)
+
+ const categories = [
+ { value: 'all', label: 'All' },
+ { value: 'setup', label: 'Setup' },
+ { value: 'send', label: 'Send' },
+ { value: 'sendDeliveryNotification', label: 'Send - Delivery Notification for Direct' },
+ { value: 'sendDirectXDM', label: 'Send using Direct+XDM' },
+ { value: 'sendConversionXDR', label: 'Send conversion XDR' },
+ { value: 'sendEdgeProtocol', label: 'Send using Edge Protocol' },
+ { value: 'receive', label: 'Receive' },
+ { value: 'receiveDeliveryNotification', label: 'Receive - Delivery Notification in Direct' },
+ { value: 'receiveDirectXDM', label: 'Receive using Direct+XDM' },
+ { value: 'receiveConversionXDR', label: 'Receive conversion XDR' },
+ { value: 'receiveEdgeProtocol', label: 'Receive using Edge Protocol' },
]
- const handleFirstDropdownChange = (event: SelectChangeEvent) => {
- setFirstDropdownSelectedOption(event.target.value as string)
- const selectedOption = h2FirstDropdown.filter((o) => _.isEqual(o.name, event.target.value))
- const criteriaList = h2CriteriaList.filter((c) => c.selectOption?.includes(selectedOption[0].selectOption))
- setCriteriaOptions(criteriaList)
- setSelectedCriteria('')
+ function parseCriteria(criteria: string | string[] | undefined): string[] {
+ if (!criteria) return []
+ if (Array.isArray(criteria)) return criteria
+ if (typeof criteria === 'string') {
+ if (criteria.startsWith('[') && criteria.endsWith(']')) {
+ try {
+ const jsonCriteria = criteria.replace(/'/g, '"')
+ const parsed = JSON.parse(jsonCriteria)
+ if (Array.isArray(parsed)) {
+ return parsed
+ }
+ } catch (e) {
+ console.error('Failed to parse criteria:', criteria, e)
+ return []
+ }
+ }
+ return [criteria]
+ }
+ return []
}
- const handleCriteriaChange = (event: SelectChangeEvent) => {
- setSelectedCriteria(event.target.value as string)
+ const subcategories: Subcategory[] = [
+ {
+ value: 'certificateDiscoveryHosting',
+ label: 'Criteria (i) Certificate Discovery / Hosting - 2015 DCDT',
+ categories: ['all', 'setup'],
+ link: '/direct/dcdt#hosting',
+ },
+ {
+ value: 'registerDirect',
+ label: 'Criteria (i) Register Direct',
+ categories: ['all', 'setup'],
+ link: '/direct/register',
+ },
+ {
+ value: 'directHomeCertificates',
+ label: 'Criteria (i) Direct Home - Certificates',
+ categories: ['all', 'setup'],
+ link: '/direct#certification-download',
+ },
+ {
+ value: 'sendDirectMessage',
+ label: 'Criteria (i) Send Direct Message',
+ categories: ['all', 'send'],
+ link: '/direct/senddirect',
+ },
+ {
+ value: 'messageStatus',
+ label: 'Criteria (i) Message Status',
+ categories: ['all', 'receive'],
+ link: '/direct/senddirect#message-status',
+ },
+ {
+ value: 'ccdaValidator',
+ label: 'Criteria (i) C-CDA R2.1 validator',
+ categories: ['all', 'setup'],
+ link: '/c-cda/uscdi-v3',
+ },
+ {
+ value: 'xdmValidator',
+ label: 'Criteria (i) XDM Validator',
+ categories: ['all', 'setup'],
+ link: '/direct/xdm',
+ },
+ {
+ value: 'sendConversionXDR',
+ label: 'Criteria Send conversion XDR',
+ categories: ['all', 'sendConversionXDR'],
+ testCard: true,
+ testSources: ['xdr'],
+ criteria: ['h2-1'],
+ },
+ {
+ value: 'receiveConversionXDR',
+ label: 'Criteria Receive conversion XDR',
+ categories: ['all', 'receiveConversionXDR'],
+ testCard: true,
+ testSources: ['xdr'],
+ criteria: ['h2-2'],
+ receive: true,
+ },
+ {
+ value: 'sendEdgeXDR',
+ label: 'Criteria (i)(C) Send using Edge Protocol - XDR',
+ categories: ['all', 'sendEdgeProtocol'],
+ testCard: true,
+ testSources: ['xdr'],
+ criteria: ['h2-3'],
+ },
+ {
+ value: 'sendEdgeSMTP',
+ label: 'Criteria (i)(C) Send using Edge Protocol - SMTP',
+ categories: ['all', 'sendEdgeProtocol'],
+ testCard: true,
+ testSources: ['smtp'],
+ criteria: [''],
+ },
+ {
+ value: 'sendEdgeDeliveryNotification',
+ label: 'Criteria (i)(C) Send using Edge Protocol - Delivery Notification',
+ categories: ['all', 'sendEdgeProtocol'],
+ testCard: true,
+ testSources: ['smtp'],
+ criteria: ['b1-8', 'sc2-4'],
+ },
+ {
+ value: 'sendEdgeIMAP',
+ label: 'Criteria (i)(C) Send using Edge Protocol - IMAP',
+ categories: ['all', 'sendEdgeProtocol'],
+ testCard: true,
+ testSources: ['smtp'],
+ criteria: ['h2-5'],
+ },
+ {
+ value: 'sendEdgePOP',
+ label: 'Criteria (i)(C) Send using Edge Protocol - POP',
+ categories: ['all', 'sendEdgeProtocol'],
+ testCard: true,
+ testSources: ['smtp'],
+ criteria: ['h2-6'],
+ },
+ {
+ value: 'receiveEdgeXDR',
+ label: 'Criteria (i)(C) Receive using Edge Protocol - XDR',
+ categories: ['all', 'receiveEdgeProtocol'],
+ testCard: true,
+ testSources: ['xdr'],
+ criteria: ['h2-7'],
+ receive: true,
+ },
+ {
+ value: 'receiveEdgeSMTP',
+ label: 'Criteria (i)(C) Receive using Edge Protocol - SMTP',
+ categories: ['all', 'receiveEdgeProtocol'],
+ testCard: true,
+ testSources: ['smtp'],
+ criteria: ['h2-8'],
+ receive: true,
+ },
+ {
+ value: 'deliveryNotificationSMTP',
+ label: 'Criteria (ii) Delivery Notification in Direct - SMTP',
+ categories: ['all', 'sendDeliveryNotification'],
+ testCard: true,
+ testSources: ['smtp'],
+ criteria: ['h2-9'],
+ },
+ {
+ value: 'receiveSMTPDispositionNotification',
+ label: 'Criteria (ii) Receive SMTP: Disposition-Notification',
+ categories: ['all', 'receiveDeliveryNotification'],
+ testCard: true,
+ testSources: ['smtp'],
+ criteria: ['h2-10'],
+ receive: true,
+ },
+ {
+ value: 'deliveryNotificationXDR',
+ label: 'Criteria (ii)(C) Delivery Notification in Direct - XDR',
+ categories: ['all', 'sendDeliveryNotification'],
+ testCard: true,
+ testSources: ['xdr'],
+ criteria: ['h2-11'],
+ },
+ {
+ value: 'receiveXDRDispositionNotification',
+ label: 'Criteria (ii)(C) Receive XDR: Disposition-Notification',
+ categories: ['all', 'receiveDeliveryNotification'],
+ testCard: true,
+ testSources: ['xdr'],
+ criteria: ['h2-12'],
+ receive: true,
+ },
+ ]
+
+ const handleCategoryChange = (event: SelectChangeEvent) => {
+ setSelectedCategory(event.target.value)
+ setSelectedSubcategory('')
+ if (event.target.value.includes('XDR')) {
+ setIsXDR(true)
+ } else {
+ setIsXDR(false)
+ }
}
+
+ const handleSubcategoryChange = (event: SelectChangeEvent) => {
+ const value = event.target.value
+ const selectedOption = subcategories.find((option) => option.value === value)
+ setSelectedSubcategory(value)
+
+ if (selectedOption) {
+ if (selectedOption.testCard) {
+ } else if (selectedOption.link) {
+ window.location.href = selectedOption.link
+ }
+ }
+ if (event.target.value.includes('XDR')) {
+ setIsXDR(true)
+ } else {
+ setIsXDR(false)
+ }
+ }
+
+ const filteredSubcategories = subcategories.filter(
+ (subcategory) =>
+ selectedCategory === 'all' || !selectedCategory || subcategory.categories.includes(selectedCategory)
+ )
+
+ const selectedOption = subcategories.find((option) => option.value === selectedSubcategory)
+
+ const showTestCard = selectedOption?.testCard
+
+ const selectedTestCases: TestCase[] =
+ showTestCard && selectedOption?.testSources?.includes('smtp')
+ ? (testCases.tests as TestCase[]).filter((test: TestCase) => {
+ const testCriteriaArray = parseCriteria(test.criteria)
+ return selectedOption?.criteria?.some((criterion) => testCriteriaArray.includes(criterion))
+ })
+ : []
+
+ const selectedXDRTestCases: XDRTestCase[] =
+ showTestCard && selectedOption?.testSources?.includes('xdr')
+ ? (xdrTestCases as XDRTestCase[]).filter((test: XDRTestCase) => {
+ const testCriteriaArray = parseCriteria(test.criteria)
+ return selectedOption?.criteria?.some((criterion) => testCriteriaArray.includes(criterion))
+ })
+ : []
+
return (
@@ -59,48 +302,41 @@ const H2Component = () => {
- Use the menu to select what sub criteria you want to test for.
+ Use the menu to select the sub-criteria you want to test for.
-
- Choose a sub category
+
+ Choose a category
-
-
-
-
-
-
-
-
- Use the menu to select what sub criteria you want to test for.
-
-
- Choose a sub category
+ Choose a sub-category
@@ -109,22 +345,47 @@ const H2Component = () => {
-
-
-
+ {isXDR && }
+ {!isXDR && (
+
+
+
+ )}
- {showTestCard && (
-
- )}
+ {showTestCard &&
+ selectedTestCases.map((test: TestCase, i: number) => (
+
+
+
+ ))}
+ {showTestCard &&
+ selectedXDRTestCases.map((test: XDRTestCase, i: number) => (
+
+
+
+ ))}
)
}
+
export default H2Component
diff --git a/src/components/direct/test-by-criteria/ServerActions.ts b/src/components/direct/test-by-criteria/ServerActions.ts
index 79dd3194..c809156b 100644
--- a/src/components/direct/test-by-criteria/ServerActions.ts
+++ b/src/components/direct/test-by-criteria/ServerActions.ts
@@ -5,7 +5,7 @@ import _ from 'lodash'
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
-interface APICallData {
+export interface APICallData {
testCaseNumber: number | string
sutSmtpAddress: string
sutEmailAddress: string
@@ -30,6 +30,7 @@ interface APICallData {
targetEndpointTLS?: string
outgoing_from?: string
attachmentType?: string
+ previousResult?: APICallResponse
}
export interface Documents {
@@ -57,6 +58,7 @@ interface XDRAPICallData {
svap: boolean
uscdiv3: boolean
}
+
export interface FileDetail {
svap: boolean
cures: boolean
@@ -71,9 +73,28 @@ export interface Directory {
files: FileDetail[]
}
-interface APIResponse {
+export interface APICallResponse {
+ didRequestTimeOut: boolean
+ timeElapsedInSeconds: number
+ proctored: boolean
+ attachments: Record
+ CCDAValidationReports: Record
+ MessageId: string
+ fetchType: string
+ searchType: string
+ startTime: string
+ lastTestResultStatus: number
+ lastTestResponse: string
+ testCaseId: number
+ testCaseDesc: string | null
+ messageId: string
criteriaMet: string
- testRequestResponses: string
+ testRequestResponses: TestRequestResponses
+ ccdavalidationReports: Record
+}
+
+export interface TestRequestResponses {
+ [key: string]: string
}
interface XDRAPIResponse {
@@ -84,15 +105,36 @@ interface XDRAPIResponse {
endpointTLS: string
}
+interface ResultsMetaData {
+ documentType: string
+ validationObjective: string
+ ccdaVersion: string
+ curesUpdate: boolean
+ svap2022: boolean
+ uscdi: boolean
+ validationDate: string
+}
+
+interface CCDAValidationResult {
+ errorType: string
+ messageId: string
+}
+
+interface ValidationResults {
+ resultsMetaData: ResultsMetaData
+ ccdaValidationResults: CCDAValidationResult[]
+}
+
interface StatusResponse {
criteriaMet: string
testRequest: string
testResponse: string
message: string
status: string
+ results?: ValidationResults
}
-export async function handleAPICall(data: APICallData): Promise {
+export async function handleAPICall(data: APICallData): Promise {
const apiUrl = process.env.SMTP_TEST_BY_CRITERIA_ENDPOINT
const config = {
method: 'post',
@@ -103,17 +145,16 @@ export async function handleAPICall(data: APICallData): Promise {
try {
const response = await axios(config)
- return {
- criteriaMet: response.data[0].criteriaMet,
- testRequestResponses: response.data[0].testRequestResponses,
- }
+ console.log('raw API call data: ', response)
+ const responseData: APICallResponse[] = response.data
+ return responseData
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
console.error('API Error Response:', error.response.data)
console.error('Status:', error.response.status)
console.error('Headers:', error.response.headers)
} else {
- console.error('Error Message:')
+ console.error('Error')
}
throw error
}
@@ -155,7 +196,7 @@ export async function handleXDRAPICall(data: XDRAPICallData): Promise {
+ const apiUrl = process.env.SMTP_TEST_BY_CRITERIA_ENDPOINT
+ const session = await getServerSession(authOptions)
+ const jsessionid = session?.user?.jsessionid ?? ''
+
+ const config = {
+ method: 'post',
+ url: apiUrl,
+ headers: session
+ ? { 'Content-Type': 'application/json', Cookie: `JSESSIONID=${jsessionid}` }
+ : { 'Content-Type': 'application/json' },
+ data: data,
+ }
+
+ console.log('Sending data:', config)
+
+ try {
+ const response = await axios(config)
+ console.log('MDN check raw content:', response.data)
+ return response.data[0]
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response) {
+ console.error('API Error Response:', error.response.data)
+ console.error('Status:', error.response.status)
+ console.error('Headers:', error.response.headers)
+ } else {
+ console.error('MDN Check Error')
+ }
+ throw error
+ }
+}
+
export async function GetStatus(testCaseId: string): 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
@@ -207,11 +281,25 @@ export async function GetStatus(testCaseId: string): Promise {
let testRequest = ''
let testResponse = ''
let criteriaMet = ''
+ let results: ValidationResults | undefined
+
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
+ testRequest = content.content.value.request || content.message || ''
+ testResponse = content.content.value.response || content.message || ''
+ criteriaMet = content.content.criteriaMet || ''
+
+ const ccdaReport = content.content.value.ccdaReport
+ if (ccdaReport) {
+ const resultsMetaData: ResultsMetaData = ccdaReport.resultsMetaData
+ const ccdaValidationResults: CCDAValidationResult[] = ccdaReport.ccdaValidationResults
+
+ results = {
+ resultsMetaData: resultsMetaData,
+ ccdaValidationResults: ccdaValidationResults,
+ }
+ }
}
+
console.log('Status fetched: ', content)
return {
criteriaMet: criteriaMet,
@@ -219,6 +307,7 @@ export async function GetStatus(testCaseId: string): Promise {
testResponse: testResponse,
message: content.message,
status: content.status,
+ results: results,
}
} catch (error) {
if (axios.isAxiosError(error)) {
diff --git a/src/components/direct/test-by-criteria/TestByCriteria.tsx b/src/components/direct/test-by-criteria/TestByCriteria.tsx
index 606b4d46..8eaa153e 100644
--- a/src/components/direct/test-by-criteria/TestByCriteria.tsx
+++ b/src/components/direct/test-by-criteria/TestByCriteria.tsx
@@ -7,6 +7,7 @@ import H2Component from './H2Tab'
import Link from 'next/link'
import styles from '@shared/styles.module.css'
import TabsComponent, { TabInputs } from '@/components/shared/TabsComponent'
+import ProfileProvider from '../hisp/provider'
export interface criteriaProps {
selectedTab: string
}
@@ -33,7 +34,9 @@ const TestByCriteria = ({ selectedTab }: criteriaProps) => {
heading={'Test By Criteria'}
description={<>New Helper Text>}
/>
-
+
+
+
>
)
}
diff --git a/src/components/direct/validators/actions.ts b/src/components/direct/validators/actions.ts
index 5d15fb49..d45a8868 100644
--- a/src/components/direct/validators/actions.ts
+++ b/src/components/direct/validators/actions.ts
@@ -1,4 +1,5 @@
'use server'
+import { GENERIC_ERROR_MESSAGE } from '@/constants/errorConstants'
import axios from 'axios'
import _ from 'lodash'
@@ -36,7 +37,7 @@ export async function handleXDMUpload(prevState: object | undefined, formData: F
console.error(error.response?.data)
return {
response: {
- error: 'There was an error completing the request, Please try again later!',
+ error: GENERIC_ERROR_MESSAGE,
errorStatus: error.response?.status,
},
}
diff --git a/src/components/direct/xdr-edge/ResultsComponent.tsx b/src/components/direct/xdr-edge/ResultsComponent.tsx
new file mode 100644
index 00000000..6c0c9068
--- /dev/null
+++ b/src/components/direct/xdr-edge/ResultsComponent.tsx
@@ -0,0 +1,145 @@
+import { useFormStatus } from 'react-dom'
+import React, { useState, useEffect, FC } from 'react'
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogContent,
+ DialogTitle,
+ Divider,
+ IconButton,
+ LinearProgress,
+ Typography,
+} from '@mui/material'
+import _ from 'lodash'
+
+import ErrorDisplayCard from '@/components/c-cda/validation/results/ErrorDisplay'
+import CloseIcon from '@mui/icons-material/Close'
+import palette from '@/styles/palette'
+interface ResultsComponentProps {
+ response: object
+ disabled?: boolean
+ buttonTitle: string
+ loadingDialogTitle: string
+ loadingDialogContent: React.ReactNode
+ resultsDialogTitle: string
+ resultsDialogContent: React.ReactNode
+}
+
+interface ResultsDialogProps {
+ response?: object
+ open: boolean
+ handleClose: () => void
+ contentDisplay?: React.ReactNode
+ title: string
+}
+interface LoadingResultsProps {
+ open: boolean
+ title?: string
+ content?: React.ReactNode
+}
+const LoadingResults: FC = ({ open, title, content }) => {
+ return (
+
+ )
+}
+const ResultsDialog: FC = ({ open, handleClose, contentDisplay, title }) => {
+ //console.log(JSON.stringify(response))
+
+ return (
+
+ )
+}
+const ResultsComponent = ({
+ response,
+ disabled,
+ buttonTitle,
+ loadingDialogTitle,
+ loadingDialogContent,
+ resultsDialogTitle,
+ resultsDialogContent,
+}: ResultsComponentProps) => {
+ const [loadingOpen, setLoadingOpen] = useState(false)
+ const [resultsOpen, setResultsOpen] = useState(false)
+ const [errorOpen, setErrorOpen] = useState(false)
+ const { pending } = useFormStatus()
+ //console.log(response)
+ const handleLoadingOpen = () => {
+ setLoadingOpen(true)
+ }
+ const handleCloseDialog = () => {
+ setResultsOpen(false)
+ }
+
+ const handleErrorClose = () => {
+ setErrorOpen(false)
+ }
+
+ useEffect(() => {
+ if (!pending && !_.isEmpty(response) && !_.has(response, 'error')) {
+ setResultsOpen(true)
+ }
+ if (!pending && _.has(response, 'error')) {
+ setErrorOpen(true)
+ }
+ }, [pending, response])
+
+ return (
+ <>
+
+ {buttonTitle}
+
+
+ {pending && }
+ {!pending && _.has(response, 'error') && (
+
+ )}
+
+ {!pending && !_.has(response, 'error') && (
+
+ )}
+ >
+ )
+}
+
+export default ResultsComponent
diff --git a/src/components/direct/xdr-edge/SendTab.tsx b/src/components/direct/xdr-edge/SendTab.tsx
index ad6a34a5..e942dbd0 100644
--- a/src/components/direct/xdr-edge/SendTab.tsx
+++ b/src/components/direct/xdr-edge/SendTab.tsx
@@ -5,11 +5,14 @@ import Content from './send/ContentTab'
import bulletedList from '../shared/BulletList'
import { Container, Typography, List, ListItem } from '@mui/material'
-const SendTab = () => {
+interface SendTabProps {
+ sampleCCDAFiles: string[]
+}
+const SendTab = ({ sampleCCDAFiles }: SendTabProps) => {
const [selectedTab, setSelectedTab] = useState('Choose template')
const sendTabs: TabInputs[] = [
- { tabName: 'Choose template', tabIndex: 0, tabPanel: },
+ { tabName: 'Choose template', tabIndex: 0, tabPanel: },
{ tabName: 'Choose own content', tabIndex: 1, tabPanel: },
]
@@ -27,7 +30,6 @@ const SendTab = () => {
window.removeEventListener('hashchange', handleRouteChange)
}
})
-
return (
<>
@@ -42,13 +44,12 @@ const SendTab = () => {
- Enter any optional properties by expanding the optional properties panel. To remove the entered
- properties, collapse the panel by clicking on the tile.
+ Choose a CCDA document that you would like to attach as part of the payload.
- Choose any CCDA document that you would like to attach as part of the payload.
+ To add additional properties, toggle the 'Show Optional XDR Message Properties' switch.
diff --git a/src/components/direct/xdr-edge/ValidateResult.tsx b/src/components/direct/xdr-edge/ValidateResult.tsx
new file mode 100644
index 00000000..231caf89
--- /dev/null
+++ b/src/components/direct/xdr-edge/ValidateResult.tsx
@@ -0,0 +1,87 @@
+import { Box, Typography, Accordion, AccordionSummary, AccordionDetails } from '@mui/material'
+import ResultsComponent from './ResultsComponent'
+import { ExpandMore } from '@mui/icons-material'
+
+interface ResultsComponentProps {
+ response: MessagesProp[]
+ disabled?: boolean
+ buttonTitle: string
+}
+type MessagesProp = {
+ id: number
+ request: string
+ response: string
+ fromAddress: string
+ dateLogged: string
+}
+const Content = ({ response }: { response: MessagesProp[] }) => {
+ console.log(response)
+ return (
+
+ {response && (
+ <>
+ Search Results for {response[0].fromAddress}
+ {response.map((message: MessagesProp) => (
+
+ }>
+ Message recieved on: {message.dateLogged}
+
+
+
+
+
+
+ Sent Message
+
+
+
+ {message.request}
+
+
+
+
+
+ Response
+
+
+
+ {message.response}
+
+
+
+
+
+ ))}
+ >
+ )}
+
+ )
+}
+const ValidateResult = ({ response, buttonTitle }: ResultsComponentProps) => {
+ return (
+ }
+ />
+ )
+}
+
+export default ValidateResult
diff --git a/src/components/direct/xdr-edge/ValidateTab.tsx b/src/components/direct/xdr-edge/ValidateTab.tsx
index c2f5d0e7..2abe049a 100644
--- a/src/components/direct/xdr-edge/ValidateTab.tsx
+++ b/src/components/direct/xdr-edge/ValidateTab.tsx
@@ -1,7 +1,12 @@
-import { Container, List, ListItem, Typography, Card, CardContent, Box, TextField, Button } from '@mui/material'
+import { Container, List, ListItem, Typography, Card, CardContent, Box, TextField } from '@mui/material'
import bulletedList from '../shared/BulletList'
+import { useFormState } from 'react-dom'
+import { handleMessageValidation } from './actions'
+import ValidateResult from './ValidateResult'
const ValidateTab = () => {
+ const [data, handleSubmit] = useFormState(handleMessageValidation, { response: {} })
+
return (
<>
@@ -39,21 +44,21 @@ const ValidateTab = () => {
+
-
-
-
- SEARCH
-
-
+
diff --git a/src/components/direct/xdr-edge/XDREdgeTestTool.tsx b/src/components/direct/xdr-edge/XDREdgeTestTool.tsx
index 243e9b38..b8d65301 100644
--- a/src/components/direct/xdr-edge/XDREdgeTestTool.tsx
+++ b/src/components/direct/xdr-edge/XDREdgeTestTool.tsx
@@ -1,59 +1,9 @@
-'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'
-import TabsComponent, { TabInputs } from '@/components/shared/TabsComponent'
-import Send from './SendTab'
-import Validate from './ValidateTab'
+import XDREdgeTestContent from './XDREdgeTestToolContent'
+import { getSampleCCDAFiles } from './actions'
-const SendDirect = () => {
- const [selectedTab, setSelectedTab] = useState('SITE Sends XDR Messages to Your System')
+export default async function XDREdgeTest() {
+ const sampleCCDAFilesEndpoint = process.env.TTT_SAMPLE_CCDA_FILES_ENDPOINT || ''
+ const sampleCCDAFiles: string[] = (await getSampleCCDAFiles(sampleCCDAFilesEndpoint)) || []
- const XDREdgeTabs: TabInputs[] = [
- { tabName: 'SITE Sends XDR Messages to Your System', tabIndex: 0, tabPanel: },
- { tabName: 'Validate XDR Messages Generated by Your System', tabIndex: 1, tabPanel: },
- ]
-
- useEffect(() => {
- const handleRouteChange = () => {
- const hash = window.location.hash.replace('#', '').replace(/-/g, ' ').toLowerCase()
- const tab = XDREdgeTabs.find((t) => t.tabName.toLowerCase() === hash)
- setSelectedTab(tab ? tab.tabName : 'SITE Sends XDR Messages to Your System')
- }
-
- handleRouteChange()
- window.addEventListener('hashchange', handleRouteChange)
-
- return () => {
- window.removeEventListener('hashchange', handleRouteChange)
- }
- })
-
- return (
- <>
- {/* Global Header */}
-
- Direct
- ,
-
- XDR Edge Test Tool
- ,
- ]}
- heading={'Direct XDR Edge Test Tool'}
- description={
- <>
- The goal of the Direct XDR Edge Test Tool is to provide a mechanism for developers to test their Direct Test
- Tools protocols implementation based on IHE XDR profile.
- >
- }
- />
- {/* Main Content */}
-
- >
- )
+ return
}
-
-export default SendDirect
diff --git a/src/components/direct/xdr-edge/XDREdgeTestToolContent.tsx b/src/components/direct/xdr-edge/XDREdgeTestToolContent.tsx
new file mode 100644
index 00000000..4fa0325d
--- /dev/null
+++ b/src/components/direct/xdr-edge/XDREdgeTestToolContent.tsx
@@ -0,0 +1,66 @@
+'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'
+import TabsComponent, { TabInputs } from '@/components/shared/TabsComponent'
+import Send from './SendTab'
+import Validate from './ValidateTab'
+
+interface XDREdgeTestContentProps {
+ sampleCCDAFiles: string[]
+}
+const XDREdgeTestContent = ({ sampleCCDAFiles }: XDREdgeTestContentProps) => {
+ const [selectedTab, setSelectedTab] = useState('SITE Sends XDR Messages to Your System')
+
+ const XDREdgeTabs: TabInputs[] = [
+ {
+ tabName: 'SITE Sends XDR Messages to Your System',
+ tabIndex: 0,
+ tabPanel: ,
+ },
+ { tabName: 'Validate XDR Messages Generated by Your System', tabIndex: 1, tabPanel: },
+ ]
+
+ useEffect(() => {
+ const handleRouteChange = () => {
+ const hash = window.location.hash.replace('#', '').replace(/-/g, ' ').toLowerCase()
+ const tab = XDREdgeTabs.find((t) => t.tabName.toLowerCase() === hash)
+ setSelectedTab(tab ? tab.tabName : 'SITE Sends XDR Messages to Your System')
+ }
+
+ handleRouteChange()
+ window.addEventListener('hashchange', handleRouteChange)
+
+ return () => {
+ window.removeEventListener('hashchange', handleRouteChange)
+ }
+ })
+
+ return (
+ <>
+ {/* Global Header */}
+
+ Direct
+ ,
+
+ XDR Edge Test Tool
+ ,
+ ]}
+ heading={'Direct XDR Edge Test Tool'}
+ description={
+ <>
+ The goal of the Direct XDR Edge Test Tool is to provide a mechanism for developers to test their Direct Test
+ Tools protocols implementation based on IHE XDR profile.
+ >
+ }
+ />
+ {/* Main Content */}
+
+ >
+ )
+}
+
+export default XDREdgeTestContent
diff --git a/src/components/direct/xdr-edge/actions.ts b/src/components/direct/xdr-edge/actions.ts
new file mode 100644
index 00000000..5e2daddf
--- /dev/null
+++ b/src/components/direct/xdr-edge/actions.ts
@@ -0,0 +1,76 @@
+'use server'
+import { GENERIC_ERROR_MESSAGE } from '@/constants/errorConstants'
+import axios from 'axios'
+
+export async function getSampleCCDAFiles(sampleCCDAFilesEndpoint: string) {
+ const res = await fetch(sampleCCDAFilesEndpoint, { next: { revalidate: 3600 } })
+ if (!res.ok) {
+ console.error(res)
+ return []
+ }
+ return res.json()
+}
+
+export async function handleSendMessage(prevState: object | undefined, formData: FormData) {
+ let Api = ''
+ if (formData.has('attachmentFilePath')) {
+ Api = process.env.XDR_SEND_MESSAGE_WITH_ATTACHEMENTFILEPATH || ''
+ } else if (formData.has('attachment')) {
+ Api = process.env.XDR_SEND_MESSAGE_WITH_ATTACHEMENTFILE || ''
+ }
+
+ const config = {
+ method: 'post',
+ url: Api,
+
+ data: formData,
+ }
+ console.log('Submitted data for XDR SendMessage ', config, formData)
+ try {
+ const response = await axios.request(config)
+ console.log('XDR SendMessage response status', response.status)
+ return { response: response.data }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ console.error(error.response?.data)
+ return {
+ response: {
+ error: GENERIC_ERROR_MESSAGE,
+ errorStatus: error.response?.status,
+ },
+ }
+ } else {
+ console.error(error)
+ }
+ }
+}
+
+export async function handleMessageValidation(prevState: object | undefined, formData: FormData) {
+ const address = formData.get('fromAddress') || ''
+ const url = process.env.XDR_SEARCH_MESSAGE_LOGS_BY_FROM_ADDRESS_URL || ''
+ const Api = url + address
+
+ const config = {
+ method: 'get',
+ url: Api,
+ }
+ console.log('Submitted data for XDR ValidateMessage ', config)
+ try {
+ const response = await axios.request(config)
+ console.log('XDR ValidateMessage response status', response.status)
+ console.log('XDR ValidateMessage response data', response.data)
+ return { response: response.data }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ console.error(error.response?.data)
+ return {
+ response: {
+ error: GENERIC_ERROR_MESSAGE,
+ errorStatus: error.response?.status,
+ },
+ }
+ } else {
+ console.error(error)
+ }
+ }
+}
diff --git a/src/components/direct/xdr-edge/send/ContentTab.tsx b/src/components/direct/xdr-edge/send/ContentTab.tsx
index de54405f..3a2b0e5f 100644
--- a/src/components/direct/xdr-edge/send/ContentTab.tsx
+++ b/src/components/direct/xdr-edge/send/ContentTab.tsx
@@ -1,6 +1,6 @@
import SendForm from './SendForm'
const Content = () => {
- return
+ return
}
export default Content
diff --git a/src/components/direct/xdr-edge/send/SendForm.tsx b/src/components/direct/xdr-edge/send/SendForm.tsx
index 1698c09b..01c09989 100644
--- a/src/components/direct/xdr-edge/send/SendForm.tsx
+++ b/src/components/direct/xdr-edge/send/SendForm.tsx
@@ -1,31 +1,22 @@
import {
Box,
- Button,
Container,
Divider,
- FormControlLabel,
- FormGroup,
MenuItem,
Stack,
- Switch,
TextField,
Tooltip,
Typography,
Card,
CardContent,
- FormLabel,
} from '@mui/material'
import DragandDropFile from '@components/shared/DragandDropFile'
import HelpIcon from '@mui/icons-material/Help'
-import React, { useState } from 'react'
-
-//Pull these from TTT_SAMPLE_CCDA_FILES_ENDPOINT?
-const documentDropdown = [
- {
- value: 'Placeholder',
- label: 'Placeholder',
- },
-]
+import React, { useState, useEffect } from 'react'
+import { useFormState } from 'react-dom'
+import SwitchWithLabel from '@components/shared/SwitchWithLabel'
+import { handleSendMessage } from '../actions'
+import SendResults from './SendResult'
const messageType = [
{ value: 'Minimal', label: 'Minimal' },
@@ -34,18 +25,21 @@ const messageType = [
export interface SendFormProps {
version: string
+ sampleCCDAFiles: string[]
}
-const SendForm = ({ version }: SendFormProps) => {
- const [formValues, setFormValues] = useState({})
+const SendForm = ({ version, sampleCCDAFiles }: SendFormProps) => {
+ //const [formValues, setFormValues] = useState({})
const [showOptional, setShowOptional] = React.useState(false)
- const handleSubmit = (e: React.FormEvent) => {
- const { name, value } = e.currentTarget
- setFormValues({
- ...formValues,
- [name]: value,
- })
- console.log(formValues)
+ const [ccdaFiles, setCcdaFiles] = useState([])
+ const [selectedFile, setSelectedFile] = useState(' ')
+ const [data, handleSubmit] = useFormState(handleSendMessage, { response: {} })
+
+ useEffect(() => {
+ setCcdaFiles(sampleCCDAFiles)
+ }, [sampleCCDAFiles])
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ setSelectedFile(e.target.value)
}
const handleOptionalChange = (e: React.ChangeEvent) => {
@@ -56,110 +50,103 @@ const SendForm = ({ version }: SendFormProps) => {
-
-
-
-
- {messageType.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
- {version === 'template' && (
+
+
diff --git a/src/components/direct/xdr-edge/send/SendResult.tsx b/src/components/direct/xdr-edge/send/SendResult.tsx
new file mode 100644
index 00000000..c8c89abe
--- /dev/null
+++ b/src/components/direct/xdr-edge/send/SendResult.tsx
@@ -0,0 +1,89 @@
+import { Box, Typography } from '@mui/material'
+import palette from '@/styles/palette'
+import ResultsComponent from '../ResultsComponent'
+//import { Check } from '@mui/icons-material'
+import ErrorIcon from '@mui/icons-material/Error'
+
+interface ResultsComponentProps {
+ response: ContentProps
+ disabled?: boolean
+ buttonTitle: string
+}
+type ContentProps = {
+ success: boolean
+ message: string
+ payload: string
+}
+const Content = (response: ContentProps) => {
+ return (
+
+ {response.success && (
+ <>
+
+
+
+
+ Sent Message
+
+
+
+ {response.payload}
+
+
+
+
+
+ Response
+
+
+
+ {response.message}
+
+
+
+ >
+ )}
+ {!response.success && (
+ <>
+
+
+
+
+ Failed
+
+
+
+
+ {response.message}
+
+ >
+ )}
+
+ )
+}
+const SendResult = ({ response, buttonTitle }: ResultsComponentProps) => {
+ return (
+
+ }
+ />
+ )
+}
+
+export default SendResult
diff --git a/src/components/direct/xdr-edge/send/TemplateTab.tsx b/src/components/direct/xdr-edge/send/TemplateTab.tsx
index 7eb0ace7..9f673d97 100644
--- a/src/components/direct/xdr-edge/send/TemplateTab.tsx
+++ b/src/components/direct/xdr-edge/send/TemplateTab.tsx
@@ -1,6 +1,10 @@
import SendForm from './SendForm'
-const Template = () => {
- return
+
+interface TemplateProps {
+ sampleCCDAFiles: string[]
+}
+const Template = ({ sampleCCDAFiles }: TemplateProps) => {
+ return
}
export default Template
diff --git a/src/components/home/SiteHomeRows.tsx b/src/components/home/SiteHomeRows.tsx
index 631e3050..c62c2097 100644
--- a/src/components/home/SiteHomeRows.tsx
+++ b/src/components/home/SiteHomeRows.tsx
@@ -18,7 +18,7 @@ import {
lanternSvg,
referenceDataSvg,
} from '@public/home'
-import ONCLogo from '@public/shared/ONCLogo-backgroundImage.png'
+import ONCLogo from '@public/shared/LogoBackgroundImage.png'
import Image from 'next/image'
import CardWithImageHome from '@shared/CardWithImageHome'
import SectionHeader from '../shared/SectionHeader'
@@ -105,7 +105,8 @@ export default function SiteHomeRows() {
overflowX: 'clip',
width: '100%',
height: '100%',
- objectFit: 'contain',
+ objectFit: 'none',
+ objectPosition: 'right',
}}
src={ONCLogo}
alt="ONC Logo"
diff --git a/src/components/shared/SwitchWIthLabel.tsx b/src/components/shared/SwitchWithLabel.tsx
similarity index 84%
rename from src/components/shared/SwitchWIthLabel.tsx
rename to src/components/shared/SwitchWithLabel.tsx
index 6b0dad5e..7243a613 100644
--- a/src/components/shared/SwitchWIthLabel.tsx
+++ b/src/components/shared/SwitchWithLabel.tsx
@@ -14,6 +14,7 @@ interface SwitchWithLabelProps {
handleToggleSwitch: (event: React.ChangeEvent) => void
labelText: string
labelOnRight?: boolean
+ name?: string
}
export default function SwitchWithLabel({
@@ -21,8 +22,11 @@ export default function SwitchWithLabel({
handleToggleSwitch: handleChangeSwitch,
labelText,
labelOnRight,
+ name,
}: SwitchWithLabelProps) {
- const SwitchControl =
+ const SwitchControl = (
+
+ )
return labelOnRight ? (
diff --git a/src/components/shared/nav/CombinedNavAndAppBar.tsx b/src/components/shared/nav/CombinedNavAndAppBar.tsx
index dc3221c1..8b56a872 100644
--- a/src/components/shared/nav/CombinedNavAndAppBar.tsx
+++ b/src/components/shared/nav/CombinedNavAndAppBar.tsx
@@ -8,12 +8,8 @@ import SiteAppBar from '@/components/shared/nav/app-bar/SiteAppBar'
import Nav from '@/components/shared/nav/nav/Nav'
export default function CombinedNavAndAppBar() {
- const handleAuthChange = (event: React.ChangeEvent) => {
- //setAuth(event.target.checked)
- }
-
- // TODO: default to false based on DEV_MODE env var
- const [open, setOpen] = React.useState(true)
+ // When false, nav expanded by default (prod). When true, nav collapsed (dev)
+ const [open, setOpen] = React.useState(!(process.env.NEXT_PUBLIC_IS_DEBUG_MODE === 'true'))
const handleDrawerOpen = () => {
setOpen(true)
}
@@ -25,7 +21,7 @@ export default function CombinedNavAndAppBar() {
-
+
)
}
diff --git a/src/components/shared/nav/actions.ts b/src/components/shared/nav/actions.ts
new file mode 100644
index 00000000..c6557355
--- /dev/null
+++ b/src/components/shared/nav/actions.ts
@@ -0,0 +1,59 @@
+'use server'
+const ETT_API_URL = process.env.ETT_API_URL
+
+export async function registerAccount({ username, password }: { username: string; password: string }) {
+ const ettAPIUrl = `${ETT_API_URL}/login/register`
+ try {
+ const response = await fetch(ettAPIUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ password: password,
+ username: username,
+ }),
+ })
+ const data = await response.json()
+ if (!response.ok) {
+ console.log(`Error: ${data}`)
+ }
+ return data
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ return JSON.stringify({
+ status: 500,
+ success: false,
+ error: error.message || 'An error occurred',
+ })
+ }
+ }
+}
+
+export async function forgotPassword(username: string) {
+ const ettAPIUrl = `${ETT_API_URL}/passwordManager/forgot`
+ try {
+ const response = await fetch(ettAPIUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ username,
+ }),
+ })
+ const data = await response.json()
+ if (!response.ok) {
+ console.log(`Error: ${data}`)
+ }
+ return data
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ return JSON.stringify({
+ status: 500,
+ success: false,
+ error: error.message || 'An error occurred',
+ })
+ }
+ }
+}
diff --git a/src/components/shared/nav/app-bar/Auth.tsx b/src/components/shared/nav/app-bar/Auth.tsx
index 337bd76c..74d1af11 100644
--- a/src/components/shared/nav/app-bar/Auth.tsx
+++ b/src/components/shared/nav/app-bar/Auth.tsx
@@ -1,18 +1,25 @@
import { AccountCircle } from '@mui/icons-material'
-import { Box, Button, Menu, MenuItem, Popover, Typography } from '@mui/material'
+import { Box, Button, Divider, Link, Menu, MenuItem, Popover, Typography } from '@mui/material'
import Login from './Login'
import { signOut, useSession } from 'next-auth/react'
import { useState } from 'react'
+import palette from '@/styles/palette'
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
export default function Auth(props: any) {
const [anchorEl, setAnchorEl] = useState(null)
+ const [isCreatingAccount, setIsCreatingAccount] = useState(false)
+ const [isForgotPassword, setIsForgotPassword] = useState(false)
+ const [message, setMessage] = useState({ message: '', severity: 'info' })
const { data: session } = useSession()
const handleAuthMenu = (event: React.MouseEvent) => {
setAnchorEl(event.currentTarget)
}
const handleAuthClose = () => {
setAnchorEl(null)
+ setIsCreatingAccount(false)
+ setIsForgotPassword(false)
+ setMessage({ message: '', severity: 'info' })
}
return (
@@ -27,14 +34,14 @@ export default function Auth(props: any) {
onClick={handleAuthMenu}
color="inherit"
>
-
+
{session ? `${session.user?.name}` : 'LOGIN'}
{''}
) : (
@@ -85,7 +103,15 @@ export default function Auth(props: any) {
open={Boolean(anchorEl)}
onClose={handleAuthClose}
>
-
+
)}
diff --git a/src/components/shared/nav/app-bar/Login.tsx b/src/components/shared/nav/app-bar/Login.tsx
index 4991627e..dc79f1f9 100644
--- a/src/components/shared/nav/app-bar/Login.tsx
+++ b/src/components/shared/nav/app-bar/Login.tsx
@@ -10,90 +10,272 @@ import {
CardContent,
IconButton,
Button,
- styled,
+ Box,
+ Alert,
} from '@mui/material'
import { signIn } from 'next-auth/react'
import React, { useState } from 'react'
import { Visibility, VisibilityOff } from '@mui/icons-material'
-
-const StyledLoginButtonRowWrapper = styled('div')(() => ({
- paddingTop: 24,
- width: '100%',
- display: 'flex',
- justifyContent: 'space-around',
-}))
+import { registerAccount, forgotPassword } from '../actions'
+import _ from 'lodash'
const LoginButtonStyle = {
+ padding: '10px 0',
width: '100%',
margin: 1,
}
-const Login = (props: { handleAuthClose: () => void }) => {
+const Login = ({
+ handleAuthClose,
+ setIsCreatingAccount,
+ isCreatingAccount,
+ message,
+ setMessage,
+ isForgotPassword,
+ setIsForgotPassword,
+}: {
+ handleAuthClose: () => void
+ setIsCreatingAccount: React.Dispatch>
+ isCreatingAccount: boolean
+ message: { message: string; severity: string }
+ setMessage: React.Dispatch>
+ isForgotPassword: boolean
+ setIsForgotPassword: React.Dispatch>
+}) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
+ const [repeatPassword, setRepeatPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
- const handleLogin = () => {
+ const handleLogin = (e: React.FormEvent) => {
+ e.preventDefault()
signIn('credentials', { username: email, password: password })
}
+ const handleCreateAccount = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (password === repeatPassword) {
+ registerAccount({ username: email, password: password }).then((data) => {
+ if (data === true) {
+ signIn('credentials', { username: email, password: password })
+ } else {
+ setMessage({ message: 'User already exists', severity: 'error' })
+ }
+ })
+ } else {
+ setMessage({ message: 'Passwords do not match', severity: 'error' })
+ }
+ }
+
+ const handleForgotPassword = async (e: React.FormEvent) => {
+ e.preventDefault()
+ forgotPassword(email).then((data) => {
+ if (data !== true) {
+ setMessage({ message: 'This username does not exist', severity: 'error' })
+ }
+ })
+ }
+
const handleClickShowPassword = () => {
setShowPassword((prev) => !prev)
}
+ const LoginGrid = (
+ handleLogin(e)} sx={{ backgroundColor: palette.white }}>
+
+
+ setEmail(e.target.value)}
+ />
+
+
+ setPassword(e.target.value)}
+ InputProps={{
+ endAdornment: (
+
+
+ {showPassword ? : }
+
+
+ ),
+ }}
+ />
+
+
+
+
+ SIGN IN
+
+ setIsCreatingAccount(true)}
+ color="secondary"
+ >
+ CREATE ACCOUNT
+
+
+
+
+
+ setIsForgotPassword(true)} color="secondary" variant="text">
+ FORGOT PASSWORD?
+
+
+
+ )
+
+ const CreateAccountGrid = (
+ handleCreateAccount(e)} sx={{ backgroundColor: palette.white }}>
+
+
+ setEmail(e.target.value)}
+ />
+
+
+ setPassword(e.target.value)}
+ InputProps={{
+ endAdornment: (
+
+
+ {showPassword ? : }
+
+
+ ),
+ }}
+ />
+
+
+ setRepeatPassword(e.target.value)}
+ InputProps={{
+ endAdornment: (
+
+
+ {showPassword ? : }
+
+
+ ),
+ }}
+ />
+
+
+
+
+ SIGN UP
+
+
+
+
+
+ )
+
+ const ForgotPasswordGrid = (
+ handleForgotPassword(e)} sx={{ backgroundColor: palette.white }}>
+
+
+ setEmail(e.target.value)}
+ />
+
+
+
+
+ SEND
+
+ handleAuthClose()}
+ color="secondary"
+ >
+ CANCEL
+
+
+
+
+
+ )
+
return (
<>
-
- props.handleAuthClose()}>
-
-
- }
- titleTypographyProps={{ fontWeight: '600', variant: 'h3', color: palette.primary }}
- />
-
-
-
- setEmail(e.target.value)}
- />
-
-
- setPassword(e.target.value)}
- InputProps={{
- endAdornment: (
-
-
- {showPassword ? : }
-
-
- ),
- }}
- />
-
-
-
-
- SIGN IN
-
-
-
-
-
-
+
+ {isForgotPassword ? (
+
+ handleAuthClose()}>
+
+
+ }
+ titleTypographyProps={{ fontWeight: '600', variant: 'h3', color: palette.primary }}
+ />
+ {ForgotPasswordGrid}
+
+ ) : (
+
+ handleAuthClose()}>
+
+
+ }
+ titleTypographyProps={{ fontWeight: '600', variant: 'h3', color: palette.primary }}
+ />
+ {isCreatingAccount ? CreateAccountGrid : LoginGrid}
+
+ )}
+
+
+ {!_.isEmpty(message.message) && (
+ setMessage({ message: '', severity: 'info' })}
+ >
+ {message.message}
+
+ )}
+
>
)
}
diff --git a/src/components/shared/nav/nav/Nav.tsx b/src/components/shared/nav/nav/Nav.tsx
index 9104ff2b..271a23cd 100644
--- a/src/components/shared/nav/nav/Nav.tsx
+++ b/src/components/shared/nav/nav/Nav.tsx
@@ -63,9 +63,8 @@ const Drawer = styled(MuiDrawer, {
interface SiteNavProps {
open: boolean
handleDrawerClose: () => void
- handleAuthChange: (event: React.ChangeEvent) => void
}
-export default function SiteNav({ open, handleDrawerClose, handleAuthChange }: SiteNavProps) {
+export default function SiteNav({ open, handleDrawerClose }: SiteNavProps) {
const theme = useTheme()
return (
diff --git a/src/services/analytics.ts b/src/services/analytics.ts
new file mode 100644
index 00000000..cd7f4893
--- /dev/null
+++ b/src/services/analytics.ts
@@ -0,0 +1,26 @@
+'use client'
+// Note: Explicitly defining 'use client' as this funciton uses 'window', which is part of the browser
+/*
+eventType: Describes the type of action being tracked, Button Click Form Submission, Dropdown Selection,
+Link Click, Sub Menu Anchor Link Click, etc.
+eventCategory: Groups events together, such as by a specific tool name or navigation page
+eventLabel: Provides specific details for the event such as what the action and outcome is after the event is fired
+*/
+const eventTrack = (eventType: string, eventCategory: string, eventLabel: string) => {
+ if (process.env.NEXT_PUBLIC_IS_EVENT_TRACKING === 'true') {
+ if (typeof window.gtag === 'function') {
+ console.info({
+ event: eventType,
+ eventCategory: eventCategory,
+ eventLabel: eventLabel,
+ })
+
+ window.gtag('event', eventType, {
+ event_category: eventCategory,
+ event_label: eventLabel,
+ })
+ }
+ }
+}
+
+export default eventTrack