From 24ae47b92997023c077f4342feb62a505c56a6f0 Mon Sep 17 00:00:00 2001 From: Liu Date: Fri, 2 Sep 2022 09:56:18 -0700 Subject: [PATCH] working with multiple forms --- .graphqlconfig.yml | 15 + .../backend/api/b4hGraphQL/cli-inputs.json | 12 + .../backend/api/b4hGraphQL/parameters.json | 11 + .../api/b4hGraphQL/resolvers/README.md | 2 + amplify/backend/api/b4hGraphQL/schema.graphql | 15 + .../b4hGraphQL/stacks/CustomResources.json | 58 + .../api/b4hGraphQL/transform.conf.json | 4 + amplify/backend/backend-config.json | 27 +- .../storage/b4hstorage/cli-inputs.json | 5 +- .../amplify-dependent-resources-ref.d.ts | 4 + src/App.js | 17 +- src/Components/navbar.js | 17 +- src/Helpers/notification.js | 27 - src/Pages/form.js | 213 +- src/Pages/upload.js | 69 +- src/graphql/mutations.js | 63 + src/graphql/queries.js | 76 + src/graphql/schema.json | 2465 +++++++++++++++++ src/graphql/subscriptions.js | 54 + 19 files changed, 3028 insertions(+), 126 deletions(-) create mode 100644 .graphqlconfig.yml create mode 100644 amplify/backend/api/b4hGraphQL/cli-inputs.json create mode 100644 amplify/backend/api/b4hGraphQL/parameters.json create mode 100644 amplify/backend/api/b4hGraphQL/resolvers/README.md create mode 100644 amplify/backend/api/b4hGraphQL/schema.graphql create mode 100644 amplify/backend/api/b4hGraphQL/stacks/CustomResources.json create mode 100644 amplify/backend/api/b4hGraphQL/transform.conf.json delete mode 100644 src/Helpers/notification.js create mode 100644 src/graphql/mutations.js create mode 100644 src/graphql/queries.js create mode 100644 src/graphql/schema.json create mode 100644 src/graphql/subscriptions.js diff --git a/.graphqlconfig.yml b/.graphqlconfig.yml new file mode 100644 index 0000000..1b919fc --- /dev/null +++ b/.graphqlconfig.yml @@ -0,0 +1,15 @@ +projects: + b4hGraphQL: + schemaPath: src/graphql/schema.json + includes: + - src/graphql/**/*.js + excludes: + - ./amplify/** + extensions: + amplify: + codeGenTarget: javascript + generatedFileName: '' + docsFilePath: src/graphql +extensions: + amplify: + version: 3 diff --git a/amplify/backend/api/b4hGraphQL/cli-inputs.json b/amplify/backend/api/b4hGraphQL/cli-inputs.json new file mode 100644 index 0000000..80da6e1 --- /dev/null +++ b/amplify/backend/api/b4hGraphQL/cli-inputs.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "serviceConfiguration": { + "apiName": "b4hGraphQL", + "serviceName": "AppSync", + "gqlSchemaPath": "C:\\Users\\eliu10\\basics4health\\amplify\\backend\\api\\b4hGraphQL\\schema.graphql", + "defaultAuthType": { + "mode": "AMAZON_COGNITO_USER_POOLS", + "cognitoUserPoolId": "authbasics4health41656259" + } + } +} \ No newline at end of file diff --git a/amplify/backend/api/b4hGraphQL/parameters.json b/amplify/backend/api/b4hGraphQL/parameters.json new file mode 100644 index 0000000..b8d7c2b --- /dev/null +++ b/amplify/backend/api/b4hGraphQL/parameters.json @@ -0,0 +1,11 @@ +{ + "AppSyncApiName": "b4hGraphQL", + "DynamoDBBillingMode": "PAY_PER_REQUEST", + "DynamoDBEnableServerSideEncryption": false, + "AuthCognitoUserPoolId": { + "Fn::GetAtt": [ + "authbasics4health41656259", + "Outputs.UserPoolId" + ] + } +} \ No newline at end of file diff --git a/amplify/backend/api/b4hGraphQL/resolvers/README.md b/amplify/backend/api/b4hGraphQL/resolvers/README.md new file mode 100644 index 0000000..89e564c --- /dev/null +++ b/amplify/backend/api/b4hGraphQL/resolvers/README.md @@ -0,0 +1,2 @@ +Any resolvers that you add in this directory will override the ones automatically generated by Amplify CLI and will be directly copied to the cloud. +For more information, visit [https://docs.amplify.aws/cli/graphql-transformer/resolvers](https://docs.amplify.aws/cli/graphql-transformer/resolvers) \ No newline at end of file diff --git a/amplify/backend/api/b4hGraphQL/schema.graphql b/amplify/backend/api/b4hGraphQL/schema.graphql new file mode 100644 index 0000000..2f3333e --- /dev/null +++ b/amplify/backend/api/b4hGraphQL/schema.graphql @@ -0,0 +1,15 @@ + +type Form @model @auth(rules: [{ allow: owner }, { allow: private, operations: [read] }]) { + id: ID! + name: String @index(name: "byFormName", queryField: "getFormByName") + otherUser: String + owner: String + file: S3Object +} + +type S3Object { + bucket: String! + region: String! + key: String! +} + \ No newline at end of file diff --git a/amplify/backend/api/b4hGraphQL/stacks/CustomResources.json b/amplify/backend/api/b4hGraphQL/stacks/CustomResources.json new file mode 100644 index 0000000..f95feea --- /dev/null +++ b/amplify/backend/api/b4hGraphQL/stacks/CustomResources.json @@ -0,0 +1,58 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "An auto-generated nested stack.", + "Metadata": {}, + "Parameters": { + "AppSyncApiId": { + "Type": "String", + "Description": "The id of the AppSync API associated with this project." + }, + "AppSyncApiName": { + "Type": "String", + "Description": "The name of the AppSync API", + "Default": "AppSyncSimpleTransform" + }, + "env": { + "Type": "String", + "Description": "The environment name. e.g. Dev, Test, or Production", + "Default": "NONE" + }, + "S3DeploymentBucket": { + "Type": "String", + "Description": "The S3 bucket containing all deployment assets for the project." + }, + "S3DeploymentRootKey": { + "Type": "String", + "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." + } + }, + "Resources": { + "EmptyResource": { + "Type": "Custom::EmptyResource", + "Condition": "AlwaysFalse" + } + }, + "Conditions": { + "HasEnvironmentParameter": { + "Fn::Not": [ + { + "Fn::Equals": [ + { + "Ref": "env" + }, + "NONE" + ] + } + ] + }, + "AlwaysFalse": { + "Fn::Equals": ["true", "false"] + } + }, + "Outputs": { + "EmptyOutput": { + "Description": "An empty output. You may delete this if you have at least one resource above.", + "Value": "" + } + } +} diff --git a/amplify/backend/api/b4hGraphQL/transform.conf.json b/amplify/backend/api/b4hGraphQL/transform.conf.json new file mode 100644 index 0000000..98e1e19 --- /dev/null +++ b/amplify/backend/api/b4hGraphQL/transform.conf.json @@ -0,0 +1,4 @@ +{ + "Version": 5, + "ElasticsearchWarning": true +} \ No newline at end of file diff --git a/amplify/backend/backend-config.json b/amplify/backend/backend-config.json index 969609e..1a3f3de 100644 --- a/amplify/backend/backend-config.json +++ b/amplify/backend/backend-config.json @@ -79,6 +79,30 @@ ] } ] + }, + "b4hGraphQL": { + "service": "AppSync", + "providerPlugin": "awscloudformation", + "dependsOn": [ + { + "category": "auth", + "resourceName": "basics4health41656259", + "attributes": [ + "UserPoolId" + ] + } + ], + "output": { + "authConfig": { + "defaultAuthentication": { + "authenticationType": "AMAZON_COGNITO_USER_POOLS", + "userPoolConfig": { + "userPoolId": "authbasics4health41656259" + } + }, + "additionalAuthenticationProviders": [] + } + } } }, "storage": { @@ -102,5 +126,6 @@ } ] } - } + }, + "custom": {} } \ No newline at end of file diff --git a/amplify/backend/storage/b4hstorage/cli-inputs.json b/amplify/backend/storage/b4hstorage/cli-inputs.json index 491bd2d..a26d55c 100644 --- a/amplify/backend/storage/b4hstorage/cli-inputs.json +++ b/amplify/backend/storage/b4hstorage/cli-inputs.json @@ -4,8 +4,9 @@ "bucketName": "basics4health", "storageAccess": "auth", "guestAccess": [], - "authAccess": [], - "triggerFunction": "NONE", + "authAccess": [ + "READ" + ], "groupAccess": { "Admins": [ "CREATE_AND_UPDATE", diff --git a/amplify/backend/types/amplify-dependent-resources-ref.d.ts b/amplify/backend/types/amplify-dependent-resources-ref.d.ts index e4a77cf..4c4db8f 100644 --- a/amplify/backend/types/amplify-dependent-resources-ref.d.ts +++ b/amplify/backend/types/amplify-dependent-resources-ref.d.ts @@ -26,6 +26,10 @@ export type AmplifyDependentResourcesAttributes = { "RootUrl": "string", "ApiName": "string", "ApiId": "string" + }, + "b4hGraphQL": { + "GraphQLAPIIdOutput": "string", + "GraphQLAPIEndpointOutput": "string" } }, "storage": { diff --git a/src/App.js b/src/App.js index 95d21b7..03cb059 100644 --- a/src/App.js +++ b/src/App.js @@ -15,11 +15,22 @@ Amplify.configure(awsExports); function App(props) { const client = props.client; - const [patient, setPatient] = useState(); + const [patient, setPatient] = useState(''); + const [loading, setLoading] = useState(true); useEffect(() => { - client.patient.read().then((patient) => setPatient(patient)); - }, [client.patient]); + getPatient(); + }, []); + + async function getPatient() { + let pat = await client.patient.read(); + setPatient(pat); + setLoading(false); + } + + if (loading) { + return Loading... + } return ( <> diff --git a/src/Components/navbar.js b/src/Components/navbar.js index 9d68277..942c033 100644 --- a/src/Components/navbar.js +++ b/src/Components/navbar.js @@ -2,11 +2,13 @@ import { useState } from "react"; import { AppBar, Toolbar, Button, Typography } from "@mui/material"; import Sidebar from './sidebar'; import { Auth } from 'aws-amplify'; +import { useNavigate } from 'react-router-dom'; import { useEffect } from "react"; -// import AdminStatus from "../adminStatus"; +import AdminStatus from "../Helpers/adminStatus"; export default function Navbar() { const [admin, setAdmin] = useState(false); + const navigate = useNavigate(); useEffect(() => { welcomeUser(); @@ -21,21 +23,12 @@ export default function Navbar() { async function signOut() { await Auth.signOut(); + navigate("/"); window.location.reload(); } async function isAdmin() { - let user = await Auth.currentAuthenticatedUser(); - let group = user.signInUserSession.accessToken.payload['cognito:groups']; - if (group === undefined) { - setAdmin(false); - } else { - if (group.includes('Admins')) { - setAdmin(true); - } - } - // let adminStatus = await IsAdmin(); - // setAdmin(await AdminStatus()); + setAdmin(await AdminStatus()); } return ( diff --git a/src/Helpers/notification.js b/src/Helpers/notification.js deleted file mode 100644 index 875621f..0000000 --- a/src/Helpers/notification.js +++ /dev/null @@ -1,27 +0,0 @@ -import { toast, Zoom} from 'react-toastify'; - -export function Notify(type, text) { - if (type === 'success') { - toast.success(text, { - position: "top-center", - autoClose: 3000, - transition: Zoom, - hideProgressBar: true, - closeOnClick: true, - pauseOnHover: true, - draggable: false, - progress: undefined, - }); - } else { - toast.error(text, { - position: "top-center", - autoClose: 3000, - transition: Zoom, - hideProgressBar: true, - closeOnClick: true, - pauseOnHover: true, - draggable: false, - progress: undefined, - }); - } - } \ No newline at end of file diff --git a/src/Pages/form.js b/src/Pages/form.js index be54556..51cd299 100644 --- a/src/Pages/form.js +++ b/src/Pages/form.js @@ -1,51 +1,121 @@ import { useEffect, useState } from 'react'; -import { FhirQ } from '../questionnaire'; import axios from 'axios'; -import { Alert, Collapse, Button, TextField, IconButton, InputAdornment } from '@mui/material' +import { Alert, Collapse, Button, FormControl, InputLabel, Select, MenuItem } from '@mui/material' import SendIcon from '@mui/icons-material/Send'; -import SearchIcon from '@mui/icons-material/Search' -import { Auth, Signer } from 'aws-amplify'; +import AdminStatus from '../Helpers/adminStatus'; +import { Auth, Signer, Storage, API, graphqlOperation } from 'aws-amplify'; +import { listForms, getFormByName } from '../graphql/queries'; -function Form(thistest) { - const [patientID, setPatientID] = useState(''); +function Form(props) { + const patientID = props.param.id; + const [formLoaded, setFormLoaded] = useState(false); + const [availableForms, setAvailableForms] = useState([]); + const [selectedForm, setSelectedForm] = useState(''); const [buttonClicked, setButtonClicked] = useState(''); - const [isFieldError, setIsFieldError] = useState(false); - const [errorText, setErrorText] = useState(''); const [alert, setAlert] = useState(false); const [alertContent, setAlertContent] = useState(''); useEffect(() => { - renderForm(); + fetchForms() + patientData(); }, []); - async function renderForm() { - let formDef = FhirQ; - window.LForms.Util.addFormToPage(formDef, 'formContainer'); + async function fetchForms() { + try { + let user = await Auth.currentAuthenticatedUser(); + let isAdmin = await AdminStatus(); + let forms; + if (isAdmin) { + forms = await API.graphql(graphqlOperation(listForms)) + } else { + forms = await API.graphql(graphqlOperation(listForms, {filter: {otherUser: {eq: user.username}}})) + } + + let formNames = forms.data.listForms.items; + setAvailableForms(formNames); + } catch (error) { + console.log(error) + } + } + + async function patientData() { + let name = 'Patient Name:  ' + props.param.name[0].given + ' ' + props.param.name[0].family; + let id = 'Patient ID:  ' + patientID; + document.getElementById('patientDataContainer').innerHTML = name + ',  ' + id; } + // async function renderForm() { + // let s3Key = await getS3Key(); + // let formDef = await Storage.get(s3Key, { download: true }); + // let formResult = await formDef.Body.text() + // window.LForms.Util.addFormToPage(formResult, 'formContainer'); + // setFormLoaded(true); + // } + async function loadResponse() { - let searchParameter = '?subject=Patient/' + patientID; - let signedURL = await signRequest('get', searchParameter); + let s3Key = await getS3Key(); + let formDef = await Storage.get(s3Key, { download: true }); + let formResult = await formDef.Body.text() + let formObj = JSON.parse(formResult) + + let signedURL = await signRequest('get'); + // await axios.get(signedURL).then(async (resp) => { + // console.log(resp) + // let correctForm = null; + // const returnedForms = resp.data.entry; + // const formURL = await getS3URL(); + // returnedForms.forEach(element => { + // if (element.resource.questionnaire === formURL ) { + // correctForm = element.resource; + // } + // }); + // if (correctForm !== null) { + // let lhcForm = window.LForms.Util.convertFHIRQuestionnaireToLForms(formObj, 'R4'); + // let formWithUserData = window.LForms.Util.mergeFHIRDataIntoLForms("QuestionnaireResponse", correctForm, lhcForm, "R4"); + // window.LForms.Util.addFormToPage(formWithUserData, 'formContainer'); + // setFormLoaded(true); + // } else { + // window.LForms.Util.addFormToPage(formResult, 'formContainer'); + // setFormLoaded(true); + // } + // } + // ) await axios.get(signedURL).then((resp) => { + console.log(resp) if (resp.data["entry"].length === 1) { - fieldError('Patient does not exist'); + window.LForms.Util.addFormToPage(formResult, 'formContainer'); + setFormLoaded(true); } else if (resp.data["entry"][1]["search"]["mode"] === "match") { - let fhirForm = FhirQ; - let lhcForm = window.LForms.Util.convertFHIRQuestionnaireToLForms(fhirForm, 'R4'); + let lhcForm = window.LForms.Util.convertFHIRQuestionnaireToLForms(formObj, 'R4'); let formWithUserData = window.LForms.Util.mergeFHIRDataIntoLForms("QuestionnaireResponse", resp.data["entry"][1]["resource"], lhcForm, "R4"); window.LForms.Util.addFormToPage(formWithUserData, 'formContainer'); - setAlertContent('Form loaded'); - setAlert(true); + setFormLoaded(true); } }) } async function sendToHealthlake() { - let searchParameter = '?subject=Patient/' + patientID; - let signedURL = await signRequest('get', searchParameter); + let signedURL = await signRequest('get'); + // await axios.get(signedURL).then(async (resp) => { + // let formID = null; + // const returnedForms = resp.data.entry; + // const formURL = await getS3URL(); + // returnedForms.forEach(element => { + // if (element.resource.questionnaire === formURL) { + // formID = element.resource.id + // } + // }); + // if (formID !== null) { + // updateResponse(formID) + // } else { + // storeResponse() + // } + // } + // ) await axios.get(signedURL).then((resp) => { + console.log(resp) if (resp.data["entry"].length === 1) { storeResponse(); } else if (resp.data["entry"][1]["search"]["mode"] === "match") { @@ -99,9 +169,29 @@ function Form(thistest) { let resourceType = 'QuestionnaireResponse'; let endpoint = dataStore + resourceType; - let fhirQR = window.LForms.Util.getFormFHIRData('QuestionnaireResponse', 'R4'); - fhirQR.subject = { - reference: "Patient/" + patientID + let formURL = await getS3URL(); + let user = await Auth.currentAuthenticatedUser(); + + let fhirQR; + + if (requestMethod !== 'get') { + fhirQR = window.LForms.Util.getFormFHIRData('QuestionnaireResponse', 'R4'); + + fhirQR.subject = { + reference: "Patient/" + patientID + } + fhirQR.meta.tag = [ + { + "code": "lformsVersion: 30.0.0" + }, + { + "code": formURL + } + ] + fhirQR.questionnaire = formURL + fhirQR.author = { + reference: user.username + } } const serviceInfo = { @@ -133,67 +223,56 @@ function Form(thistest) { delete signedRequest.headers['host']; signedRequest.headers['content-type'] = 'application/json'; return signedRequest; - } else { - let endpoint = dataStore + resourceType + param; + } else if (requestMethod === 'get') { + // let searchParam = '?subject=Patient/' + patientID + '&questionnaire=' + formURL + let searchParam = '?subject=Patient/' + patientID + '&_tag=' + formURL + let endpoint = dataStore + resourceType + searchParam; return Signer.signUrl(endpoint, credentials) } } - function numbersOnly(e) { - const re = /^[0-9\b]+$/; - - if (e.target.value === '' || re.test(e.target.value)) { - setPatientID(e.target.value); - } - } + async function getS3Key() { + let chosenForm = await API.graphql(graphqlOperation(getFormByName, {name: selectedForm})); + let key = chosenForm.data.getFormByName.items[0].file.key; + return key + } - function fieldError(text) { - setIsFieldError(true); - setErrorText(text) - document.querySelector('#patient-id').scrollIntoView({block: 'center', inline: 'start'}); + async function getS3URL() { + let s3Key = await getS3Key() + let signedFormURL = await Storage.get(s3Key) + let formURL = signedFormURL.substring(0, signedFormURL.indexOf('?X-Amz-Algorithm')) + return formURL } - function handleSubmit(e) { + async function handleSubmit(e) { e.preventDefault(); - if (patientID === '') { - fieldError('Field required'); - } else if (patientID.length !== 10) { - fieldError('Please enter a valid ID number'); + if (buttonClicked === 'store') { + sendToHealthlake(); + } else if (buttonClicked === 'loadForm') { + loadResponse(); } else { - setIsFieldError(false); - setErrorText(''); - if (buttonClicked === 'store') { - sendToHealthlake(); - } else { - loadResponse(); - } + // loadResponse(); } } return ( <> +
{alert ? setAlert(false)}>{alertContent} : <> }
- - setButtonClicked('load')}> - - - - )}} - /> +
+ + Select a form + + + +
+ {/* */}
- + {formLoaded ? : null} ); diff --git a/src/Pages/upload.js b/src/Pages/upload.js index 4339229..7f60627 100644 --- a/src/Pages/upload.js +++ b/src/Pages/upload.js @@ -1,12 +1,19 @@ import { useState } from 'react'; import { Collapse, Alert, Button, Box, Stack, TextField } from "@mui/material"; import { UploadFile } from "@mui/icons-material"; -import { Storage } from 'aws-amplify'; +import { Storage, API, graphqlOperation } from 'aws-amplify'; +import { createForm } from '../graphql/mutations'; +import awsExports from '../aws-exports'; export default function Upload() { + const [formTitle, setFormTitle] = useState(''); + const [formUser, setFormUser] = useState(''); + const [fieldError, setFieldError] = useState(false); + const [errorText, setErrorText] = useState(''); const [file, setFile] = useState(); const [selectedFile, setSelectedFile] = useState(''); const [alert, setAlert] = useState(false); + const [alertType, setAlertType] = useState('') const [alertContent, setAlertContent] = useState(''); async function handleChange(e) { @@ -17,25 +24,47 @@ export default function Upload() { async function handleSubmit(e) { e.preventDefault(); - try { - await Storage.put(file.name, file, { - level: "protected", - contentType: "application/json" - }) - .then(resp => { - if (resp.key === file.name) { + if (formTitle === '') { + setFieldError(true); + setErrorText("Please enter form title"); + } else { + setFieldError(false); + setErrorText(''); + try { + await Storage.put(file.name, file, { + contentType: "application/json" + }) + .then(async (resp) => { + const fileData = { + input: { + name: formTitle, + otherUser: formUser, + file: { + bucket: awsExports.aws_user_files_s3_bucket, + region: awsExports.aws_user_files_s3_bucket_region, + key: file.name + } + } + } + await API.graphql(graphqlOperation(createForm, fileData)); + if (resp.key === file.name) { + setAlertType('success') + setAlert(true); + setAlertContent('File uploaded successfully'); + } + }) + } catch (error) { + console.log(error) + setAlertType('error') setAlert(true); - setAlertContent('File uploaded successfully'); + setAlertContent('Error uploading file'); } - }) - } catch (error) { - console.log("Error uploading file: ", error); } } return ( <> - {alert ? setAlert(false)}>{alertContent} : <> } + {alert ? setAlert(false)}>{alertContent} : <> }

Upload a new questionnaire

{setFormTitle(e.target.value)}} + error={fieldError} + helperText={errorText} + inputProps={{autoComplete: 'off'}} + /> + {setFormUser(e.target.value)}} inputProps={{autoComplete: 'off'}} />