From 9671ef4351926be5576a2f7f3499627cefbf0613 Mon Sep 17 00:00:00 2001 From: etanb Date: Mon, 9 Dec 2024 16:53:51 -0800 Subject: [PATCH 01/11] first bones of message testing --- frontend-react/src/AppRouter.tsx | 6 ++- .../src/components/Admin/ReportTesting.tsx | 37 +++++++++++++++++++ .../src/config/endpoints/settings.ts | 12 ++++++ .../src/hooks/api/reports/UseReportTesting.ts | 34 +++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 frontend-react/src/components/Admin/ReportTesting.tsx create mode 100644 frontend-react/src/hooks/api/reports/UseReportTesting.ts diff --git a/frontend-react/src/AppRouter.tsx b/frontend-react/src/AppRouter.tsx index 9cbbf55e334..e1faea00b12 100644 --- a/frontend-react/src/AppRouter.tsx +++ b/frontend-react/src/AppRouter.tsx @@ -6,7 +6,7 @@ import { RequireGate } from "./shared/RequireGate/RequireGate"; import { SenderType } from "./utils/DataDashboardUtils"; import { lazyRouteMarkdown } from "./utils/LazyRouteMarkdown"; import { PERMISSIONS } from "./utils/UsefulTypes"; - +const ReportTestingPage = lazy(() => import("./components/Admin/ReportTesting")); /* Content Pages */ const Home = lazy(lazyRouteMarkdown(() => import("./content/home/index.mdx"))); const About = lazy(lazyRouteMarkdown(() => import("./content/about/index.mdx"))); @@ -437,6 +437,10 @@ export const appRoutes: RouteObject[] = [ path: "orgreceiversettings/org/:orgname/receiver/:receivername/action/:action", element: , }, + { + path: "orgreceiversettings/org/:orgname/receiver/:receivername/action/:action/message-testing", + element: , + }, { path: "orgsendersettings/org/:orgname/sender/:sendername/action/:action", element: , diff --git a/frontend-react/src/components/Admin/ReportTesting.tsx b/frontend-react/src/components/Admin/ReportTesting.tsx new file mode 100644 index 00000000000..27f04d5b200 --- /dev/null +++ b/frontend-react/src/components/Admin/ReportTesting.tsx @@ -0,0 +1,37 @@ +import { GridContainer } from "@trussworks/react-uswds"; +import { Helmet } from "react-helmet-async"; +import useOrganizationSettings from "../../hooks/api/organizations/UseOrganizationSettings/UseOrganizationSettings"; +import useReportTesting from "../../hooks/api/reports/UseReportTesting"; +import AdminFetchAlert from "../alerts/AdminFetchAlert"; +import Spinner from "../Spinner"; + +function ReportTesting() { + const { data: organization } = useOrganizationSettings(); + const { testMessages, isDisabled, isLoading } = useReportTesting(); + if (isDisabled) { + return ; + } + if (isLoading) return ; + + console.log("testMessages = ", testMessages); + return ( + <> + + Message Testing - ReportStream + + +
+

Message testing

+ {organization?.description &&

{organization.description}

} +

+ Test a message from the message bank or by entering a custom message. You can view test results + in this window while you are logged in. To save for later reference, you can open messages, test + results and output messages in separate tabs.  +

+
+
+ + ); +} + +export default ReportTesting; diff --git a/frontend-react/src/config/endpoints/settings.ts b/frontend-react/src/config/endpoints/settings.ts index eabc90f3af6..af234a2832a 100644 --- a/frontend-react/src/config/endpoints/settings.ts +++ b/frontend-react/src/config/endpoints/settings.ts @@ -8,6 +8,10 @@ export enum ServicesUrls { PUBLIC_KEYS = "/settings/organizations/:orgName/public-keys", } +export enum ReportsUrls { + TESTING = "/reports/testing", +} + export interface RSSettings { version: number; createdAt: string; @@ -106,3 +110,11 @@ export const servicesEndpoints: RSApiEndpoints = { queryKey: "createPublicKey", }), }; + +export const reportsEndpoints: RSApiEndpoints = { + testing: new RSEndpoint({ + path: ReportsUrls.TESTING, + method: HTTPMethods.GET, + queryKey: "reportsTesting", + }), +}; diff --git a/frontend-react/src/hooks/api/reports/UseReportTesting.ts b/frontend-react/src/hooks/api/reports/UseReportTesting.ts new file mode 100644 index 00000000000..db439b79bf2 --- /dev/null +++ b/frontend-react/src/hooks/api/reports/UseReportTesting.ts @@ -0,0 +1,34 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { useCallback } from "react"; +import { reportsEndpoints, RSReceiver } from "../../../config/endpoints/settings"; +import useSessionContext from "../../../contexts/Session/useSessionContext"; +import { Organizations } from "../../UseAdminSafeOrganizationName/UseAdminSafeOrganizationName"; + +const { testing } = reportsEndpoints; + +const useReportTesting = () => { + const { activeMembership, authorizedFetch } = useSessionContext(); + const parsedName = activeMembership?.parsedName; + const isAdmin = Boolean(parsedName) && parsedName === Organizations.PRIMEADMINS; + + const memoizedDataFetch = useCallback(() => { + if (isAdmin) { + return authorizedFetch({}, testing); + } + return null; + }, [isAdmin, authorizedFetch]); + const useSuspenseQueryResult = useSuspenseQuery({ + queryKey: [testing.queryKey, activeMembership], + queryFn: memoizedDataFetch, + }); + + const { data } = useSuspenseQueryResult; + + return { + ...useSuspenseQueryResult, + testMessages: data, + isDisabled: !isAdmin, + }; +}; + +export default useReportTesting; From f863210a16f4b79898be155c1ae675dced2e8202 Mon Sep 17 00:00:00 2001 From: etanb Date: Wed, 11 Dec 2024 16:48:56 -0800 Subject: [PATCH 02/11] skeleton of messages list --- .../components/Admin/EditReceiverSettings.tsx | 178 +++++------------- .../src/components/Admin/ReportTesting.tsx | 62 ++++-- 2 files changed, 93 insertions(+), 147 deletions(-) diff --git a/frontend-react/src/components/Admin/EditReceiverSettings.tsx b/frontend-react/src/components/Admin/EditReceiverSettings.tsx index efae9af94d4..cbc9e724721 100644 --- a/frontend-react/src/components/Admin/EditReceiverSettings.tsx +++ b/frontend-react/src/components/Admin/EditReceiverSettings.tsx @@ -3,17 +3,9 @@ import { FC, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useController, useResource } from "rest-hooks"; -import { - CheckboxComponent, - DropdownComponent, - TextAreaComponent, - TextInputComponent, -} from "./AdminFormEdit"; +import { CheckboxComponent, DropdownComponent, TextAreaComponent, TextInputComponent } from "./AdminFormEdit"; import { AdminFormWrapper } from "./AdminFormWrapper"; -import { - ConfirmSaveSettingModal, - ConfirmSaveSettingModalRef, -} from "./CompareJsonModal"; +import { ConfirmSaveSettingModal, ConfirmSaveSettingModalRef } from "./CompareJsonModal"; import Title from "../../components/Title"; import config from "../../config"; import useSessionContext from "../../contexts/Session/useSessionContext"; @@ -21,11 +13,7 @@ import { showToast } from "../../contexts/Toast"; import useAppInsightsContext from "../../hooks/UseAppInsightsContext/UseAppInsightsContext"; import OrgReceiverSettingsResource from "../../resources/OrgReceiverSettingsResource"; import { jsonSortReplacer } from "../../utils/JsonSortReplacer"; -import { - getErrorDetailFromResponse, - getVersionWarning, - VersionWarningType, -} from "../../utils/misc"; +import { getErrorDetailFromResponse, getVersionWarning, VersionWarningType } from "../../utils/misc"; import { getListOfEnumValues, SampleTimingObj, @@ -43,35 +31,29 @@ interface EditReceiverSettingsFormProps { action: "edit" | "clone"; } -const EditReceiverSettingsForm: FC = ({ - orgname, - receivername, - action, -}) => { +const EditReceiverSettingsForm: FC = ({ orgname, receivername, action }) => { const { properties } = useAppInsightsContext(); const [loading, setLoading] = useState(false); const navigate = useNavigate(); const { activeMembership, authState } = useSessionContext(); const confirmModalRef = useRef(null); - const orgReceiverSettings: OrgReceiverSettingsResource = useResource( - OrgReceiverSettingsResource.detail(), - { orgname, receivername, action }, - ); + const orgReceiverSettings: OrgReceiverSettingsResource = useResource(OrgReceiverSettingsResource.detail(), { + orgname, + receivername, + action, + }); const { fetch: fetchController } = useController(); - const [orgReceiverSettingsOldJson, setOrgReceiverSettingsOldJson] = - useState(""); - const [orgReceiverSettingsNewJson, setOrgReceiverSettingsNewJson] = - useState(""); + const [orgReceiverSettingsOldJson, setOrgReceiverSettingsOldJson] = useState(""); + const [orgReceiverSettingsNewJson, setOrgReceiverSettingsNewJson] = useState(""); const { invalidate } = useController(); const modalRef = useRef(null); const ShowDeleteConfirm = (deleteItemId: string) => { modalRef?.current?.showModal({ title: "Confirm Delete", - message: - "Deleting a setting will only mark it deleted. It can be accessed via the revision history", + message: "Deleting a setting will only mark it deleted. It can be accessed via the revision history", okButtonText: "Delete", itemId: deleteItemId, }); @@ -89,10 +71,7 @@ const EditReceiverSettingsForm: FC = ({ navigate(-1); return true; } catch (e: any) { - showToast( - `Deleting item '${deleteItemId}' failed. ${e.toString()}`, - "error", - ); + showToast(`Deleting item '${deleteItemId}' failed. ${e.toString()}`, "error"); return false; } }; @@ -101,16 +80,13 @@ const EditReceiverSettingsForm: FC = ({ const accessToken = authState.accessToken?.accessToken; const organization = activeMembership?.parsedName; - const response = await fetch( - `${RS_API_URL}/api/settings/organizations/${orgname}/receivers/${receivername}`, - { - headers: { - "x-ms-session-id": properties.context.getSessionId(), - Authorization: `Bearer ${accessToken}`, - Organization: organization!, - }, + const response = await fetch(`${RS_API_URL}/api/settings/organizations/${orgname}/receivers/${receivername}`, { + headers: { + "x-ms-session-id": properties.context.getSessionId(), + Authorization: `Bearer ${accessToken}`, + Organization: organization!, }, - ); + }); return await response.json(); } @@ -120,20 +96,11 @@ const EditReceiverSettingsForm: FC = ({ // fetch original version setLoading(true); const latestResponse = await getLatestReceiverResponse(); - setOrgReceiverSettingsOldJson( - JSON.stringify(latestResponse, jsonSortReplacer, 2), - ); - setOrgReceiverSettingsNewJson( - JSON.stringify(orgReceiverSettings, jsonSortReplacer, 2), - ); - if ( - action === "edit" && - latestResponse?.version !== orgReceiverSettings?.version - ) { + setOrgReceiverSettingsOldJson(JSON.stringify(latestResponse, jsonSortReplacer, 2)); + setOrgReceiverSettingsNewJson(JSON.stringify(orgReceiverSettings, jsonSortReplacer, 2)); + if (action === "edit" && latestResponse?.version !== orgReceiverSettings?.version) { showToast(getVersionWarning(VersionWarningType.POPUP), "error"); - confirmModalRef?.current?.setWarning( - getVersionWarning(VersionWarningType.FULL, latestResponse), - ); + confirmModalRef?.current?.setWarning(getVersionWarning(VersionWarningType.FULL, latestResponse)); confirmModalRef?.current?.disableSave(); } @@ -142,10 +109,7 @@ const EditReceiverSettingsForm: FC = ({ } catch (e: any) { setLoading(false); const errorDetail = await getErrorDetailFromResponse(e); - showToast( - `Reloading receiver '${receivername}' failed with: ${errorDetail}`, - "error", - ); + showToast(`Reloading receiver '${receivername}' failed with: ${errorDetail}`, "error"); return false; } }; @@ -166,21 +130,16 @@ const EditReceiverSettingsForm: FC = ({ const latestResponse = await getLatestReceiverResponse(); if (latestResponse.version !== orgReceiverSettings?.version) { // refresh left-side panel in compare modal to make it obvious what has changed - setOrgReceiverSettingsOldJson( - JSON.stringify(latestResponse, jsonSortReplacer, 2), - ); + setOrgReceiverSettingsOldJson(JSON.stringify(latestResponse, jsonSortReplacer, 2)); showToast(getVersionWarning(VersionWarningType.POPUP), "error"); - confirmModalRef?.current?.setWarning( - getVersionWarning(VersionWarningType.FULL, latestResponse), - ); + confirmModalRef?.current?.setWarning(getVersionWarning(VersionWarningType.FULL, latestResponse)); confirmModalRef?.current?.disableSave(); return false; } const data = confirmModalRef?.current?.getEditedText(); - const receivernamelocal = - action === "clone" ? orgReceiverSettings.name : receivername; + const receivernamelocal = action === "clone" ? orgReceiverSettings.name : receivername; await fetchController( OrgReceiverSettingsResource.update(), @@ -196,10 +155,7 @@ const EditReceiverSettingsForm: FC = ({ } catch (e: any) { setLoading(false); const errorDetail = await getErrorDetailFromResponse(e); - showToast( - `Updating receiver '${receivername}' failed with: ${errorDetail}`, - "error", - ); + showToast(`Updating receiver '${receivername}' failed with: ${errorDetail}`, "error"); return false; } @@ -212,9 +168,7 @@ const EditReceiverSettingsForm: FC = ({ (orgReceiverSettings.name = v)} disabled={action === "edit"} /> @@ -262,29 +216,15 @@ const EditReceiverSettingsForm: FC = ({ - } + toolTip={} defaultvalue={orgReceiverSettings.jurisdictionalFilter} defaultnullvalue="[]" - savefunc={(v) => - (orgReceiverSettings.jurisdictionalFilter = v) - } + savefunc={(v) => (orgReceiverSettings.jurisdictionalFilter = v)} /> - } + toolTip={} defaultvalue={orgReceiverSettings.qualityFilter} defaultnullvalue="[]" savefunc={(v) => (orgReceiverSettings.qualityFilter = v)} @@ -293,20 +233,12 @@ const EditReceiverSettingsForm: FC = ({ fieldname={"reverseTheQualityFilter"} label={"Reverse the Quality Filter"} defaultvalue={orgReceiverSettings.reverseTheQualityFilter} - savefunc={(v) => - (orgReceiverSettings.reverseTheQualityFilter = v) - } + savefunc={(v) => (orgReceiverSettings.reverseTheQualityFilter = v)} /> - } + toolTip={} defaultvalue={orgReceiverSettings.routingFilter} defaultnullvalue="[]" savefunc={(v) => (orgReceiverSettings.routingFilter = v)} @@ -314,18 +246,10 @@ const EditReceiverSettingsForm: FC = ({ - } + toolTip={} defaultvalue={orgReceiverSettings.processingModeFilter} defaultnullvalue="[]" - savefunc={(v) => - (orgReceiverSettings.processingModeFilter = v) - } + savefunc={(v) => (orgReceiverSettings.processingModeFilter = v)} /> = ({ - } + toolTip={} defaultvalue={orgReceiverSettings.transport} defaultnullvalue={null} savefunc={(v) => (orgReceiverSettings.transport = v)} @@ -374,11 +296,7 @@ const EditReceiverSettingsForm: FC = ({ @@ -394,11 +312,7 @@ const EditReceiverSettingsForm: FC = ({ void saveReceiverData()} oldjson={orgReceiverSettingsOldJson} @@ -414,25 +328,17 @@ const EditReceiverSettingsForm: FC = ({ ); }; -interface EditReceiverSettingsParams extends Record { +export interface EditReceiverSettingsParams extends Record { orgname: string; receivername: string; action: "edit" | "clone"; } export function EditReceiverSettingsPage() { - const { orgname, receivername, action } = - useParams(); + const { orgname, receivername, action } = useParams(); return ( - - } - > + }> (); const { testMessages, isDisabled, isLoading } = useReportTesting(); if (isDisabled) { return ; } - if (isLoading) return ; + if (isLoading || !testMessages) return ; + + const RadioField = ({ title, index }: { title: string; index: number }) => { + return ( +
+ + +
+ ); + }; console.log("testMessages = ", testMessages); return ( <> - Message Testing - ReportStream + Message testing - ReportStream - -
-

Message testing

- {organization?.description &&

{organization.description}

} + + + <h2 className="margin-bottom-0"> + <span className="text-normal font-body-md text-base margin-bottom-0"> + Org name: {orgname} + <br /> + Receiver name: {receivername} + </span> + </h2> + </> + } + > + <GridContainer> <p> Test a message from the message bank or by entering a custom message. You can view test results in this window while you are logged in. To save for later reference, you can open messages, test results and output messages in separate tabs.  </p> - </article> - </GridContainer> + <hr /> + <p className="font-sans-xl text-bold">Test message bank</p> + <Form className="width-full maxw-full"> + <fieldset className="usa-fieldset bg-base-lighter padding-3"> + {testMessages?.map((item, index) => ( + <RadioField key={index} title={item.fileName} index={index} /> + ))} + </fieldset> + </Form> + </GridContainer> + </AdminFormWrapper> </> ); } From 50a8133085b44c761b305b88835a8beea61186d4 Mon Sep 17 00:00:00 2001 From: etanb <etan.berkowitz@gmail.com> Date: Thu, 12 Dec 2024 11:15:02 -0800 Subject: [PATCH 03/11] simple view message --- .../src/components/Admin/ReportTesting.tsx | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/frontend-react/src/components/Admin/ReportTesting.tsx b/frontend-react/src/components/Admin/ReportTesting.tsx index 628e56cdfed..79d6ef77a6a 100644 --- a/frontend-react/src/components/Admin/ReportTesting.tsx +++ b/frontend-react/src/components/Admin/ReportTesting.tsx @@ -1,4 +1,4 @@ -import { Form, GridContainer } from "@trussworks/react-uswds"; +import { Button, GridContainer } from "@trussworks/react-uswds"; import { Helmet } from "react-helmet-async"; import { useParams } from "react-router"; import { AdminFormWrapper } from "./AdminFormWrapper"; @@ -7,6 +7,7 @@ import useReportTesting from "../../hooks/api/reports/UseReportTesting"; import AdminFetchAlert from "../alerts/AdminFetchAlert"; import Spinner from "../Spinner"; import Title from "../Title"; +import { Icon } from "../../shared"; function ReportTesting() { const { orgname, receivername } = useParams<EditReceiverSettingsParams>(); @@ -16,9 +17,25 @@ function ReportTesting() { } if (isLoading || !testMessages) return <Spinner />; - const RadioField = ({ title, index }: { title: string; index: number }) => { + const RadioField = ({ title, body, index }: { title: string; body: string; index: number }) => { + const openJsonInNewTab = () => { + // Your JSON object or string + const jsonString = JSON.stringify(JSON.parse(body), null, 2); + + // Create a Blob from the JSON string + const blob = new Blob([jsonString], { type: "application/json" }); + + // Create an object URL for the Blob + const url = URL.createObjectURL(blob); + + // Open the URL in a new browser tab + window.open(url, "_blank"); + + // Revoke the URL after some time to free up memory + setTimeout(() => URL.revokeObjectURL(url), 1000); + }; return ( - <div className="usa-radio bg-base-lighter padding-2 border-bottom-1px border-gray-30"> + <div className="usa-radio bg-base-lightest padding-2 border-bottom-1px border-gray-30"> <input className="usa-radio__input" id={`message-${index}`} @@ -26,8 +43,11 @@ function ReportTesting() { name="message-test-form" value={title} /> - <label className="usa-radio__label margin-0" htmlFor={`message-${index}`}> - {title} + <label className="usa-radio__label" htmlFor={`message-${index}`}> + {title}{" "} + <Button type="button" unstyled onClick={openJsonInNewTab}> + View message <Icon name="Visibility" /> + </Button> </label> </div> ); @@ -61,13 +81,13 @@ function ReportTesting() { </p> <hr /> <p className="font-sans-xl text-bold">Test message bank</p> - <Form className="width-full maxw-full"> - <fieldset className="usa-fieldset bg-base-lighter padding-3"> + <form> + <fieldset className="usa-fieldset bg-base-lightest padding-3"> {testMessages?.map((item, index) => ( - <RadioField key={index} title={item.fileName} index={index} /> + <RadioField key={index} index={index} title={item.fileName} body={item.reportBody} /> ))} </fieldset> - </Form> + </form> </GridContainer> </AdminFormWrapper> </> From 316395d49858b415419301188e72af44dfbdfa9a Mon Sep 17 00:00:00 2001 From: etanb <etan.berkowitz@gmail.com> Date: Thu, 12 Dec 2024 17:58:46 -0800 Subject: [PATCH 04/11] style cleanup --- .../src/components/Admin/ReportTesting.tsx | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/frontend-react/src/components/Admin/ReportTesting.tsx b/frontend-react/src/components/Admin/ReportTesting.tsx index 79d6ef77a6a..af60ba449b7 100644 --- a/frontend-react/src/components/Admin/ReportTesting.tsx +++ b/frontend-react/src/components/Admin/ReportTesting.tsx @@ -8,15 +8,21 @@ import AdminFetchAlert from "../alerts/AdminFetchAlert"; import Spinner from "../Spinner"; import Title from "../Title"; import { Icon } from "../../shared"; +import { useState } from "react"; function ReportTesting() { const { orgname, receivername } = useParams<EditReceiverSettingsParams>(); const { testMessages, isDisabled, isLoading } = useReportTesting(); + const [selectedOption, setSelectedOption] = useState(null); if (isDisabled) { return <AdminFetchAlert />; } if (isLoading || !testMessages) return <Spinner />; + const handleSelect = (event) => { + setSelectedOption(event.target.value); // Update the selected option in state + }; + const RadioField = ({ title, body, index }: { title: string; body: string; index: number }) => { const openJsonInNewTab = () => { // Your JSON object or string @@ -42,18 +48,19 @@ function ReportTesting() { type="radio" name="message-test-form" value={title} + onChange={handleSelect} /> - <label className="usa-radio__label" htmlFor={`message-${index}`}> + <label className="usa-radio__label margin-top-0" htmlFor={`message-${index}`}> {title}{" "} <Button type="button" unstyled onClick={openJsonInNewTab}> - View message <Icon name="Visibility" /> + View message + <Icon name="Visibility" className="text-tbottom margin-left-05" /> </Button> </label> </div> ); }; - console.log("testMessages = ", testMessages); return ( <> <Helmet> @@ -87,6 +94,14 @@ function ReportTesting() { <RadioField key={index} index={index} title={item.fileName} body={item.reportBody} /> ))} </fieldset> + <div className="padding-top-4"> + <Button type="button" outline> + Test custom message + </Button> + <Button disabled={!selectedOption} type="button"> + Run test + </Button> + </div> </form> </GridContainer> </AdminFormWrapper> From f520b2e710a0f9fdbc6afed6447f77b735160ca7 Mon Sep 17 00:00:00 2001 From: etanb <etan.berkowitz@gmail.com> Date: Fri, 13 Dec 2024 11:38:07 -0800 Subject: [PATCH 05/11] add custom message cmp + fix ts errors --- .../src/components/Admin/ReportTesting.tsx | 82 ++++++++++++++++--- .../src/config/endpoints/settings.ts | 6 ++ .../src/hooks/api/reports/UseReportTesting.ts | 4 +- frontend-react/src/utils/misc.ts | 24 ++---- 4 files changed, 85 insertions(+), 31 deletions(-) diff --git a/frontend-react/src/components/Admin/ReportTesting.tsx b/frontend-react/src/components/Admin/ReportTesting.tsx index af60ba449b7..12a60c39c70 100644 --- a/frontend-react/src/components/Admin/ReportTesting.tsx +++ b/frontend-react/src/components/Admin/ReportTesting.tsx @@ -1,26 +1,84 @@ -import { Button, GridContainer } from "@trussworks/react-uswds"; +import { Button, GridContainer, Textarea } from "@trussworks/react-uswds"; +import { ChangeEvent, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useParams } from "react-router"; import { AdminFormWrapper } from "./AdminFormWrapper"; import { EditReceiverSettingsParams } from "./EditReceiverSettings"; import useReportTesting from "../../hooks/api/reports/UseReportTesting"; +import { Icon } from "../../shared"; import AdminFetchAlert from "../alerts/AdminFetchAlert"; import Spinner from "../Spinner"; import Title from "../Title"; -import { Icon } from "../../shared"; -import { useState } from "react"; function ReportTesting() { const { orgname, receivername } = useParams<EditReceiverSettingsParams>(); const { testMessages, isDisabled, isLoading } = useReportTesting(); - const [selectedOption, setSelectedOption] = useState(null); + const [selectedOption, setSelectedOption] = useState<string | null>(null); + const [currentTestMessages, setCurrentTestMessages] = useState(testMessages); + const [openCustomMessage, setOpenCustomMessage] = useState(false); + const [customMessageNumber, setCustomMessageNumber] = useState(1); + if (isDisabled) { return <AdminFetchAlert />; } - if (isLoading || !testMessages) return <Spinner />; + if (isLoading || !currentTestMessages) return <Spinner />; + + const handleSelect = (event: ChangeEvent<HTMLInputElement>) => { + setSelectedOption(event.target.value); + }; + + const handleAddCustomMessage = () => { + setSelectedOption(null); + setOpenCustomMessage(true); + }; + + const CustomMessage = () => { + const [text, setText] = useState(""); + const handleTextareaChange = (event: ChangeEvent<HTMLTextAreaElement>) => { + setText(event.target.value); + }; + const handleAddCustomMessage = () => { + const dateCreated = new Date(); + setCurrentTestMessages([ + ...currentTestMessages, + { + dateCreated: dateCreated.toString(), + fileName: `Custom message ${customMessageNumber}`, + reportBody: text, + }, + ]); + setCustomMessageNumber(customMessageNumber + 1); + setText(""); + setOpenCustomMessage(false); + }; - const handleSelect = (event) => { - setSelectedOption(event.target.value); // Update the selected option in state + return ( + <div className="width-full"> + <p className="text-bold">Enter custom message</p> + <p>Custom messages do not save to the bank after you log out.</p> + <Textarea + value={text} + onChange={handleTextareaChange} + id="custom-message-text" + name="custom-message-text" + className="width-full maxw-full margin-bottom-205" + /> + <div className="width-full text-right"> + <Button + type="button" + outline + onClick={() => { + setOpenCustomMessage(false); + }} + > + Cancel + </Button> + <Button type="button" onClick={handleAddCustomMessage} disabled={text.length === 0}> + Add + </Button> + </div> + </div> + ); }; const RadioField = ({ title, body, index }: { title: string; body: string; index: number }) => { @@ -47,8 +105,9 @@ function ReportTesting() { id={`message-${index}`} type="radio" name="message-test-form" - value={title} + value={body} onChange={handleSelect} + checked={selectedOption === body} /> <label className="usa-radio__label margin-top-0" htmlFor={`message-${index}`}> {title}{" "} @@ -84,18 +143,19 @@ function ReportTesting() { <p> Test a message from the message bank or by entering a custom message. You can view test results in this window while you are logged in. To save for later reference, you can open messages, test - results and output messages in separate tabs.  + results and output messages in separate tabs. </p> <hr /> <p className="font-sans-xl text-bold">Test message bank</p> <form> <fieldset className="usa-fieldset bg-base-lightest padding-3"> - {testMessages?.map((item, index) => ( + {currentTestMessages?.map((item, index) => ( <RadioField key={index} index={index} title={item.fileName} body={item.reportBody} /> ))} + {openCustomMessage && <CustomMessage />} </fieldset> <div className="padding-top-4"> - <Button type="button" outline> + <Button type="button" outline onClick={handleAddCustomMessage}> Test custom message </Button> <Button disabled={!selectedOption} type="button"> diff --git a/frontend-react/src/config/endpoints/settings.ts b/frontend-react/src/config/endpoints/settings.ts index af234a2832a..628af29bb37 100644 --- a/frontend-react/src/config/endpoints/settings.ts +++ b/frontend-react/src/config/endpoints/settings.ts @@ -26,6 +26,12 @@ export interface RSService extends RSSettings { customerStatus?: string; } +export interface RSMessage { + dateCreated: string; + fileName: string; + reportBody: string; +} + export interface RSOrganizationSettings extends RSSettings { description: string; filters: string[]; diff --git a/frontend-react/src/hooks/api/reports/UseReportTesting.ts b/frontend-react/src/hooks/api/reports/UseReportTesting.ts index db439b79bf2..b91d14874cb 100644 --- a/frontend-react/src/hooks/api/reports/UseReportTesting.ts +++ b/frontend-react/src/hooks/api/reports/UseReportTesting.ts @@ -1,6 +1,6 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { useCallback } from "react"; -import { reportsEndpoints, RSReceiver } from "../../../config/endpoints/settings"; +import { reportsEndpoints, RSMessage } from "../../../config/endpoints/settings"; import useSessionContext from "../../../contexts/Session/useSessionContext"; import { Organizations } from "../../UseAdminSafeOrganizationName/UseAdminSafeOrganizationName"; @@ -13,7 +13,7 @@ const useReportTesting = () => { const memoizedDataFetch = useCallback(() => { if (isAdmin) { - return authorizedFetch<RSReceiver[]>({}, testing); + return authorizedFetch<RSMessage[]>({}, testing); } return null; }, [isAdmin, authorizedFetch]); diff --git a/frontend-react/src/utils/misc.ts b/frontend-react/src/utils/misc.ts index 75f6a39c6af..0d68966f305 100644 --- a/frontend-react/src/utils/misc.ts +++ b/frontend-react/src/utils/misc.ts @@ -13,17 +13,14 @@ import { convert } from "html-to-text"; export const splitOn: { <T = string>(s: T, ...i: number[]): T[]; <T extends any[]>(s: T, ...i: number[]): T[]; -} = <T>(slicable: string | T[], ...indices: number[]) => - [0, ...indices].map((n, i, m) => slicable.slice(n, m[i + 1])); +} = <T>(slicable: string | T[], ...indices: number[]) => [0, ...indices].map((n, i, m) => slicable.slice(n, m[i + 1])); /** * @param jsonTextValue * @return { valid: boolean; offset: number; errorMsg: string} If valid = false, then offset is where the error is. * valid=true, offset: -1, errorMsg: "" - this is to keep typechecking simple for the caller. offset is always a number */ -export const checkJson = ( - jsonTextValue: string, -): { valid: boolean; offset: number; errorMsg: string } => { +export const checkJson = (jsonTextValue: string): { valid: boolean; offset: number; errorMsg: string } => { try { JSON.parse(jsonTextValue); return { valid: true, offset: -1, errorMsg: "" }; @@ -35,9 +32,7 @@ export const checkJson = ( // parse out the position and try to select it for them. // NOTE: if "at position N" string not found, then assume mistake is at the end let offset = jsonTextValue.length; - const findPositionMatch = errorMsg - ?.matchAll(/position (\d+)/gi) - ?.next(); + const findPositionMatch = errorMsg?.matchAll(/position (\d+)/gi)?.next(); if (findPositionMatch?.value?.length === 2) { const possibleOffset = parseInt(findPositionMatch.value[1] || -1); if (!isNaN(possibleOffset) && possibleOffset !== -1) { @@ -71,10 +66,7 @@ export enum VersionWarningType { * @param warningType either POPUP (for the toast notification) or FULL (for the red text on the compare modal itself) * @param settings the resource object from which to use information helpful to the user */ -export function getVersionWarning( - warningType: VersionWarningType, - settings: any = null, -): string { +export function getVersionWarning(warningType: VersionWarningType, settings: any = null): string { switch (warningType) { case VersionWarningType.POPUP: return "WARNING! A newer version of this setting now exists in the database"; @@ -133,10 +125,7 @@ export const capitalizeFirst = (uncapped: string): string => { * @param array * @param predicate */ -export const groupBy = <T>( - array: T[], - predicate: (value: T, index: number, array: T[]) => string, -) => +export const groupBy = <T>(array: T[], predicate: (value: T, index: number, array: T[]) => string) => array.reduce( (acc, value, index, array) => { (acc[predicate(value, index, array)] ||= []).push(value); @@ -156,8 +145,7 @@ export const parseFileLocation = ( fileName: string; } => { const fileReportsLocation = urlFileLocation.split("/").pop() ?? ""; - const [folderLocation, sendingOrg, fileName] = - fileReportsLocation.split("%2F"); + const [folderLocation, sendingOrg, fileName] = fileReportsLocation.split("%2F"); if (!(folderLocation && sendingOrg && fileName)) { return { From ebeae57ddcf6cc5f17edf7bfa4d845c75b35b5ad Mon Sep 17 00:00:00 2001 From: etanb <etan.berkowitz@gmail.com> Date: Fri, 13 Dec 2024 11:41:10 -0800 Subject: [PATCH 06/11] rename Report to Message --- frontend-react/src/AppRouter.tsx | 2 +- .../components/Admin/{ReportTesting.tsx => MessageTesting.tsx} | 2 +- .../UseReportTesting.ts => messages/UseMessageTesting.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename frontend-react/src/components/Admin/{ReportTesting.tsx => MessageTesting.tsx} (98%) rename frontend-react/src/hooks/api/{reports/UseReportTesting.ts => messages/UseMessageTesting.ts} (100%) diff --git a/frontend-react/src/AppRouter.tsx b/frontend-react/src/AppRouter.tsx index e1faea00b12..28f3587fb8d 100644 --- a/frontend-react/src/AppRouter.tsx +++ b/frontend-react/src/AppRouter.tsx @@ -6,7 +6,7 @@ import { RequireGate } from "./shared/RequireGate/RequireGate"; import { SenderType } from "./utils/DataDashboardUtils"; import { lazyRouteMarkdown } from "./utils/LazyRouteMarkdown"; import { PERMISSIONS } from "./utils/UsefulTypes"; -const ReportTestingPage = lazy(() => import("./components/Admin/ReportTesting")); +const ReportTestingPage = lazy(() => import("./components/Admin/MessageTesting")); /* Content Pages */ const Home = lazy(lazyRouteMarkdown(() => import("./content/home/index.mdx"))); const About = lazy(lazyRouteMarkdown(() => import("./content/about/index.mdx"))); diff --git a/frontend-react/src/components/Admin/ReportTesting.tsx b/frontend-react/src/components/Admin/MessageTesting.tsx similarity index 98% rename from frontend-react/src/components/Admin/ReportTesting.tsx rename to frontend-react/src/components/Admin/MessageTesting.tsx index 12a60c39c70..bb399b5ccab 100644 --- a/frontend-react/src/components/Admin/ReportTesting.tsx +++ b/frontend-react/src/components/Admin/MessageTesting.tsx @@ -4,7 +4,7 @@ import { Helmet } from "react-helmet-async"; import { useParams } from "react-router"; import { AdminFormWrapper } from "./AdminFormWrapper"; import { EditReceiverSettingsParams } from "./EditReceiverSettings"; -import useReportTesting from "../../hooks/api/reports/UseReportTesting"; +import useReportTesting from "../../hooks/api/messages/UseMessageTesting"; import { Icon } from "../../shared"; import AdminFetchAlert from "../alerts/AdminFetchAlert"; import Spinner from "../Spinner"; diff --git a/frontend-react/src/hooks/api/reports/UseReportTesting.ts b/frontend-react/src/hooks/api/messages/UseMessageTesting.ts similarity index 100% rename from frontend-react/src/hooks/api/reports/UseReportTesting.ts rename to frontend-react/src/hooks/api/messages/UseMessageTesting.ts From 682be03dc63bd4c66f68c2f91c6351f13f5f28c8 Mon Sep 17 00:00:00 2001 From: etanb <etan.berkowitz@gmail.com> Date: Fri, 13 Dec 2024 12:06:44 -0800 Subject: [PATCH 07/11] add support for all file types for Viewing --- .../src/components/Admin/MessageTesting.tsx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend-react/src/components/Admin/MessageTesting.tsx b/frontend-react/src/components/Admin/MessageTesting.tsx index bb399b5ccab..bd05d5956fc 100644 --- a/frontend-react/src/components/Admin/MessageTesting.tsx +++ b/frontend-react/src/components/Admin/MessageTesting.tsx @@ -82,22 +82,26 @@ function ReportTesting() { }; const RadioField = ({ title, body, index }: { title: string; body: string; index: number }) => { - const openJsonInNewTab = () => { - // Your JSON object or string - const jsonString = JSON.stringify(JSON.parse(body), null, 2); + const openTextInNewTab = () => { + let formattedContent = body; - // Create a Blob from the JSON string - const blob = new Blob([jsonString], { type: "application/json" }); + // Check if the content is JSON and format it + try { + formattedContent = JSON.stringify(JSON.parse(body), null, 2); + } catch { + formattedContent = body; + } + + const blob = new Blob([formattedContent], { type: "text/plain" }); - // Create an object URL for the Blob const url = URL.createObjectURL(blob); - // Open the URL in a new browser tab window.open(url, "_blank"); - // Revoke the URL after some time to free up memory - setTimeout(() => URL.revokeObjectURL(url), 1000); + // Revoke the URL to free up memory + URL.revokeObjectURL(url); }; + return ( <div className="usa-radio bg-base-lightest padding-2 border-bottom-1px border-gray-30"> <input @@ -111,7 +115,7 @@ function ReportTesting() { /> <label className="usa-radio__label margin-top-0" htmlFor={`message-${index}`}> {title}{" "} - <Button type="button" unstyled onClick={openJsonInNewTab}> + <Button type="button" unstyled onClick={openTextInNewTab}> View message <Icon name="Visibility" className="text-tbottom margin-left-05" /> </Button> From dd56347cf1d4137cb26d8419da9251ab14b38917 Mon Sep 17 00:00:00 2001 From: etanb <etan.berkowitz@gmail.com> Date: Fri, 13 Dec 2024 13:15:14 -0800 Subject: [PATCH 08/11] add breadcrumb --- .../src/components/Admin/MessageTesting.tsx | 14 ++++++++++++++ frontend-react/src/utils/FeatureName.ts | 12 +++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend-react/src/components/Admin/MessageTesting.tsx b/frontend-react/src/components/Admin/MessageTesting.tsx index bd05d5956fc..6c677b0e6f9 100644 --- a/frontend-react/src/components/Admin/MessageTesting.tsx +++ b/frontend-react/src/components/Admin/MessageTesting.tsx @@ -6,7 +6,9 @@ import { AdminFormWrapper } from "./AdminFormWrapper"; import { EditReceiverSettingsParams } from "./EditReceiverSettings"; import useReportTesting from "../../hooks/api/messages/UseMessageTesting"; import { Icon } from "../../shared"; +import { FeatureName } from "../../utils/FeatureName"; import AdminFetchAlert from "../alerts/AdminFetchAlert"; +import Crumbs, { CrumbsProps } from "../Crumbs"; import Spinner from "../Spinner"; import Title from "../Title"; @@ -17,6 +19,15 @@ function ReportTesting() { const [currentTestMessages, setCurrentTestMessages] = useState(testMessages); const [openCustomMessage, setOpenCustomMessage] = useState(false); const [customMessageNumber, setCustomMessageNumber] = useState(1); + const crumbProps: CrumbsProps = { + crumbList: [ + { + label: FeatureName.RECEIVER_SETTINGS, + path: `/admin/orgreceiversettings/org/${orgname}/receiver/${receivername}/action/edit`, + }, + { label: FeatureName.MESSAGE_TESTING }, + ], + }; if (isDisabled) { return <AdminFetchAlert />; @@ -129,6 +140,9 @@ function ReportTesting() { <Helmet> <title>Message testing - ReportStream + + + diff --git a/frontend-react/src/utils/FeatureName.ts b/frontend-react/src/utils/FeatureName.ts index 0ca2a0eee6c..58fe425e97e 100644 --- a/frontend-react/src/utils/FeatureName.ts +++ b/frontend-react/src/utils/FeatureName.ts @@ -1,11 +1,13 @@ export enum FeatureName { + ADMIN = "Admin", DAILY_DATA = "Daily Data", + DATA_DASHBOARD = "Data Dashboard", + FACILITIES_PROVIDERS = "All facilities & providers", + MESSAGE_TESTING = "Message testing", + PUBLIC_KEY = "Public Key", + RECEIVER_SETTINGS = "Receiver settings", + REPORT_DETAILS = "Report Details", SUBMISSIONS = "Submissions", SUPPORT = "Support", - ADMIN = "Admin", UPLOAD = "Upload", - FACILITIES_PROVIDERS = "All facilities & providers", - DATA_DASHBOARD = "Data Dashboard", - REPORT_DETAILS = "Report Details", - PUBLIC_KEY = "Public Key", } From e79110ceadc013bf052d0096495f126a69027e67 Mon Sep 17 00:00:00 2001 From: etanb Date: Mon, 16 Dec 2024 13:18:19 -0800 Subject: [PATCH 09/11] reformat Radio component --- frontend-react/src/AppRouter.tsx | 1 + .../src/components/Admin/MessageTesting.tsx | 38 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/frontend-react/src/AppRouter.tsx b/frontend-react/src/AppRouter.tsx index 28f3587fb8d..88c72cb17e4 100644 --- a/frontend-react/src/AppRouter.tsx +++ b/frontend-react/src/AppRouter.tsx @@ -6,6 +6,7 @@ import { RequireGate } from "./shared/RequireGate/RequireGate"; import { SenderType } from "./utils/DataDashboardUtils"; import { lazyRouteMarkdown } from "./utils/LazyRouteMarkdown"; import { PERMISSIONS } from "./utils/UsefulTypes"; + const ReportTestingPage = lazy(() => import("./components/Admin/MessageTesting")); /* Content Pages */ const Home = lazy(lazyRouteMarkdown(() => import("./content/home/index.mdx"))); diff --git a/frontend-react/src/components/Admin/MessageTesting.tsx b/frontend-react/src/components/Admin/MessageTesting.tsx index 6c677b0e6f9..ff5fd383e57 100644 --- a/frontend-react/src/components/Admin/MessageTesting.tsx +++ b/frontend-react/src/components/Admin/MessageTesting.tsx @@ -1,4 +1,4 @@ -import { Button, GridContainer, Textarea } from "@trussworks/react-uswds"; +import { Button, GridContainer, Radio, Textarea } from "@trussworks/react-uswds"; import { ChangeEvent, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useParams } from "react-router"; @@ -114,24 +114,24 @@ function ReportTesting() { }; return ( -
- - -
+ + {" "} + {title}{" "} + + + } + /> ); }; From 8a69d90c4ba1b34aaf36dfa0022d0dfdbb761eba Mon Sep 17 00:00:00 2001 From: etanb Date: Mon, 16 Dec 2024 14:33:44 -0800 Subject: [PATCH 10/11] encapsulate components --- frontend-react/src/AppRouter.tsx | 2 +- .../src/components/Admin/MessageTesting.tsx | 190 ------------------ .../Admin/MessageTesting/CustomMessage.tsx | 64 ++++++ .../Admin/MessageTesting/MessageTesting.tsx | 114 +++++++++++ .../Admin/MessageTesting/RadioField.tsx | 57 ++++++ .../src/config/endpoints/settings.ts | 2 +- 6 files changed, 237 insertions(+), 192 deletions(-) delete mode 100644 frontend-react/src/components/Admin/MessageTesting.tsx create mode 100644 frontend-react/src/components/Admin/MessageTesting/CustomMessage.tsx create mode 100644 frontend-react/src/components/Admin/MessageTesting/MessageTesting.tsx create mode 100644 frontend-react/src/components/Admin/MessageTesting/RadioField.tsx diff --git a/frontend-react/src/AppRouter.tsx b/frontend-react/src/AppRouter.tsx index 88c72cb17e4..b2c78dc8a94 100644 --- a/frontend-react/src/AppRouter.tsx +++ b/frontend-react/src/AppRouter.tsx @@ -7,7 +7,7 @@ import { SenderType } from "./utils/DataDashboardUtils"; import { lazyRouteMarkdown } from "./utils/LazyRouteMarkdown"; import { PERMISSIONS } from "./utils/UsefulTypes"; -const ReportTestingPage = lazy(() => import("./components/Admin/MessageTesting")); +const ReportTestingPage = lazy(() => import("./components/Admin/MessageTesting/MessageTesting")); /* Content Pages */ const Home = lazy(lazyRouteMarkdown(() => import("./content/home/index.mdx"))); const About = lazy(lazyRouteMarkdown(() => import("./content/about/index.mdx"))); diff --git a/frontend-react/src/components/Admin/MessageTesting.tsx b/frontend-react/src/components/Admin/MessageTesting.tsx deleted file mode 100644 index ff5fd383e57..00000000000 --- a/frontend-react/src/components/Admin/MessageTesting.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { Button, GridContainer, Radio, Textarea } from "@trussworks/react-uswds"; -import { ChangeEvent, useState } from "react"; -import { Helmet } from "react-helmet-async"; -import { useParams } from "react-router"; -import { AdminFormWrapper } from "./AdminFormWrapper"; -import { EditReceiverSettingsParams } from "./EditReceiverSettings"; -import useReportTesting from "../../hooks/api/messages/UseMessageTesting"; -import { Icon } from "../../shared"; -import { FeatureName } from "../../utils/FeatureName"; -import AdminFetchAlert from "../alerts/AdminFetchAlert"; -import Crumbs, { CrumbsProps } from "../Crumbs"; -import Spinner from "../Spinner"; -import Title from "../Title"; - -function ReportTesting() { - const { orgname, receivername } = useParams(); - const { testMessages, isDisabled, isLoading } = useReportTesting(); - const [selectedOption, setSelectedOption] = useState(null); - const [currentTestMessages, setCurrentTestMessages] = useState(testMessages); - const [openCustomMessage, setOpenCustomMessage] = useState(false); - const [customMessageNumber, setCustomMessageNumber] = useState(1); - const crumbProps: CrumbsProps = { - crumbList: [ - { - label: FeatureName.RECEIVER_SETTINGS, - path: `/admin/orgreceiversettings/org/${orgname}/receiver/${receivername}/action/edit`, - }, - { label: FeatureName.MESSAGE_TESTING }, - ], - }; - - if (isDisabled) { - return ; - } - if (isLoading || !currentTestMessages) return ; - - const handleSelect = (event: ChangeEvent) => { - setSelectedOption(event.target.value); - }; - - const handleAddCustomMessage = () => { - setSelectedOption(null); - setOpenCustomMessage(true); - }; - - const CustomMessage = () => { - const [text, setText] = useState(""); - const handleTextareaChange = (event: ChangeEvent) => { - setText(event.target.value); - }; - const handleAddCustomMessage = () => { - const dateCreated = new Date(); - setCurrentTestMessages([ - ...currentTestMessages, - { - dateCreated: dateCreated.toString(), - fileName: `Custom message ${customMessageNumber}`, - reportBody: text, - }, - ]); - setCustomMessageNumber(customMessageNumber + 1); - setText(""); - setOpenCustomMessage(false); - }; - - return ( -
-

Enter custom message

-

Custom messages do not save to the bank after you log out.

-