Skip to content

Commit

Permalink
Merge pull request #17211 from CDCgov/experience/16073/message-testin…
Browse files Browse the repository at this point in the history
…g-download-feature

Experience/16073/message-testing-download-feature
  • Loading branch information
etanb authored Jan 31, 2025
2 parents 22ed376 + e15bb89 commit ec66eb8
Show file tree
Hide file tree
Showing 7 changed files with 698 additions and 112 deletions.
1 change: 1 addition & 0 deletions frontend-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@microsoft/applicationinsights-web": "^3.3.4",
"@okta/okta-react": "^6.9.0",
"@okta/okta-signin-widget": "^7.27.2",
"@react-pdf/renderer": "^4.1.6",
"@rest-hooks/rest": "^3.0.3",
"@tanstack/react-query": "^5.64.0",
"@tanstack/react-query-devtools": "^5.64.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { Accordion, Icon, Tag } from "@trussworks/react-uswds";
import { RSMessageResult } from "../../../config/endpoints/reports";

export const MessageTestingAccordion = ({
accordionTitle,
priority,
resultData,
fieldsToRender,
fieldData,
}: {
accordionTitle: string;
priority: "error" | "warning";
resultData: RSMessageResult;
fieldsToRender: (keyof RSMessageResult)[];
fieldData: (string | boolean | undefined)[];
}) => {
const fieldID = accordionTitle.toLowerCase().split(" ").join("-");
const existingFields = fieldsToRender.filter((field) => Object.keys(resultData).includes(field));
const combinedFieldData = existingFields.flatMap((field) => resultData[field]);

// Immediately return if there's no warning/error data to display
if (combinedFieldData.length === 0) return;
if (fieldData.length === 0) return;

return (
<div key={`${fieldID}-accordion-wrapper`} className="padding-top-4 ">
Expand All @@ -37,20 +32,20 @@ export const MessageTestingAccordion = ({
<span className="font-body-lg">{accordionTitle}</span>

{priority === "error" && (
<Tag className="margin-left-1 bg-secondary-vivid">{combinedFieldData.length}</Tag>
<Tag className="margin-left-1 bg-secondary-vivid">{fieldData.length}</Tag>
)}

{priority === "warning" && (
<Tag className="margin-left-1 bg-accent-warm">{combinedFieldData.length}</Tag>
<Tag className="margin-left-1 bg-accent-warm">{fieldData.length}</Tag>
)}
</>
),
content: (
<div className="bg-white font-sans-sm padding-top-2 padding-bottom-2 padding-left-1 padding-right-1">
{combinedFieldData.map((item, index) => (
{fieldData.map((item, index) => (
<div key={index}>
<div>{item}</div>
{index < combinedFieldData.length - 1 && <hr className="rs-hr--half-margin" />}
{index < fieldData.length - 1 && <hr className="rs-hr--half-margin" />}
</div>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { Document, Font, Page, StyleSheet, Text, View } from "@react-pdf/renderer";
import PublicSansBold from "@uswds/uswds/fonts/public-sans/PublicSans-Bold.ttf";
import PublicSansRegular from "@uswds/uswds/fonts/public-sans/PublicSans-Regular.ttf";
import language from "./language.json";
import { prettifyJSON } from "../../../utils/misc";

interface MessageTestingPDFProps {
orgName: string;
receiverName: string;
testStatus: string;
filterFieldData: (string | boolean | undefined)[];
transformFieldData: (string | boolean | undefined)[];
warningFieldData: (string | boolean | undefined)[];
testMessage: string;
outputMessage: string;
isPassed: boolean;
}

Font.register({
family: "Public Sans Web",
fonts: [{ src: PublicSansRegular }, { src: PublicSansBold, fontWeight: "bold" }],
});

const styles = StyleSheet.create({
page: {
padding: 30,
fontSize: 12,
fontFamily: "Public Sans Web",
},
section: {
marginBottom: 20,
},
sectionTitle: {
fontSize: 14,
marginBottom: 6,
fontWeight: "bold",
},
text: {
marginBottom: 4,
},
bannerContainer: {
padding: 10,
marginBottom: 20,
},
bannerText: {
color: "black",
fontWeight: "bold",
},
bannerSuccess: {
backgroundColor: "#ecf3ec",
borderLeft: "4px solid #00a91c",
},
bannerWarning: {
backgroundColor: "#faf3d1",
borderLeft: "4px solid #ffbe2e",
},
bannerError: {
backgroundColor: "#f4e3db",
borderLeft: "4px solid #d54309",
},
codeBlock: {
backgroundColor: "#f5f5f5",
padding: 10,
fontFamily: "Courier", // or 'Roboto Mono' if you registered it
marginBottom: 10,
},
hr: {
borderBottomWidth: 1,
borderBottomColor: "#000",
marginVertical: 5,
},
});

const statusConfig = {
success: {
text: language.successAlertHeading,
className: styles.bannerSuccess,
},
warning: {
text: language.warningAlertHeading,
className: styles.bannerWarning,
},
error: {
text: language.errorAlertHeading,
className: styles.bannerError,
},
};

const MessageTestingPDF = ({
orgName,
receiverName,
testStatus,
filterFieldData,
transformFieldData,
warningFieldData,
testMessage,
outputMessage,
isPassed,
}: MessageTestingPDFProps) => {
const { text: bannerText, className: bannerClass } = statusConfig[testStatus as "success" | "warning" | "error"];
const lines = prettifyJSON(testMessage)
.split("\n")
.map((line) => {
return line.replace(/ /g, "\u00A0");
});
return (
<Document>
<Page style={styles.page}>
{/* Section 1 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Message testing result</Text>
<Text style={styles.text}>Org name: {orgName}</Text>
<Text style={styles.text}>Receiver name: {receiverName}</Text>
</View>

{/* Section 2 - Banner */}
<View style={[bannerClass, styles.bannerContainer]}>
<Text style={styles.bannerText}>{bannerText}</Text>
</View>

{/* Section 3 - Filters triggered */}
{filterFieldData.length && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Filters triggered</Text>
{filterFieldData.map((line, index) => (
<View key={`filter-line-${index}`}>
<View style={styles.codeBlock}>
<Text>{line}</Text>
</View>
</View>
))}
</View>
)}

{/* Section 4 - Transforms triggered */}
{transformFieldData.length && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Transform errors</Text>
{transformFieldData.map((line, index) => (
<View key={`filter-line-${index}`}>
<View style={styles.codeBlock}>
<Text>{line}</Text>
</View>
</View>
))}
</View>
)}

{/* Section 5 - Warnings triggered */}
{warningFieldData.length && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Transform warnings</Text>
{warningFieldData.map((line, index) => (
<View key={`filter-line-${index}`}>
<View style={styles.codeBlock}>
<Text>{line}</Text>
</View>
</View>
))}
</View>
)}

{/* Section 6 - Output message (HL7) */}
{isPassed && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Output message</Text>
<View style={styles.codeBlock}>
<Text>{outputMessage}</Text>
</View>
</View>
)}

{/* Section 7 - Test message (JSON) */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Test message</Text>
<View style={styles.codeBlock}>
{lines.map((line, idx) => (
<Text key={idx}>{line}</Text>
))}
</View>
</View>
</Page>
</Document>
);
};

export default MessageTestingPDF;
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { usePDF } from "@react-pdf/renderer";
import { QueryObserverResult } from "@tanstack/react-query";
import { Accordion, Button, Icon } from "@trussworks/react-uswds";
import { type PropsWithChildren } from "react";
import language from "./language.json";
import { MessageTestingAccordion } from "./MessageTestingAccordion";
import MessageTestingPDF from "./MessageTestingPDF";
import type { RSMessage, RSMessageResult } from "../../../config/endpoints/reports";
import Alert, { type AlertProps } from "../../../shared/Alert/Alert";
import HL7Message from "../../../shared/HL7Message/HL7Message";
Expand All @@ -14,10 +16,11 @@ export interface MessageTestingResultProps extends PropsWithChildren {
resultData: RSMessageResult;
handleGoBack: () => void;
refetch: () => Promise<QueryObserverResult<RSMessageResult, Error>>;
orgname: string;
receivername: string;
}

const filterFields: (keyof RSMessageResult)[] = ["filterErrors"];

const transformFields: (keyof RSMessageResult)[] = [
"senderTransformErrors",
"enrichmentSchemaErrors",
Expand All @@ -35,6 +38,8 @@ const MessageTestingResult = ({
submittedMessage,
handleGoBack,
refetch,
orgname,
receivername,
...props
}: MessageTestingResultProps) => {
const isPassed =
Expand All @@ -43,9 +48,12 @@ const MessageTestingResult = ({
resultData.enrichmentSchemaErrors.length === 0 &&
resultData.receiverTransformErrors.length === 0;
const isWarned =
resultData.senderTransformWarnings.length > 0 &&
resultData.enrichmentSchemaWarnings.length > 0 &&
resultData.senderTransformWarnings.length > 0 ||
resultData.enrichmentSchemaWarnings.length > 0 ||
resultData.receiverTransformWarnings.length > 0;
const filterFieldData = filterFields.flatMap((key) => resultData[key]);
const transformFieldData = transformFields.flatMap((key) => resultData[key]);
const warningFieldData = warningFields.flatMap((key) => resultData[key]);

const alertType: AlertProps["type"] = !isPassed ? "error" : isWarned ? "warning" : "success";
const alertHeading = language[`${alertType}AlertHeading`];
Expand All @@ -60,14 +68,38 @@ const MessageTestingResult = ({
hour12: true,
};

const MessageTestingPDFRef = (
<MessageTestingPDF
orgName={orgname}
receiverName={receivername}
testStatus={alertType}
filterFieldData={filterFieldData}
transformFieldData={transformFieldData}
warningFieldData={warningFieldData}
testMessage={submittedMessage?.reportBody ?? ""}
outputMessage={resultData?.message ?? ""}
isPassed={isPassed}
/>
);
const [instance] = usePDF({ document: MessageTestingPDFRef });
return (
<section {...props}>
<div className="display-flex flex-justify flex-align-center">
<h2>Test results: {submittedMessage?.fileName}</h2>
<div>
<USLinkButton
href={instance.url ?? ""}
download={`message-testing-result_${Date.now()}.pdf`}
type="button"
outline
>
{instance.loading ? "Loading..." : "Download PDF"} <Icon.ArrowDropDown className="text-top" />
</USLinkButton>

<Button type="button" onClick={() => void refetch()}>
Rerun test <Icon.Autorenew className="text-top" />
</Button>
<Button className="margin-left-1" type="button" onClick={() => void refetch()}>
Rerun test <Icon.Autorenew className="text-top" />
</Button>
</div>
</div>
<USLinkButton onClick={handleGoBack} className="text-no-underline text-bold" unstyled>
<Icon.NavigateBefore className="text-top" /> Select new message
Expand All @@ -88,25 +120,18 @@ const MessageTestingResult = ({
</Alert>
</div>

<MessageTestingAccordion
accordionTitle="Filters triggered"
priority="error"
resultData={resultData}
fieldsToRender={filterFields}
/>
<MessageTestingAccordion accordionTitle="Filters triggered" priority="error" fieldData={filterFieldData} />

<MessageTestingAccordion
accordionTitle="Transform errors"
priority="error"
resultData={resultData}
fieldsToRender={transformFields}
fieldData={transformFieldData}
/>

<MessageTestingAccordion
accordionTitle="Transform warnings"
priority="warning"
resultData={resultData}
fieldsToRender={warningFields}
fieldData={warningFieldData}
/>

{resultData.message && isPassed && (
Expand All @@ -132,7 +157,7 @@ const MessageTestingResult = ({
)}
<div key={`test-submittedMessage-accordion-wrapper`} className="padding-top-4">
<Accordion
key={`test-submittedMessage-accordion-`}
key={`test-submittedMessage-accordion`}
items={[
{
className: "bg-gray-5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ const AdminMessageTestingPage = () => {
setCurrentMessageTestStep(MessageTestingSteps.StepOne);
}}
refetch={refetch}
orgname={orgname ?? "N/A"}
receivername={receivername ?? "N/A"}
/>
)}
</>
Expand Down
Loading

0 comments on commit ec66eb8

Please sign in to comment.