From 1e9ede9d033c50a8f6228c0fa152fbeb98f7cfc0 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 6 Jun 2024 16:46:45 -0700 Subject: [PATCH 01/66] Init POC for generating rule schema and using generated inputs. --- .../InputOutputTable/InputOutputTable.tsx | 56 ++++++++++++++++--- .../SimulationViewer/SimulationViewer.tsx | 29 ++++++++-- app/rule/[ruleId]/page.tsx | 6 +- app/types/rulemap.d.ts | 4 ++ app/utils/api.ts | 17 ++++++ 5 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 app/types/rulemap.d.ts diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index 9216565..157f470 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from "react"; -import { Table, Tag } from "antd"; +import { useState, useEffect, FocusEvent } from "react"; +import { Table, Tag, Input } from "antd"; import styles from "./InputOutputTable.module.css"; const COLUMNS = [ @@ -20,21 +20,63 @@ const PROPERTIES_TO_IGNORE = ["submit", "lateEntry"]; interface InputOutputTableProps { title: string; rawData: object; + setRawData?: (data: object) => void; } -export default function InputOutputTable({ title, rawData }: InputOutputTableProps) { +export default function InputOutputTable({ title, rawData, setRawData }: InputOutputTableProps) { const [dataSource, setDataSource] = useState([]); - const convertAndStyleValue = (value: any, property: string) => { + const convertAndStyleValue = (value: any, property: string, editable: boolean) => { + let displayValue = value; + + // Convert booleans and numbers to strings for the input field if editable + if (editable) { + if (typeof value === "boolean") { + displayValue = value.toString(); + } else if (typeof value === "number") { + displayValue = value.toString(); + } + return handleValueChange(e, property)} />; + } + // Handle booleans if (typeof value === "boolean") { return value ? TRUE : FALSE; } + // Handle money amounts if (typeof value === "number" && property.toLowerCase().includes("amount")) { - value = `$${value}`; + displayValue = `$${value}`; + } + + return {displayValue}; + }; + + const handleValueChange = (e: FocusEvent, property: string) => { + if (!e.target) return; + const newValue = (e.target as HTMLInputElement).value; + let queryValue: any = newValue; + + // Handle booleans + if (newValue.toLowerCase() === "true") { + queryValue = true; + } else if (newValue.toLowerCase() === "false") { + queryValue = false; + } + + // Handle numbers + if (!isNaN(Number(newValue))) { + queryValue = Number(newValue); + } + + const updatedData = { ...rawData, [property]: queryValue } || {}; + + // Ensure setRawData is defined before calling it + if (typeof setRawData === "function") { + setRawData(updatedData); + } else { + console.error("setRawData is not a function or is undefined"); } - return {value}; }; useEffect(() => { @@ -44,7 +86,7 @@ export default function InputOutputTable({ title, rawData }: InputOutputTablePro if (!PROPERTIES_TO_IGNORE.includes(property)) { newData.push({ property, - value: convertAndStyleValue(value, property), + value: convertAndStyleValue(value, property, true), key: index, }); } diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 5cfdcbe..03b84fe 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -7,6 +7,7 @@ import { ExportOutlined } from "@ant-design/icons"; import { SubmissionData } from "../../types/submission"; import SubmissionSelector from "../SubmissionSelector"; import InputOutputTable from "../InputOutputTable"; +import { RuleMap } from "../../types/rulemap"; import styles from "./SimulationViewer.module.css"; // Need to disable SSR when loading this component so it works properly @@ -16,10 +17,15 @@ interface SimulationViewerProps { jsonFile: string; docId: string; chefsFormId: string; + rulemap: RuleMap; } -export default function SimulationViewer({ jsonFile, docId, chefsFormId }: SimulationViewerProps) { - const [selectedSubmissionInputs, setSelectedSubmissionInputs] = useState(); +export default function SimulationViewer({ jsonFile, docId, chefsFormId, rulemap }: SimulationViewerProps) { + const rulemapObject = rulemap.inputs.reduce((acc: { [x: string]: null }, obj: { property: string | number }) => { + acc[obj.property] = null; + return acc; + }, {}); + const [selectedSubmissionInputs, setSelectedSubmissionInputs] = useState(rulemapObject); const [contextToSimulate, setContextToSimulate] = useState(); const [resultsOfSimulation, setResultsOfSimulation] = useState | null>(); @@ -52,9 +58,14 @@ export default function SimulationViewer({ jsonFile, docId, chefsFormId }: Simul {selectedSubmissionInputs && ( - + <> + + + )} @@ -64,7 +75,13 @@ export default function SimulationViewer({ jsonFile, docId, chefsFormId }: Simul - {selectedSubmissionInputs && } + {selectedSubmissionInputs && ( + + )} {resultsOfSimulation && } diff --git a/app/rule/[ruleId]/page.tsx b/app/rule/[ruleId]/page.tsx index 3738c53..f212cae 100644 --- a/app/rule/[ruleId]/page.tsx +++ b/app/rule/[ruleId]/page.tsx @@ -2,10 +2,12 @@ import Link from "next/link"; import { Flex } from "antd"; import { HomeOutlined } from "@ant-design/icons"; import SimulationViewer from "../../components/SimulationViewer"; -import { getRuleDataById } from "../../utils/api"; +import { getRuleDataById, getRuleMapByID } from "../../utils/api"; +import { RuleMap } from "@/app/types/rulemap"; export default async function Rule({ params: { ruleId } }: { params: { ruleId: string } }) { const { title, _id, goRulesJSONFilename, chefsFormId } = await getRuleDataById(ruleId); + const rulemap: RuleMap = await getRuleMapByID(ruleId); if (!_id) { return

Rule not found

; @@ -19,7 +21,7 @@ export default async function Rule({ params: { ruleId } }: { params: { ruleId: s

{title}

- + ); } diff --git a/app/types/rulemap.d.ts b/app/types/rulemap.d.ts new file mode 100644 index 0000000..49002fc --- /dev/null +++ b/app/types/rulemap.d.ts @@ -0,0 +1,4 @@ +export interface RuleMap { + inputs: Array; + outputs: Array; +} diff --git a/app/utils/api.ts b/app/utils/api.ts index 8acdd3d..5c3e512 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -1,6 +1,7 @@ import { DecisionGraphType } from "@gorules/jdm-editor/dist/components/decision-graph/context/dg-store.context"; import axios from "axios"; import { RuleInfo } from "../types/ruleInfo"; +import { RuleMap } from "../types/rulemap"; // For server side calls, need full URL, otherwise can just use /api const API_URI = typeof window === "undefined" ? process.env.NEXT_PUBLIC_API_URL : "/api"; @@ -117,6 +118,22 @@ export const getAllRuleData = async (): Promise => { } }; +/** + * Retrieves a rule map from the API based on the provided rule ID. + * @param ruleId The ID of the rule data to retrieve. + * @returns The rule map. + * @throws If an error occurs while retrieving the rule data. + */ +export const getRuleMapByID = async (ruleId: string): Promise => { + try { + const { data } = await axios.get(`${API_URI}/rulemap/${ruleId}`); + return data; + } catch (error) { + console.error(`Error getting rule data: ${error}`); + throw error; + } +}; + /** * Posts rule data to the API. * @param newRuleData The new rule data to post. From c7b94ade4130491c1afc6e93b2d0bdd11c0491e8 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 7 Jun 2024 10:45:43 -0700 Subject: [PATCH 02/66] Update to handle both editable and non-editable input fields. --- .../InputOutputTable/InputOutputTable.tsx | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index 157f470..7d94c2e 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -15,11 +15,15 @@ const COLUMNS = [ }, ]; -const PROPERTIES_TO_IGNORE = ["submit", "lateEntry"]; +const PROPERTIES_TO_IGNORE = ["submit", "lateEntry", "rulemap"]; + +interface rawDataProps { + rulemap?: boolean; +} interface InputOutputTableProps { title: string; - rawData: object; + rawData: rawDataProps; setRawData?: (data: object) => void; } @@ -29,22 +33,14 @@ export default function InputOutputTable({ title, rawData, setRawData }: InputOu const convertAndStyleValue = (value: any, property: string, editable: boolean) => { let displayValue = value; - // Convert booleans and numbers to strings for the input field if editable if (editable) { - if (typeof value === "boolean") { - displayValue = value.toString(); - } else if (typeof value === "number") { - displayValue = value.toString(); - } return handleValueChange(e, property)} />; } - // Handle booleans + // Custom formatting for non-editable booleans and numbers if (typeof value === "boolean") { return value ? TRUE : FALSE; } - - // Handle money amounts if (typeof value === "number" && property.toLowerCase().includes("amount")) { displayValue = `$${value}`; } @@ -81,18 +77,18 @@ export default function InputOutputTable({ title, rawData, setRawData }: InputOu useEffect(() => { if (rawData) { - const newData: object[] = []; - Object.entries(rawData).forEach(([property, value], index) => { - if (!PROPERTIES_TO_IGNORE.includes(property)) { - newData.push({ - property, - value: convertAndStyleValue(value, property, true), - key: index, - }); - } - }); + const editable = title === "Inputs" && rawData.rulemap === true; + const newData = Object.entries(rawData) + .filter(([property]) => !PROPERTIES_TO_IGNORE.includes(property)) + .sort(([propertyA], [propertyB]) => propertyA.localeCompare(propertyB)) + .map(([property, value], index) => ({ + property, + value: convertAndStyleValue(value, property, editable), + key: index, + })); setDataSource(newData); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [rawData]); return ( From 409dd160bb9d50d73ed49fa81cc55b8dde8e793d Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 7 Jun 2024 10:46:06 -0700 Subject: [PATCH 03/66] Add additional rulemap field to initialized rule object. --- app/components/SimulationViewer/SimulationViewer.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 03b84fe..a718e53 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -21,10 +21,14 @@ interface SimulationViewerProps { } export default function SimulationViewer({ jsonFile, docId, chefsFormId, rulemap }: SimulationViewerProps) { - const rulemapObject = rulemap.inputs.reduce((acc: { [x: string]: null }, obj: { property: string | number }) => { - acc[obj.property] = null; - return acc; - }, {}); + const rulemapObject = rulemap.inputs.reduce( + (acc: { [x: string]: null }, obj: { property: string | number }) => { + acc[obj.property] = null; + return acc; + }, + { rulemap: true } + ); + const [selectedSubmissionInputs, setSelectedSubmissionInputs] = useState(rulemapObject); const [contextToSimulate, setContextToSimulate] = useState(); const [resultsOfSimulation, setResultsOfSimulation] = useState | null>(); From 0a664081fbe1bcd396a5b5d01a183e3b91c7a796 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 7 Jun 2024 12:04:10 -0700 Subject: [PATCH 04/66] Update submission selector state. --- .../SimulationViewer/SimulationViewer.tsx | 18 +++++++++++++++--- .../SubmissionSelector/SubmissionSelector.tsx | 13 ++++++++++++- app/rule/[ruleId]/page.tsx | 4 ++-- app/utils/api.ts | 4 ++-- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index a718e53..bc18139 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -32,6 +32,7 @@ export default function SimulationViewer({ jsonFile, docId, chefsFormId, rulemap const [selectedSubmissionInputs, setSelectedSubmissionInputs] = useState(rulemapObject); const [contextToSimulate, setContextToSimulate] = useState(); const [resultsOfSimulation, setResultsOfSimulation] = useState | null>(); + const [resetTrigger, setResetTrigger] = useState(false); const resetContextAndResults = () => { setContextToSimulate(null); @@ -60,14 +61,25 @@ export default function SimulationViewer({ jsonFile, docId, chefsFormId, rulemap - + {selectedSubmissionInputs && ( <> - )} diff --git a/app/components/SubmissionSelector/SubmissionSelector.tsx b/app/components/SubmissionSelector/SubmissionSelector.tsx index c7b8744..2e04696 100644 --- a/app/components/SubmissionSelector/SubmissionSelector.tsx +++ b/app/components/SubmissionSelector/SubmissionSelector.tsx @@ -7,9 +7,14 @@ import styles from "./SubmissionSelector.module.css"; interface SubmissionSelectorProps { chefsFormId: string; setSelectedSubmissionInputs: (newSelection: SubmissionData) => void; + resetTrigger: boolean; } -export default function SubmissionSelector({ chefsFormId, setSelectedSubmissionInputs }: SubmissionSelectorProps) { +export default function SubmissionSelector({ + chefsFormId, + setSelectedSubmissionInputs, + resetTrigger, +}: SubmissionSelectorProps) { const [options, setOptions] = useState<{ value: string }[]>([]); const [valueToIdMap, setValueToIdMap] = useState>({}); const [searchText, setSearchText] = useState(""); @@ -22,6 +27,7 @@ export default function SubmissionSelector({ chefsFormId, setSelectedSubmissionI }, } = await getSubmissionFromCHEFSById(chefsFormId, valueToIdMap[value]); setSelectedSubmissionInputs(data); + setSearchText(value); }; // Extracted data transformation logic @@ -50,11 +56,16 @@ export default function SubmissionSelector({ chefsFormId, setSelectedSubmissionI fetchData(); }, [chefsFormId]); + useEffect(() => { + setSearchText(""); + }, [resetTrigger]); + return ( diff --git a/app/rule/[ruleId]/page.tsx b/app/rule/[ruleId]/page.tsx index f212cae..6901e71 100644 --- a/app/rule/[ruleId]/page.tsx +++ b/app/rule/[ruleId]/page.tsx @@ -2,12 +2,12 @@ import Link from "next/link"; import { Flex } from "antd"; import { HomeOutlined } from "@ant-design/icons"; import SimulationViewer from "../../components/SimulationViewer"; -import { getRuleDataById, getRuleMapByID } from "../../utils/api"; +import { getRuleDataById, getRuleMapByName } from "../../utils/api"; import { RuleMap } from "@/app/types/rulemap"; export default async function Rule({ params: { ruleId } }: { params: { ruleId: string } }) { const { title, _id, goRulesJSONFilename, chefsFormId } = await getRuleDataById(ruleId); - const rulemap: RuleMap = await getRuleMapByID(ruleId); + const rulemap: RuleMap = await getRuleMapByName(goRulesJSONFilename); if (!_id) { return

Rule not found

; diff --git a/app/utils/api.ts b/app/utils/api.ts index 5c3e512..138a30f 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -124,9 +124,9 @@ export const getAllRuleData = async (): Promise => { * @returns The rule map. * @throws If an error occurs while retrieving the rule data. */ -export const getRuleMapByID = async (ruleId: string): Promise => { +export const getRuleMapByName = async (goRulesJSONFilename: string): Promise => { try { - const { data } = await axios.get(`${API_URI}/rulemap/${ruleId}`); + const { data } = await axios.get(`${API_URI}/rulemap/${goRulesJSONFilename}`); return data; } catch (error) { console.error(`Error getting rule data: ${error}`); From 2e4326cef11b0c51f74d9f49bd81490708cc42bd Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 10 Jun 2024 14:05:16 -0700 Subject: [PATCH 05/66] Update type to finaloutputs for rulemap. --- app/types/rulemap.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/types/rulemap.d.ts b/app/types/rulemap.d.ts index 49002fc..8c525ea 100644 --- a/app/types/rulemap.d.ts +++ b/app/types/rulemap.d.ts @@ -1,4 +1,5 @@ export interface RuleMap { inputs: Array; outputs: Array; + finalOutputs: Array; } From fdc444818bb38aed95d28042741eeeb1afee6b3f Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 10 Jun 2024 14:05:56 -0700 Subject: [PATCH 06/66] Add finaloutputs display of rules, with default null results. --- .../InputOutputTable/InputOutputTable.tsx | 32 +++++++++++++++++-- .../SimulationViewer/SimulationViewer.tsx | 25 +++++++++------ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index 7d94c2e..d11213f 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -25,16 +25,24 @@ interface InputOutputTableProps { title: string; rawData: rawDataProps; setRawData?: (data: object) => void; + submitButtonRef?: React.RefObject; } -export default function InputOutputTable({ title, rawData, setRawData }: InputOutputTableProps) { +export default function InputOutputTable({ title, rawData, setRawData, submitButtonRef }: InputOutputTableProps) { const [dataSource, setDataSource] = useState([]); + const [columns, setColumns] = useState(COLUMNS); const convertAndStyleValue = (value: any, property: string, editable: boolean) => { let displayValue = value; if (editable) { - return handleValueChange(e, property)} />; + return ( + handleValueChange(e, property)} + onKeyDown={(e) => handleKeyDown(e)} + /> + ); } // Custom formatting for non-editable booleans and numbers @@ -45,6 +53,10 @@ export default function InputOutputTable({ title, rawData, setRawData }: InputOu displayValue = `$${value}`; } + if (value === null || value === undefined) { + return; + } + return {displayValue}; }; @@ -75,6 +87,18 @@ export default function InputOutputTable({ title, rawData, setRawData }: InputOu } }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && submitButtonRef) { + if (submitButtonRef.current) { + submitButtonRef.current.click(); + } + } + }; + + const showColumn = (data: any[], columnKey: string) => { + return data.some((item) => item[columnKey] !== null && item[columnKey] !== undefined); + }; + useEffect(() => { if (rawData) { const editable = title === "Inputs" && rawData.rulemap === true; @@ -87,6 +111,8 @@ export default function InputOutputTable({ title, rawData, setRawData }: InputOu key: index, })); setDataSource(newData); + const newColumns = COLUMNS.filter((column) => showColumn(newData, column.dataIndex)); + setColumns(newColumns); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [rawData]); @@ -95,7 +121,7 @@ export default function InputOutputTable({ title, rawData, setRawData }: InputOu

{title}

{ + const createRuleMap = (array: any[], defaultObj: { rulemap: boolean }) => { + return array.reduce((acc, obj) => { acc[obj.property] = null; return acc; - }, - { rulemap: true } - ); + }, defaultObj); + }; + + const ruleMapInputs = createRuleMap(rulemap.inputs, { rulemap: true }); + const ruleMapFinalOutputs = createRuleMap(rulemap.finalOutputs, { rulemap: true }); - const [selectedSubmissionInputs, setSelectedSubmissionInputs] = useState(rulemapObject); + const [selectedSubmissionInputs, setSelectedSubmissionInputs] = useState(ruleMapInputs); const [contextToSimulate, setContextToSimulate] = useState(); const [resultsOfSimulation, setResultsOfSimulation] = useState | null>(); const [resetTrigger, setResetTrigger] = useState(false); + const simulateButtonRef = useRef(null); const resetContextAndResults = () => { setContextToSimulate(null); - setResultsOfSimulation(null); + setResultsOfSimulation(ruleMapFinalOutputs); }; const runSimulation = () => { @@ -47,6 +50,7 @@ export default function SimulationViewer({ jsonFile, docId, chefsFormId, rulemap useEffect(() => { // reset context/results when a new submission is selected resetContextAndResults(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedSubmissionInputs]); return ( @@ -68,14 +72,14 @@ export default function SimulationViewer({ jsonFile, docId, chefsFormId, rulemap /> {selectedSubmissionInputs && ( <> -
+

+ {title} {title === "Outputs" && } +

+ {showTable && ( + <> +
+ + )} ); } From 1e5e3053c4a73c39a24d571871ae88ca384059eb Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Tue, 11 Jun 2024 08:42:17 -0700 Subject: [PATCH 09/66] Update to query entire schema from rule run instead of only outputs. --- app/components/InputOutputTable/InputOutputTable.tsx | 1 - .../RulesDecisionGraph/RulesDecisionGraph.tsx | 10 +++++----- app/components/SimulationViewer/SimulationViewer.tsx | 1 - app/utils/api.ts | 10 +++++----- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index f6c5a3d..78d14a1 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -1,7 +1,6 @@ import { useState, useEffect, FocusEvent } from "react"; import { Table, Tag, Input, Button } from "antd"; import styles from "./InputOutputTable.module.css"; -import { table } from "console"; const COLUMNS = [ { diff --git a/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx b/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx index 5edce99..0a09cda 100644 --- a/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx +++ b/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx @@ -6,7 +6,7 @@ import { DecisionGraphType } from "@gorules/jdm-editor/dist/components/decision- import type { ReactFlowInstance } from "reactflow"; import { Spin } from "antd"; import { SubmissionData } from "../../types/submission"; -import { getDocument, postDecision, getOutputSchema } from "../../utils/api"; +import { getDocument, postDecision, getRuleRunSchema } from "../../utils/api"; import styles from "./RulesDecisionGraph.module.css"; interface RulesViewerProps { @@ -55,11 +55,11 @@ export default function RulesDecisionGraph({ const data = await postDecision(jsonFile, decisionGraph, context); console.info("Simulation Results:", data, data?.result); setResultsOfSimulation(data?.result); - const outputData = await getOutputSchema(data); - // Filter out properties from outputData that are also present in data.result - const uniqueOutputs = Object.keys(outputData?.result || {}).reduce((acc: any, key: string) => { + const ruleRunSchema = await getRuleRunSchema(data); + // Filter out properties from ruleRunSchema outputs that are also present in data.result + const uniqueOutputs = Object.keys(ruleRunSchema?.result?.output || {}).reduce((acc: any, key: string) => { if (!(key in data?.result)) { - acc[key] = outputData?.result[key]; + acc[key] = ruleRunSchema?.result[key]; } return acc; }, {}); diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 39dfef0..18771dd 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -44,7 +44,6 @@ export default function SimulationViewer({ jsonFile, docId, chefsFormId, rulemap setOutputSchema(ruleMapOutputs); setResultsOfSimulation(ruleMapFinalOutputs); }; - console.log(ruleMapOutputs, "this is the rule map outputs"); const runSimulation = () => { // set the context to simulate - RulesDecisionGraph will use this context to run the simulation diff --git a/app/utils/api.ts b/app/utils/api.ts index 2d23670..c071beb 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -135,14 +135,14 @@ export const getRuleMapByName = async (goRulesJSONFilename: string): Promise { +export const getRuleRunSchema = async (ruleResponse: unknown) => { try { - const { data } = await axios.post(`${API_URI}/rulemap/outputschema`, ruleResponse); + const { data } = await axios.post(`${API_URI}/rulemap/rulerunschema`, ruleResponse); return data; } catch (error) { console.error(`Error posting output schema: ${error}`); From 72593a50378ecf9fa3fb0083c3143eef8bdac931 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 13 Jun 2024 16:14:20 -0700 Subject: [PATCH 10/66] Add scenario viewer component. --- .../InputOutputTable/InputOutputTable.tsx | 2 +- .../ScenarioViewer/ScenarioViewer.module.css | 39 +++++++++ .../ScenarioViewer/ScenarioViewer.tsx | 86 +++++++++++++++++++ app/components/ScenarioViewer/index.ts | 1 + .../SimulationViewer/SimulationViewer.tsx | 15 +++- app/rule/[ruleId]/page.tsx | 11 ++- app/types/scenario.d.ts | 13 +++ app/utils/api.ts | 20 ++++- 8 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 app/components/ScenarioViewer/ScenarioViewer.module.css create mode 100644 app/components/ScenarioViewer/ScenarioViewer.tsx create mode 100644 app/components/ScenarioViewer/index.ts create mode 100644 app/types/scenario.d.ts diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index 78d14a1..5327789 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -23,7 +23,7 @@ interface rawDataProps { interface InputOutputTableProps { title: string; - rawData: rawDataProps; + rawData: rawDataProps | null; setRawData?: (data: object) => void; submitButtonRef?: React.RefObject; } diff --git a/app/components/ScenarioViewer/ScenarioViewer.module.css b/app/components/ScenarioViewer/ScenarioViewer.module.css new file mode 100644 index 0000000..d26eabb --- /dev/null +++ b/app/components/ScenarioViewer/ScenarioViewer.module.css @@ -0,0 +1,39 @@ +/* ScenarioViewer.module.css */ +.scenarioViewer { + display: flex; + gap: 1rem; +} + +.scenarioList, .selectedScenarioDetails, .resultsColumn { + flex: 1 1 auto; /* Each column takes up equal space initially */ + box-sizing: border-box; + padding: 1rem; + border-radius: 4px; +} + +.scenarioList { + min-width: 300px; /* Preferable width of 300px */ + max-width: 300px; /* Stable width of 300px */ + border: 1px solid #ccc; /* Border for scenarioList */ +} + +.selectedScenarioDetails { + min-width: 450px; /* Preferable width of 450px */ + max-width: 450px; /* Stable width of 450px */ + border: 1px solid #ccc; /* Border for selectedScenarioDetails */ +} + +.resultsColumn { + flex: 1 1 300px; /* Preferable width of 300px, but can expand */ + min-width: 300px; /* Stable width of 300px */ + max-width: 300px; /* Maximum width of 300px */ + display: none; /* Initially hidden */ + white-space: pre-wrap; /* For better readability of JSON */ +} + +@media (min-width: 768px) { + .resultsColumn { + display: block; /* Displayed when viewport width is 768px or more */ + border: 1px solid #ccc; /* Border for resultsColumn */ + } +} diff --git a/app/components/ScenarioViewer/ScenarioViewer.tsx b/app/components/ScenarioViewer/ScenarioViewer.tsx new file mode 100644 index 0000000..ac89925 --- /dev/null +++ b/app/components/ScenarioViewer/ScenarioViewer.tsx @@ -0,0 +1,86 @@ +"use client"; +import React, { useState, useEffect, useRef, use } from "react"; +import dynamic from "next/dynamic"; +import Link from "next/link"; +import { Flex, Button } from "antd"; +import { ExportOutlined } from "@ant-design/icons"; +import InputOutputTable from "../InputOutputTable"; +import styles from "./ScenarioViewer.module.css"; +import { Scenario, Variable } from "@/app/types/scenario"; + +interface ScenarioViewerProps { + scenarios: Scenario[]; + resultsOfSimulation: Record | null; + setSelectedSubmissionInputs: (data: any) => void; + runSimulation: () => void; +} + +export default function ScenarioViewer({ + scenarios, + resultsOfSimulation, + setSelectedSubmissionInputs, + runSimulation, +}: ScenarioViewerProps) { + const [scenariosDisplay, setScenariosDisplay] = useState(scenarios); + const [selectedScenario, setSelectedScenario] = useState(null); + + useEffect(() => { + setScenariosDisplay(scenarios); + }, [scenarios]); + + const handleSelectScenario = (scenario: Scenario) => { + setSelectedScenario(scenario); + const submissionInputs = scenario.variables.reduce((acc, variable) => { + acc[variable.name] = variable.value; + return acc; + }, {} as Record); + setSelectedSubmissionInputs(submissionInputs); + }; + + const handleRunScenario = () => { + runSimulation(); + }; + + return ( +
+
+ {scenariosDisplay && scenariosDisplay.length > 0 ? ( +
    + {scenariosDisplay.map((scenario, index) => ( +
  1. handleSelectScenario(scenario)} + className={selectedScenario === scenario ? styles.selected : ""} + > + {scenario.title} +
  2. + ))} +
+ ) : ( +
No scenarios available
+ )} +
+ {selectedScenario && ( +
+

Selected Scenario

+

{selectedScenario.title}

+
+ { + acc[variable.name] = variable.value; + return acc; + }, {} as Record)} + /> +
+ +
+ )} + {!resultsOfSimulation?.rulemap && ( +
+ +
+ )} +
+ ); +} diff --git a/app/components/ScenarioViewer/index.ts b/app/components/ScenarioViewer/index.ts new file mode 100644 index 0000000..2ef6345 --- /dev/null +++ b/app/components/ScenarioViewer/index.ts @@ -0,0 +1 @@ +export { default } from "./ScenarioViewer"; diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 53c5d89..e96ec4e 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -8,7 +8,9 @@ import { SubmissionData } from "../../types/submission"; import SubmissionSelector from "../SubmissionSelector"; import InputOutputTable from "../InputOutputTable"; import { RuleMap } from "../../types/rulemap"; +import { Scenario } from "@/app/types/scenario"; import styles from "./SimulationViewer.module.css"; +import ScenarioViewer from "../ScenarioViewer/ScenarioViewer"; // Need to disable SSR when loading this component so it works properly const RulesDecisionGraph = dynamic(() => import("../RulesDecisionGraph"), { ssr: false }); @@ -17,9 +19,10 @@ interface SimulationViewerProps { jsonFile: string; chefsFormId: string; rulemap: RuleMap; + scenarios: Scenario[]; } -export default function SimulationViewer({ jsonFile, chefsFormId, rulemap }: SimulationViewerProps) { +export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scenarios }: SimulationViewerProps) { const createRuleMap = (array: any[], defaultObj: { rulemap: boolean }) => { return array.reduce((acc, obj) => { acc[obj.property] = null; @@ -52,6 +55,7 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap }: Sim useEffect(() => { // reset context/results when a new submission is selected resetContextAndResults(); + console.log(selectedSubmissionInputs, "this is submissionn inputs updating"); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedSubmissionInputs]); @@ -67,6 +71,7 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap }: Sim setOutputsOfSimulation={setOutputSchema} /> + {/* } {resultsOfSimulation && } - + */} + ); } diff --git a/app/rule/[ruleId]/page.tsx b/app/rule/[ruleId]/page.tsx index 86ba22f..a83b990 100644 --- a/app/rule/[ruleId]/page.tsx +++ b/app/rule/[ruleId]/page.tsx @@ -2,12 +2,14 @@ import Link from "next/link"; import { Flex } from "antd"; import { HomeOutlined } from "@ant-design/icons"; import SimulationViewer from "../../components/SimulationViewer"; -import { getRuleDataById, getRuleMapByName } from "../../utils/api"; +import { getRuleDataById, getRuleMapByName, getScenariosByFilename } from "../../utils/api"; import { RuleMap } from "@/app/types/rulemap"; +import { Scenario } from "@/app/types/scenario"; export default async function Rule({ params: { ruleId } }: { params: { ruleId: string } }) { const { title, _id, goRulesJSONFilename, chefsFormId } = await getRuleDataById(ruleId); const rulemap: RuleMap = await getRuleMapByName(goRulesJSONFilename); + const scenarios: Scenario[] = await getScenariosByFilename(goRulesJSONFilename); if (!_id) { return

Rule not found

; @@ -21,7 +23,12 @@ export default async function Rule({ params: { ruleId } }: { params: { ruleId: s

{title || goRulesJSONFilename}

- + ); } diff --git a/app/types/scenario.d.ts b/app/types/scenario.d.ts new file mode 100644 index 0000000..3a97688 --- /dev/null +++ b/app/types/scenario.d.ts @@ -0,0 +1,13 @@ +export interface Scenario { + _id: string; + title: string; + ruleID: string; + goRulesJsonFilename: string; + variables: any[]; +} + +export interface Variable { + name: string; + value: any; + type: string; +} diff --git a/app/utils/api.ts b/app/utils/api.ts index 39896d2..131347d 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -177,8 +177,8 @@ export const deleteRuleData = async (ruleId: string) => { }; /** - * Retrieves a rule map from the API based on the provided rule ID. - * @param ruleId The ID of the rule data to retrieve. + * Retrieves a rule map from the API based on the provided json filename. + * @param goRulesJSONFilename The ID of the rule data to retrieve. * @returns The rule map. * @throws If an error occurs while retrieving the rule data. */ @@ -207,3 +207,19 @@ export const getRuleRunSchema = async (ruleResponse: unknown) => { throw error; } }; + +/** + * Retrieves the scenarios for a rule from the API based on the provided filename + * @param goRulesJSONFilename The name of the rule data to retrieve. + * @returns The scenarios for the rule. + * @throws If an error occurs while retrieving the rule data. + */ +export const getScenariosByFilename = async (goRulesJSONFilename: string) => { + try { + const { data } = await axiosAPIInstance.get(`/scenario/by-filename/${goRulesJSONFilename}`); + return data; + } catch (error) { + console.error(`Error posting output schema: ${error}`); + throw error; + } +}; From 6978ec41285e76cf46c12c0afb266b9b82040348 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 13 Jun 2024 16:27:01 -0700 Subject: [PATCH 11/66] Add scenario and inputs switch. --- .../SimulationViewer/SimulationViewer.tsx | 63 +++++++++++++++++-- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index e96ec4e..5ad0b0f 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -40,6 +40,7 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scena const [resultsOfSimulation, setResultsOfSimulation] = useState | null>(); const [resetTrigger, setResetTrigger] = useState(false); const simulateButtonRef = useRef(null); + const [manualOrPredefined, setManualOrPredefined] = useState(false); const resetContextAndResults = () => { setContextToSimulate(null); @@ -71,6 +72,32 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scena setOutputsOfSimulation={setOutputSchema} /> + + + + + + + {/* @@ -115,12 +142,36 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scena {outputSchema && } {resultsOfSimulation && } */} - + + + {manualOrPredefined ? ( + <> + {selectedSubmissionInputs && ( + <> + + + + )} + {outputSchema && } + {resultsOfSimulation && } + + ) : ( + + )} + + ); } From a47c6ff294d4ef7901f21243071a804d0a21dfb7 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 17 Jun 2024 08:30:37 -0700 Subject: [PATCH 12/66] Update allowed types to include undefined. --- app/components/InputOutputTable/InputOutputTable.tsx | 2 +- app/components/ScenarioViewer/ScenarioViewer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index 5327789..19f14a1 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -23,7 +23,7 @@ interface rawDataProps { interface InputOutputTableProps { title: string; - rawData: rawDataProps | null; + rawData: rawDataProps | null | undefined; setRawData?: (data: object) => void; submitButtonRef?: React.RefObject; } diff --git a/app/components/ScenarioViewer/ScenarioViewer.tsx b/app/components/ScenarioViewer/ScenarioViewer.tsx index ac89925..0043782 100644 --- a/app/components/ScenarioViewer/ScenarioViewer.tsx +++ b/app/components/ScenarioViewer/ScenarioViewer.tsx @@ -10,7 +10,7 @@ import { Scenario, Variable } from "@/app/types/scenario"; interface ScenarioViewerProps { scenarios: Scenario[]; - resultsOfSimulation: Record | null; + resultsOfSimulation: Record | null | undefined; setSelectedSubmissionInputs: (data: any) => void; runSimulation: () => void; } From 86b4fa1967b6b52f95d58f44684771d1a99393ec Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 17 Jun 2024 09:20:27 -0700 Subject: [PATCH 13/66] Update scenario selector styling. --- .../ScenarioViewer/ScenarioViewer.module.css | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/components/ScenarioViewer/ScenarioViewer.module.css b/app/components/ScenarioViewer/ScenarioViewer.module.css index d26eabb..48237d0 100644 --- a/app/components/ScenarioViewer/ScenarioViewer.module.css +++ b/app/components/ScenarioViewer/ScenarioViewer.module.css @@ -15,6 +15,37 @@ min-width: 300px; /* Preferable width of 300px */ max-width: 300px; /* Stable width of 300px */ border: 1px solid #ccc; /* Border for scenarioList */ + .selected { + background-color: rgba(209, 230, 255, 0.5); + border-radius: 5px; /* Adjust value for desired corner roundness */ + } +} + +.scenarioList ol { + list-style-type: none; /* Remove default bullets */ + counter-reset: item; /* Create a counter for the list items */ + padding: 0; /* Remove default padding */ +} + +.scenarioList li { + cursor: pointer; /* Add a pointer cursor to the scenarioList */ + counter-increment: item; /* Increment the counter for each list item */ + padding-block: 1rem; /* Add some space between list items */ + padding-inline: 0.75rem; /* Add some space between list items */ + margin-block: 0.5rem; /* Add some space between list items */ +} + +.scenarioList li:before { + content: counter(item) ""; /* Add a counter before each list item */ + /* font-weight: bold; Make the counter bold */ + color: white; /* Change the color of the counter */ + background-color: black; /* Change the background color of the counter */ + margin-right: 10px; /* Add some space after the counter */ + border-radius: 50%; /* Add border-radius to make it circular */ + width:30px; + height: 20px; + padding-block: 5px; + padding-inline: 10px; } .selectedScenarioDetails { From f2732fcd42722f86e538854b3b76f560b1ffe342 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 17 Jun 2024 10:07:12 -0700 Subject: [PATCH 14/66] Update styling of tables. --- .../ScenarioViewer/ScenarioViewer.module.css | 13 +++-- .../ScenarioViewer/ScenarioViewer.tsx | 50 +++++++++++-------- .../SimulationViewer.module.css | 4 ++ .../SimulationViewer/SimulationViewer.tsx | 10 ++-- 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/app/components/ScenarioViewer/ScenarioViewer.module.css b/app/components/ScenarioViewer/ScenarioViewer.module.css index 48237d0..1adbfc5 100644 --- a/app/components/ScenarioViewer/ScenarioViewer.module.css +++ b/app/components/ScenarioViewer/ScenarioViewer.module.css @@ -14,7 +14,6 @@ .scenarioList { min-width: 300px; /* Preferable width of 300px */ max-width: 300px; /* Stable width of 300px */ - border: 1px solid #ccc; /* Border for scenarioList */ .selected { background-color: rgba(209, 230, 255, 0.5); border-radius: 5px; /* Adjust value for desired corner roundness */ @@ -54,17 +53,25 @@ border: 1px solid #ccc; /* Border for selectedScenarioDetails */ } +.selectedScenarioDetails button { + width: 10rem; + align-self: end; +} + .resultsColumn { flex: 1 1 300px; /* Preferable width of 300px, but can expand */ min-width: 300px; /* Stable width of 300px */ max-width: 300px; /* Maximum width of 300px */ display: none; /* Initially hidden */ - white-space: pre-wrap; /* For better readability of JSON */ + white-space: pre-wrap; + border: 1px solid #6fb1fe; + background-color: rgba(209, 230, 255, 0.5); + border-radius: 5px; } @media (min-width: 768px) { .resultsColumn { display: block; /* Displayed when viewport width is 768px or more */ - border: 1px solid #ccc; /* Border for resultsColumn */ + } } diff --git a/app/components/ScenarioViewer/ScenarioViewer.tsx b/app/components/ScenarioViewer/ScenarioViewer.tsx index 0043782..5b6148a 100644 --- a/app/components/ScenarioViewer/ScenarioViewer.tsx +++ b/app/components/ScenarioViewer/ScenarioViewer.tsx @@ -42,8 +42,8 @@ export default function ScenarioViewer({ }; return ( -
-
+ + {scenariosDisplay && scenariosDisplay.length > 0 ? (
    {scenariosDisplay.map((scenario, index) => ( @@ -59,28 +59,36 @@ export default function ScenarioViewer({ ) : (
    No scenarios available
    )} -
+ {selectedScenario && ( -
-

Selected Scenario

-

{selectedScenario.title}

-
- { - acc[variable.name] = variable.value; - return acc; - }, {} as Record)} - /> -
- -
+ + +
+ { + acc[variable.name] = variable.value; + return acc; + }, {} as Record)} + /> +
+
+ +
)} + {!resultsOfSimulation?.rulemap && ( -
- -
+ <> + + <>→ + + + + + )} -
+ ); } diff --git a/app/components/SimulationViewer/SimulationViewer.module.css b/app/components/SimulationViewer/SimulationViewer.module.css index 209b335..b74b0ad 100644 --- a/app/components/SimulationViewer/SimulationViewer.module.css +++ b/app/components/SimulationViewer/SimulationViewer.module.css @@ -5,4 +5,8 @@ .contentSection { margin: "0 8px"; +} + +.inputSection button { + width: 10rem; } \ No newline at end of file diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 5ad0b0f..716dcef 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -147,17 +147,17 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scena {manualOrPredefined ? ( <> {selectedSubmissionInputs && ( - <> - + - + + )} {outputSchema && } {resultsOfSimulation && } From 49467ceedf6c90af7ae87f8b3d49dd0db65905c1 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 17 Jun 2024 11:47:26 -0700 Subject: [PATCH 15/66] Update with tab view. --- .../SimulationViewer/SimulationViewer.tsx | 162 +++++++----------- 1 file changed, 64 insertions(+), 98 deletions(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 716dcef..0aa0831 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect, useRef } from "react"; import dynamic from "next/dynamic"; import Link from "next/link"; -import { Flex, Button } from "antd"; +import { Flex, Button, Tabs } from "antd"; +import type { TabsProps } from "antd"; import { ExportOutlined } from "@ant-design/icons"; import { SubmissionData } from "../../types/submission"; import SubmissionSelector from "../SubmissionSelector"; @@ -15,6 +16,7 @@ import ScenarioViewer from "../ScenarioViewer/ScenarioViewer"; // Need to disable SSR when loading this component so it works properly const RulesDecisionGraph = dynamic(() => import("../RulesDecisionGraph"), { ssr: false }); +const { TabPane } = Tabs; interface SimulationViewerProps { jsonFile: string; chefsFormId: string; @@ -38,6 +40,7 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scena const [contextToSimulate, setContextToSimulate] = useState(); const [outputSchema, setOutputSchema] = useState | null>(ruleMapOutputs); const [resultsOfSimulation, setResultsOfSimulation] = useState | null>(); + const [activeTab, setActiveTab] = useState("scenarios"); const [resetTrigger, setResetTrigger] = useState(false); const simulateButtonRef = useRef(null); const [manualOrPredefined, setManualOrPredefined] = useState(false); @@ -56,12 +59,70 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scena useEffect(() => { // reset context/results when a new submission is selected resetContextAndResults(); - console.log(selectedSubmissionInputs, "this is submissionn inputs updating"); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedSubmissionInputs]); useEffect(() => {}, [resultsOfSimulation]); + const handleTabChange = (key: string) => { + if (key === "reset") { + handleReset(); + } + }; + + const handleReset = () => { + setSelectedSubmissionInputs(ruleMapInputs); + setResetTrigger((prev) => !prev); + }; + + const scenarioTab = ( + + ); + + const manualInputTab = ( + + {selectedSubmissionInputs && ( + + + + + )} + {outputSchema && } + {resultsOfSimulation && } + + ); + + const resetTab = ( + + ); + + const items: TabsProps["items"] = [ + { + key: "1", + label: "Simulate pre-defined test scenarios", + children: scenarioTab, + }, + { + key: "2", + label: "Simulate inputs manually", + children: manualInputTab, + }, + ]; + return (
@@ -74,102 +135,7 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scena
- - - - - - {/* - - - - {selectedSubmissionInputs && ( - <> - - - - )} - - - - - - - {selectedSubmissionInputs && ( - - )} - {outputSchema && } - {resultsOfSimulation && } - */} - - - {manualOrPredefined ? ( - <> - {selectedSubmissionInputs && ( - - - - - )} - {outputSchema && } - {resultsOfSimulation && } - - ) : ( - - )} +
From 3f380c8977eeaeb48a213b4193e12be5b6993d20 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 17 Jun 2024 13:01:09 -0700 Subject: [PATCH 16/66] Remove unused imports. --- .../SimulationViewer/SimulationViewer.tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 0aa0831..b241b8b 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -6,7 +6,6 @@ import { Flex, Button, Tabs } from "antd"; import type { TabsProps } from "antd"; import { ExportOutlined } from "@ant-design/icons"; import { SubmissionData } from "../../types/submission"; -import SubmissionSelector from "../SubmissionSelector"; import InputOutputTable from "../InputOutputTable"; import { RuleMap } from "../../types/rulemap"; import { Scenario } from "@/app/types/scenario"; @@ -16,7 +15,6 @@ import ScenarioViewer from "../ScenarioViewer/ScenarioViewer"; // Need to disable SSR when loading this component so it works properly const RulesDecisionGraph = dynamic(() => import("../RulesDecisionGraph"), { ssr: false }); -const { TabPane } = Tabs; interface SimulationViewerProps { jsonFile: string; chefsFormId: string; @@ -24,7 +22,7 @@ interface SimulationViewerProps { scenarios: Scenario[]; } -export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scenarios }: SimulationViewerProps) { +export default function SimulationViewer({ jsonFile, rulemap, scenarios }: SimulationViewerProps) { const createRuleMap = (array: any[], defaultObj: { rulemap: boolean }) => { return array.reduce((acc, obj) => { acc[obj.property] = null; @@ -40,10 +38,8 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scena const [contextToSimulate, setContextToSimulate] = useState(); const [outputSchema, setOutputSchema] = useState | null>(ruleMapOutputs); const [resultsOfSimulation, setResultsOfSimulation] = useState | null>(); - const [activeTab, setActiveTab] = useState("scenarios"); const [resetTrigger, setResetTrigger] = useState(false); const simulateButtonRef = useRef(null); - const [manualOrPredefined, setManualOrPredefined] = useState(false); const resetContextAndResults = () => { setContextToSimulate(null); @@ -104,12 +100,6 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scena ); - const resetTab = ( - - ); - const items: TabsProps["items"] = [ { key: "1", @@ -134,8 +124,11 @@ export default function SimulationViewer({ jsonFile, chefsFormId, rulemap, scena /> - - + + + From 352e378ccdbef0d38a5b864651e92ac220c61d7e Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 17 Jun 2024 14:41:48 -0700 Subject: [PATCH 17/66] Initialize scenario generator. --- .../ScenarioGenerator.module.css | 0 .../ScenarioGenerator/ScenarioGenerator.tsx | 129 ++++++++++++++++++ app/components/ScenarioGenerator/index.ts | 1 + .../SimulationViewer/SimulationViewer.tsx | 37 ++++- app/utils/api.ts | 16 +++ 5 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 app/components/ScenarioGenerator/ScenarioGenerator.module.css create mode 100644 app/components/ScenarioGenerator/ScenarioGenerator.tsx create mode 100644 app/components/ScenarioGenerator/index.ts diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.module.css b/app/components/ScenarioGenerator/ScenarioGenerator.module.css new file mode 100644 index 0000000..e69de29 diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.tsx b/app/components/ScenarioGenerator/ScenarioGenerator.tsx new file mode 100644 index 0000000..5f1ffc9 --- /dev/null +++ b/app/components/ScenarioGenerator/ScenarioGenerator.tsx @@ -0,0 +1,129 @@ +"use client"; +import React, { useState, useEffect, useRef } from "react"; +import { Flex, Button, Input } from "antd"; +import InputOutputTable from "../InputOutputTable"; +import styles from "./ScenarioGenerator.module.css"; +import { Scenario } from "@/app/types/scenario"; +import { SubmissionData } from "@/app/types/submission"; +import { createScenario } from "@/app/utils/api"; + +interface ScenarioGeneratorProps { + scenarios: Scenario[]; + resultsOfSimulation: Record | null | undefined; + setSelectedSubmissionInputs: (data: any) => void; + runSimulation: () => void; + simulateButtonRef: React.RefObject; + selectedSubmissionInputs: SubmissionData; + outputSchema: Record | null; + setOutputSchema: (data: any) => void; + resetTrigger: boolean; +} + +export default function ScenarioGenerator({ + scenarios, + resultsOfSimulation, + setSelectedSubmissionInputs, + runSimulation, + simulateButtonRef, + selectedSubmissionInputs, + outputSchema, + setOutputSchema, + resetTrigger, +}: ScenarioGeneratorProps) { + const [simulationRun, setSimulationRun] = useState(false); + const [isInputsValid, setIsInputsValid] = useState(false); + const [newScenarioName, setNewScenarioName] = useState(""); + + const handleSaveScenario = async () => { + if (!simulationRun || !selectedSubmissionInputs || !newScenarioName) return; + + const variables = Object.entries(selectedSubmissionInputs).map(([key, value]) => ({ key, value })); + + const newScenario: Scenario = { + title: newScenarioName, // User-defined title + ruleID: "**Replace with your rule ID**", // Replace with the actual rule ID + goRulesJsonFilename: "**Replace with your rule filename**", // Replace with the actual rule filename + variables, + _id: "", + }; + + try { + await createScenario(newScenario); + console.log("Scenario created successfully!"); // Handle success + setNewScenarioName(""); // Clear the input field after successful save + } catch (error) { + console.error("Error creating scenario:", error); // Handle error + } + }; + + const runScenarioSimulation = () => { + if (!selectedSubmissionInputs) return; + + runSimulation(); + setSimulationRun(true); + }; + + const validateInputs = (inputs: object) => { + return Object.values(inputs).every((value) => value !== null && value !== undefined); + }; + + useEffect(() => { + setIsInputsValid(validateInputs(selectedSubmissionInputs)); + }, [selectedSubmissionInputs]); + + useEffect(() => { + setSimulationRun(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resetTrigger]); + + return ( + + + {selectedSubmissionInputs && ( + + { + setSelectedSubmissionInputs(data); + setIsInputsValid(validateInputs(data)); + }} + submitButtonRef={simulateButtonRef} + /> + + + + {simulationRun && ( + <> + setNewScenarioName(e.target.value)} + placeholder="Enter Scenario Name" + /> + + + )} + + + + )} + + {outputSchema && } + + + {resultsOfSimulation && } + + + + ); +} diff --git a/app/components/ScenarioGenerator/index.ts b/app/components/ScenarioGenerator/index.ts new file mode 100644 index 0000000..e163497 --- /dev/null +++ b/app/components/ScenarioGenerator/index.ts @@ -0,0 +1 @@ +export { default } from "./ScenarioGenerator"; diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index b241b8b..cc3a9ba 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -11,6 +11,7 @@ import { RuleMap } from "../../types/rulemap"; import { Scenario } from "@/app/types/scenario"; import styles from "./SimulationViewer.module.css"; import ScenarioViewer from "../ScenarioViewer/ScenarioViewer"; +import ScenarioGenerator from "../ScenarioGenerator/ScenarioGenerator"; // Need to disable SSR when loading this component so it works properly const RulesDecisionGraph = dynamic(() => import("../RulesDecisionGraph"), { ssr: false }); @@ -61,13 +62,16 @@ export default function SimulationViewer({ jsonFile, rulemap, scenarios }: Simul useEffect(() => {}, [resultsOfSimulation]); const handleTabChange = (key: string) => { - if (key === "reset") { + if (key === "1") { handleReset(); } }; const handleReset = () => { - setSelectedSubmissionInputs(ruleMapInputs); + setSelectedSubmissionInputs({}); + setTimeout(() => { + setSelectedSubmissionInputs(ruleMapInputs); + }, 0); setResetTrigger((prev) => !prev); }; @@ -100,6 +104,25 @@ export default function SimulationViewer({ jsonFile, rulemap, scenarios }: Simul ); + const scenarioGenerator = ( + + + + + ); + const items: TabsProps["items"] = [ { key: "1", @@ -111,6 +134,11 @@ export default function SimulationViewer({ jsonFile, rulemap, scenarios }: Simul label: "Simulate inputs manually", children: manualInputTab, }, + { + key: "3", + label: "Scenario Generator", + children: scenarioGenerator, + }, ]; return ( @@ -125,10 +153,7 @@ export default function SimulationViewer({ jsonFile, rulemap, scenarios }: Simul - - + diff --git a/app/utils/api.ts b/app/utils/api.ts index 131347d..d7d0a3d 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -223,3 +223,19 @@ export const getScenariosByFilename = async (goRulesJSONFilename: string) => { throw error; } }; + +/** + * Creates a new scenario for a rule + * @param scenarioResponse The response from scenario creation. + * @returns The confirmation of rule posting. + * @throws If an error occurs while retrieving the rule data. + */ +export const createScenario = async (scenarioResponse: unknown) => { + try { + const { data } = await axiosAPIInstance.post(`/scenario`, scenarioResponse); + return data; + } catch (error) { + console.error(`Error posting output schema: ${error}`); + throw error; + } +}; From be63dd7aee14fa041d7cd50b59ba83785c13a26c Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 17 Jun 2024 15:01:40 -0700 Subject: [PATCH 18/66] Add scenario creation. --- .../ScenarioGenerator/ScenarioGenerator.tsx | 21 ++++++++++++------- .../SimulationViewer/SimulationViewer.tsx | 5 ++++- app/rule/[ruleId]/page.tsx | 1 + app/types/scenario.d.ts | 4 ++-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.tsx b/app/components/ScenarioGenerator/ScenarioGenerator.tsx index 5f1ffc9..64a1e3e 100644 --- a/app/components/ScenarioGenerator/ScenarioGenerator.tsx +++ b/app/components/ScenarioGenerator/ScenarioGenerator.tsx @@ -17,6 +17,8 @@ interface ScenarioGeneratorProps { outputSchema: Record | null; setOutputSchema: (data: any) => void; resetTrigger: boolean; + ruleId: string; + jsonFile: string; } export default function ScenarioGenerator({ @@ -29,6 +31,8 @@ export default function ScenarioGenerator({ outputSchema, setOutputSchema, resetTrigger, + ruleId, + jsonFile, }: ScenarioGeneratorProps) { const [simulationRun, setSimulationRun] = useState(false); const [isInputsValid, setIsInputsValid] = useState(false); @@ -37,22 +41,23 @@ export default function ScenarioGenerator({ const handleSaveScenario = async () => { if (!simulationRun || !selectedSubmissionInputs || !newScenarioName) return; - const variables = Object.entries(selectedSubmissionInputs).map(([key, value]) => ({ key, value })); + const variables = Object.entries(selectedSubmissionInputs) + .filter(([name, value]) => name !== "rulemap") + .map(([name, value]) => ({ name, value })); const newScenario: Scenario = { - title: newScenarioName, // User-defined title - ruleID: "**Replace with your rule ID**", // Replace with the actual rule ID - goRulesJsonFilename: "**Replace with your rule filename**", // Replace with the actual rule filename + title: newScenarioName, + ruleID: ruleId, + goRulesJSONFilename: jsonFile, variables, - _id: "", }; try { await createScenario(newScenario); - console.log("Scenario created successfully!"); // Handle success - setNewScenarioName(""); // Clear the input field after successful save + console.log("Scenario created successfully!"); + setNewScenarioName(""); } catch (error) { - console.error("Error creating scenario:", error); // Handle error + console.error("Error creating scenario:", error); } }; diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index cc3a9ba..66a0003 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -17,13 +17,14 @@ import ScenarioGenerator from "../ScenarioGenerator/ScenarioGenerator"; const RulesDecisionGraph = dynamic(() => import("../RulesDecisionGraph"), { ssr: false }); interface SimulationViewerProps { + ruleId: string; jsonFile: string; chefsFormId: string; rulemap: RuleMap; scenarios: Scenario[]; } -export default function SimulationViewer({ jsonFile, rulemap, scenarios }: SimulationViewerProps) { +export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios }: SimulationViewerProps) { const createRuleMap = (array: any[], defaultObj: { rulemap: boolean }) => { return array.reduce((acc, obj) => { acc[obj.property] = null; @@ -116,6 +117,8 @@ export default function SimulationViewer({ jsonFile, rulemap, scenarios }: Simul outputSchema={outputSchema} setOutputSchema={setOutputSchema} resetTrigger={resetTrigger} + ruleId={ruleId} + jsonFile={jsonFile} /> + + + )} + + ))} + + + ) : (
No scenarios available
)} diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 66a0003..3bd8271 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -85,26 +85,6 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios /> ); - const manualInputTab = ( - - {selectedSubmissionInputs && ( - - - - - )} - {outputSchema && } - {resultsOfSimulation && } - - ); - const scenarioGenerator = ( - + From 581dafbe68884fc468b345ef53a28f54fa4cfdc4 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Tue, 18 Jun 2024 12:05:06 -0700 Subject: [PATCH 21/66] Add scenario formatter for custom scenario table formatting. --- .../ScenarioFormatter.module.css | 12 + .../ScenarioFormatter/ScenarioFormatter.tsx | 214 ++++++++++++++++++ app/components/ScenarioFormatter/index.ts | 1 + .../ScenarioGenerator/ScenarioGenerator.tsx | 5 +- .../ScenarioViewer/ScenarioViewer.tsx | 15 +- 5 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 app/components/ScenarioFormatter/ScenarioFormatter.module.css create mode 100644 app/components/ScenarioFormatter/ScenarioFormatter.tsx create mode 100644 app/components/ScenarioFormatter/index.ts diff --git a/app/components/ScenarioFormatter/ScenarioFormatter.module.css b/app/components/ScenarioFormatter/ScenarioFormatter.module.css new file mode 100644 index 0000000..37f0f90 --- /dev/null +++ b/app/components/ScenarioFormatter/ScenarioFormatter.module.css @@ -0,0 +1,12 @@ +.tableTitle { + display: flex; + gap: 20px; + justify-content: space-between; + align-items: center; + margin: 0; + background: #f9f9f9; + padding: 16px; + border: 1px solid #f0f0f0; + border-radius: 8px 8px 0 0; + font-weight: normal; +} diff --git a/app/components/ScenarioFormatter/ScenarioFormatter.tsx b/app/components/ScenarioFormatter/ScenarioFormatter.tsx new file mode 100644 index 0000000..31fe642 --- /dev/null +++ b/app/components/ScenarioFormatter/ScenarioFormatter.tsx @@ -0,0 +1,214 @@ +import { useState, useEffect, FocusEvent } from "react"; +import { Table, Tag, Input, Button, Radio, AutoComplete, InputNumber } from "antd"; +import { Scenario } from "@/app/types/scenario"; +import styles from "./ScenarioFormatter.module.css"; + +const COLUMNS = [ + { + title: "Property", + dataIndex: "property", + key: "property", + }, + { + title: "Value", + dataIndex: "value", + key: "value", + }, +]; + +const PROPERTIES_TO_IGNORE = ["submit", "lateEntry", "rulemap"]; + +interface rawDataProps { + rulemap?: boolean; +} + +interface ScenarioFormatterProps { + title: string; + rawData: rawDataProps | null | undefined; + setRawData?: (data: object) => void; + submitButtonRef?: React.RefObject; + scenarios?: Scenario[]; +} + +export default function ScenarioFormatter({ + title, + rawData, + setRawData, + submitButtonRef, + scenarios, +}: ScenarioFormatterProps) { + const [dataSource, setDataSource] = useState([]); + const [columns, setColumns] = useState(COLUMNS); + const [showTable, setShowTable] = useState(true); + + const toggleTableVisibility = () => { + setShowTable(!showTable); + }; + + const getAutoCompleteOptions = (property: string) => { + if (!scenarios) return []; + const optionsSet = new Set(); + + scenarios.forEach((scenario) => { + scenario.variables + .filter((variable) => variable.name === property) + .forEach((variable) => optionsSet.add(variable.value)); + }); + + return Array.from(optionsSet).map((value) => ({ value, type: typeof value })); + }; + + const convertAndStyleValue = (value: any, property: string, editable: boolean) => { + const valuesArray = getAutoCompleteOptions(property); + let type = typeof value; + if (getAutoCompleteOptions(property).length > 0) { + type = typeof valuesArray[0].value; + } + + if (editable) { + if (type === "boolean" || typeof value === "boolean") { + return ( + handleInputChange(e.target.value, property)} value={value}> + Yes + No + + ); + } + + if (type === "string" || typeof value === "string") { + return ( + handleValueChange((e.target as HTMLInputElement).value, property)} + onKeyDown={(e) => handleKeyDown(e.target as HTMLInputElement, property)} + style={{ width: 200 }} + onChange={(val) => handleInputChange(val, property)} + /> + ); + } + + if (type === "number" || typeof value === "number") { + return ( + handleValueChange(e.target.value, property)} + onKeyDown={(e) => handleKeyDown(e, property)} + onChange={(val) => handleInputChange(val, property)} + /> + ); + } + + if (value === null || value === undefined) { + return ( + handleValueChange(e, property)} + onKeyDown={(e) => handleKeyDown(e, property)} + onChange={(e) => handleInputChange(e.target.value, property)} + /> + ); + } + } else { + if (type === "boolean" || typeof value === "boolean") { + return ( + null} value={value}> + Yes + No + + ); + } + + if (type === "string" || typeof value === "string") { + return {value}; + } + + if (type === "number" || typeof value === "number") { + if (property.toLowerCase().includes("amount")) { + return ${value}; + } else { + return {value}; + } + } + + if (value === null || value === undefined) { + return null; + } + } + + return {value}; + }; + + const handleValueChange = (value: any, property: string) => { + let queryValue: any = value; + // Handle booleans + if (typeof value === "string") { + if (value.toLowerCase() === "true") { + queryValue = true; + } else if (value.toLowerCase() === "false") { + queryValue = false; + } else if (!isNaN(Number(value))) { + // Handle numbers + queryValue = Number(value); + } + } + + const updatedData = { ...rawData, [property]: queryValue } || {}; + + // Ensure setRawData is defined before calling it + if (typeof setRawData === "function") { + setRawData(updatedData); + } else { + console.error("setRawData is not a function or is undefined"); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent, property: string) => { + if (e.key === "Enter" && submitButtonRef) { + if (submitButtonRef.current) { + submitButtonRef.current.click(); + } + } + }; + + const handleInputChange = (val: any, property: string) => { + const updatedData = { ...rawData, [property]: val }; + if (typeof setRawData === "function") { + setRawData(updatedData); + } + }; + + const showColumn = (data: any[], columnKey: string) => { + return data.some((item) => item[columnKey] !== null && item[columnKey] !== undefined); + }; + + useEffect(() => { + if (rawData) { + const editable = title === "Inputs" && rawData.rulemap === true; + const newData = Object.entries(rawData) + .filter(([property]) => !PROPERTIES_TO_IGNORE.includes(property)) + .sort(([propertyA], [propertyB]) => propertyA.localeCompare(propertyB)) + .map(([property, value], index) => ({ + property, + value: convertAndStyleValue(value, property, editable), + key: index, + })); + setDataSource(newData); + const newColumns = COLUMNS.filter((column) => showColumn(newData, column.dataIndex)); + setColumns(newColumns); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rawData]); + + return ( +
+

+ {title} {title === "Outputs" && } +

+ {showTable && ( + <> +
+ + )} + + ); +} diff --git a/app/components/ScenarioFormatter/index.ts b/app/components/ScenarioFormatter/index.ts new file mode 100644 index 0000000..4f9027a --- /dev/null +++ b/app/components/ScenarioFormatter/index.ts @@ -0,0 +1 @@ +export { default } from "./ScenarioFormatter"; diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.tsx b/app/components/ScenarioGenerator/ScenarioGenerator.tsx index 31e6d63..7544ad4 100644 --- a/app/components/ScenarioGenerator/ScenarioGenerator.tsx +++ b/app/components/ScenarioGenerator/ScenarioGenerator.tsx @@ -6,6 +6,7 @@ import styles from "./ScenarioGenerator.module.css"; import { Scenario } from "@/app/types/scenario"; import { SubmissionData } from "@/app/types/submission"; import { createScenario } from "@/app/utils/api"; +import ScenarioFormatter from "../ScenarioFormatter"; interface ScenarioGeneratorProps { scenarios: Scenario[]; @@ -66,6 +67,7 @@ export default function ScenarioGenerator({ const runScenarioSimulation = () => { if (!selectedSubmissionInputs) return; + console.log("Running scenario simulation", selectedSubmissionInputs); runSimulation(); setSimulationRun(true); @@ -89,7 +91,7 @@ export default function ScenarioGenerator({ {selectedSubmissionInputs && ( - { @@ -97,6 +99,7 @@ export default function ScenarioGenerator({ setIsInputsValid(validateInputs(data)); }} submitButtonRef={simulateButtonRef} + scenarios={scenarios} /> + + Download Scenarios + { + setFile(file as File); + message.success(`${(file as File).name} file uploaded successfully.`); + onSuccess && onSuccess("ok"); + }} + showUploadList={false} + > + + + + + + +
+ + + )} + + ); +} diff --git a/app/components/ScenarioTester/index.ts b/app/components/ScenarioTester/index.ts new file mode 100644 index 0000000..8aa3a54 --- /dev/null +++ b/app/components/ScenarioTester/index.ts @@ -0,0 +1 @@ +export { default } from "./ScenarioTester"; diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 0e57827..05d2bff 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -12,6 +12,7 @@ import { Scenario } from "@/app/types/scenario"; import styles from "./SimulationViewer.module.css"; import ScenarioViewer from "../ScenarioViewer/ScenarioViewer"; import ScenarioGenerator from "../ScenarioGenerator/ScenarioGenerator"; +import ScenarioTester from "../ScenarioTester/ScenarioTester"; // Need to disable SSR when loading this component so it works properly const RulesDecisionGraph = dynamic(() => import("../RulesDecisionGraph"), { ssr: false }); @@ -109,6 +110,8 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios ); + const scenarioTests = ; + const items: TabsProps["items"] = [ { key: "1", @@ -120,6 +123,11 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios label: "Simulate inputs manually and create new scenarios", children: scenarioGenerator, }, + { + key: "3", + label: "Export Scenario Results", + children: scenarioTests, + }, ]; return ( @@ -134,7 +142,7 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios - + diff --git a/app/utils/api.ts b/app/utils/api.ts index 7307b15..ede190c 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -258,3 +258,57 @@ export const deleteScenario = async (scenarioId: string) => { throw error; } }; +/** + * Runs all scenarios against a rule and exports the results as a CSV. + * @param goRulesJSONFilename The filename of the rule to evaluate scenarios against. + * @returns The CSV data containing the results of the scenario evaluations. + * @throws If an error occurs while running the scenarios or generating the CSV. + */ +export const runDecisionsForScenarios = async (goRulesJSONFilename: string) => { + try { + const { data } = await axiosAPIInstance.get(`/scenario/run-decisions/${goRulesJSONFilename}`); + return data; + } catch (error) { + console.error(`Error running scenarios: ${error}`); + throw error; + } +}; + +interface FileUploadResponse { + data: string; +} + +/** + * Uploads a CSV file containing scenarios and processes the scenarios against the specified rule. + * @param file The file to be uploaded. + * @param goRulesJSONFilename The filename for the JSON rule. + * @returns The processed CSV content as a string. + * @throws If an error occurs during file upload or processing. + */ +export const uploadCSVAndProcess = async (file: File, goRulesJSONFilename: string): Promise => { + const formData = new FormData(); + formData.append("file", file); + + try { + const response = await axiosAPIInstance.post(`/scenario/evaluation/upload/${goRulesJSONFilename}`, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + responseType: "blob", // Important: This ensures that the response is treated as a file + }); + + const blob = new Blob([response.data], { type: "text/csv" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + const timestamp = new Date().toISOString().replace(/:/g, "-").replace(/\.\d+/, ""); + a.download = `${goRulesJSONFilename}_tested_scenarios_${timestamp}.csv`; + a.click(); + window.URL.revokeObjectURL(url); + + return "File processed successfully"; + } catch (error) { + console.error(`Error processing CSV file: ${error}`); + throw new Error("Error processing CSV file"); + } +}; From 151d54167b18b7dd3564c79c23efba7256eddbfa Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 11:14:13 -0700 Subject: [PATCH 30/66] Clean up formatting. --- .../ScenarioTester/ScenarioTester.module.css | 77 -------- .../ScenarioTester/ScenarioTester.tsx | 169 ++++-------------- .../SimulationViewer/SimulationViewer.tsx | 7 + app/utils/api.ts | 11 +- 4 files changed, 49 insertions(+), 215 deletions(-) diff --git a/app/components/ScenarioTester/ScenarioTester.module.css b/app/components/ScenarioTester/ScenarioTester.module.css index 1adbfc5..e69de29 100644 --- a/app/components/ScenarioTester/ScenarioTester.module.css +++ b/app/components/ScenarioTester/ScenarioTester.module.css @@ -1,77 +0,0 @@ -/* ScenarioViewer.module.css */ -.scenarioViewer { - display: flex; - gap: 1rem; -} - -.scenarioList, .selectedScenarioDetails, .resultsColumn { - flex: 1 1 auto; /* Each column takes up equal space initially */ - box-sizing: border-box; - padding: 1rem; - border-radius: 4px; -} - -.scenarioList { - min-width: 300px; /* Preferable width of 300px */ - max-width: 300px; /* Stable width of 300px */ - .selected { - background-color: rgba(209, 230, 255, 0.5); - border-radius: 5px; /* Adjust value for desired corner roundness */ - } -} - -.scenarioList ol { - list-style-type: none; /* Remove default bullets */ - counter-reset: item; /* Create a counter for the list items */ - padding: 0; /* Remove default padding */ -} - -.scenarioList li { - cursor: pointer; /* Add a pointer cursor to the scenarioList */ - counter-increment: item; /* Increment the counter for each list item */ - padding-block: 1rem; /* Add some space between list items */ - padding-inline: 0.75rem; /* Add some space between list items */ - margin-block: 0.5rem; /* Add some space between list items */ -} - -.scenarioList li:before { - content: counter(item) ""; /* Add a counter before each list item */ - /* font-weight: bold; Make the counter bold */ - color: white; /* Change the color of the counter */ - background-color: black; /* Change the background color of the counter */ - margin-right: 10px; /* Add some space after the counter */ - border-radius: 50%; /* Add border-radius to make it circular */ - width:30px; - height: 20px; - padding-block: 5px; - padding-inline: 10px; -} - -.selectedScenarioDetails { - min-width: 450px; /* Preferable width of 450px */ - max-width: 450px; /* Stable width of 450px */ - border: 1px solid #ccc; /* Border for selectedScenarioDetails */ -} - -.selectedScenarioDetails button { - width: 10rem; - align-self: end; -} - -.resultsColumn { - flex: 1 1 300px; /* Preferable width of 300px, but can expand */ - min-width: 300px; /* Stable width of 300px */ - max-width: 300px; /* Maximum width of 300px */ - display: none; /* Initially hidden */ - white-space: pre-wrap; - border: 1px solid #6fb1fe; - background-color: rgba(209, 230, 255, 0.5); - border-radius: 5px; -} - -@media (min-width: 768px) { - .resultsColumn { - display: block; /* Displayed when viewport width is 768px or more */ - - } -} diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index 79f1b76..631ba54 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -4,35 +4,15 @@ import { UploadOutlined } from "@ant-design/icons"; import styles from "./ScenarioTester.module.css"; import { runDecisionsForScenarios, uploadCSVAndProcess } from "@/app/utils/api"; -const COLUMNS = [ - { - title: "Property", - dataIndex: "property", - key: "property", - }, - { - title: "Value", - dataIndex: "value", - key: "value", - }, -]; - -const PROPERTIES_TO_IGNORE = ["submit", "lateEntry", "rulemap"]; - -interface rawDataProps { - rulemap?: boolean; -} - interface ScenarioTesterProps { jsonFile: string; + uploader?: boolean; } -export default function ScenarioTester({ jsonFile }: ScenarioTesterProps) { - const [dataSource, setDataSource] = useState([]); - const [columns, setColumns] = useState(COLUMNS); - const [showTable, setShowTable] = useState(true); +export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterProps) { const [scenarioResults, setScenarioResults] = useState({}); const [file, setFile] = useState(null); + const [uploadedFile, setUploadedFile] = useState(false); type DataType = { key: string; @@ -147,96 +127,6 @@ export default function ScenarioTester({ jsonFile }: ScenarioTesterProps) { useEffect(() => { updateScenarioResults(jsonFile); }, [jsonFile]); - /* - const toggleTableVisibility = () => { - setShowTable(!showTable); - }; - - const convertAndStyleValue = (value: any, property: string, editable: boolean) => { - let displayValue = value; - - if (editable) { - return ( - handleValueChange(e, property)} - onKeyDown={(e) => handleKeyDown(e)} - /> - ); - } - - // Custom formatting for non-editable booleans and numbers - if (typeof value === "boolean") { - return value ? TRUE : FALSE; - } - if (typeof value === "number" && property.toLowerCase().includes("amount")) { - displayValue = `$${value}`; - } - - if (value === null || value === undefined) { - return; - } - - return {displayValue}; - }; - - const handleValueChange = (e: FocusEvent, property: string) => { - if (!e.target) return; - const newValue = (e.target as HTMLInputElement).value; - let queryValue: any = newValue; - - // Handle booleans - if (newValue.toLowerCase() === "true") { - queryValue = true; - } else if (newValue.toLowerCase() === "false") { - queryValue = false; - } - - // Handle numbers - if (!isNaN(Number(newValue))) { - queryValue = Number(newValue); - } - - const updatedData = { ...rawData, [property]: queryValue } || {}; - - // Ensure setRawData is defined before calling it - if (typeof setRawData === "function") { - setRawData(updatedData); - } else { - console.error("setRawData is not a function or is undefined"); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && submitButtonRef) { - if (submitButtonRef.current) { - submitButtonRef.current.click(); - } - } - }; - - const showColumn = (data: any[], columnKey: string) => { - return data.some((item) => item[columnKey] !== null && item[columnKey] !== undefined); - }; - - useEffect(() => { - if (rawData) { - const editable = title === "Inputs" && rawData.rulemap === true; - const newData = Object.entries(rawData) - .filter(([property]) => !PROPERTIES_TO_IGNORE.includes(property)) - .sort(([propertyA], [propertyB]) => propertyA.localeCompare(propertyB)) - .map(([property, value], index) => ({ - property, - value: convertAndStyleValue(value, property, editable), - key: index, - })); - setDataSource(newData); - const newColumns = COLUMNS.filter((column) => showColumn(newData, column.dataIndex)); - setColumns(newColumns); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rawData]); -*/ const handleUpload = (info: any) => { setFile(info.file.originFileObj); @@ -263,30 +153,45 @@ export default function ScenarioTester({ jsonFile }: ScenarioTesterProps) { return (
- {showTable && ( + {uploader ? ( + + Download Scenarios + { + setFile(file as File); + message.success(`${(file as File).name} file uploaded successfully.`); + onSuccess && onSuccess("ok"); + setUploadedFile(true); + }} + onRemove={() => { + setFile(null); + setUploadedFile(false); + }} + showUploadList={true} + > + + + + + ) : ( <> - - Download Scenarios - { - setFile(file as File); - message.success(`${(file as File).name} file uploaded successfully.`); - onSuccess && onSuccess("ok"); - }} - showUploadList={false} - > - - - -
; + const csvScenarioTests = ; + const items: TabsProps["items"] = [ { key: "1", @@ -128,6 +130,11 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios label: "Export Scenario Results", children: scenarioTests, }, + { + key: "4", + label: "CSV Tests", + children: csvScenarioTests, + }, ]; return ( diff --git a/app/utils/api.ts b/app/utils/api.ts index ede190c..be10d40 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -274,10 +274,6 @@ export const runDecisionsForScenarios = async (goRulesJSONFilename: string) => { } }; -interface FileUploadResponse { - data: string; -} - /** * Uploads a CSV file containing scenarios and processes the scenarios against the specified rule. * @param file The file to be uploaded. @@ -294,7 +290,7 @@ export const uploadCSVAndProcess = async (file: File, goRulesJSONFilename: strin headers: { "Content-Type": "multipart/form-data", }, - responseType: "blob", // Important: This ensures that the response is treated as a file + responseType: "blob", }); const blob = new Blob([response.data], { type: "text/csv" }); @@ -302,7 +298,10 @@ export const uploadCSVAndProcess = async (file: File, goRulesJSONFilename: strin const a = document.createElement("a"); a.href = url; const timestamp = new Date().toISOString().replace(/:/g, "-").replace(/\.\d+/, ""); - a.download = `${goRulesJSONFilename}_tested_scenarios_${timestamp}.csv`; + a.download = `${goRulesJSONFilename.replace(".json", "")}_testing_${file.name.replace( + ".csv", + "" + )}_${timestamp}.csv`; a.click(); window.URL.revokeObjectURL(url); From 7082b0cf74d50f6d2a07648cef0f985bc3342375 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 11:20:09 -0700 Subject: [PATCH 31/66] Update types and remove unused imports. --- app/components/ScenarioTester/ScenarioTester.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index 631ba54..d23cccf 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, FocusEvent, use } from "react"; -import { Table, Tag, Input, Button, TableProps, Flex, Upload, message } from "antd"; +import { useState, useEffect } from "react"; +import { Table, Tag, Button, TableProps, Flex, Upload, message } from "antd"; import { UploadOutlined } from "@ant-design/icons"; import styles from "./ScenarioTester.module.css"; import { runDecisionsForScenarios, uploadCSVAndProcess } from "@/app/utils/api"; @@ -20,7 +20,9 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro [key: string]: any; }; - const formatData = (data: any): { formattedData: DataType[]; columns: TableProps["columns"] } => { + const formatData = ( + data: Record; outputs: Record }> + ): { formattedData: DataType[]; columns: TableProps["columns"] } => { const uniqueInputKeys = new Set(); const uniqueOutputKeys = new Set(); @@ -86,7 +88,7 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro title: key, dataIndex: `${prefix.toLowerCase()}_${key}`, key: `${prefix.toLowerCase()}_${key}`, - render: (value) => applyConditionalStyling(value, key), + render: (value: any) => applyConditionalStyling(value, key), })); }; @@ -126,6 +128,7 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro useEffect(() => { updateScenarioResults(jsonFile); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [jsonFile]); const handleUpload = (info: any) => { From 40b02268864e87d6b58222a68da37212e4b54f05 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 11:28:40 -0700 Subject: [PATCH 32/66] Update styling and remove unused imports. --- .../SimulationViewer/SimulationViewer.tsx | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 6979b73..a5cc0de 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -1,12 +1,9 @@ "use client"; import React, { useState, useEffect, useRef } from "react"; import dynamic from "next/dynamic"; -import Link from "next/link"; import { Flex, Button, Tabs } from "antd"; import type { TabsProps } from "antd"; -import { ExportOutlined } from "@ant-design/icons"; import { SubmissionData } from "../../types/submission"; -import InputOutputTable from "../InputOutputTable"; import { RuleMap } from "../../types/rulemap"; import { Scenario } from "@/app/types/scenario"; import styles from "./SimulationViewer.module.css"; @@ -79,18 +76,20 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios const scenarioTab = ( <> - - Export Scenario Results + + + Export Scenario Results + ); const scenarioGenerator = ( - + ; - const csvScenarioTests = ; + const csvScenarioTests = ( + + + + ); const items: TabsProps["items"] = [ { From e0a40a8acf4934cb7aa75bf1e981756707702400 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 11:32:30 -0700 Subject: [PATCH 33/66] Update styling and remove unused imports. --- .../ScenarioViewer/ScenarioViewer.module.css | 52 +++++++++---------- .../ScenarioViewer/ScenarioViewer.tsx | 3 +- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/app/components/ScenarioViewer/ScenarioViewer.module.css b/app/components/ScenarioViewer/ScenarioViewer.module.css index 1adbfc5..45a77f1 100644 --- a/app/components/ScenarioViewer/ScenarioViewer.module.css +++ b/app/components/ScenarioViewer/ScenarioViewer.module.css @@ -1,46 +1,44 @@ -/* ScenarioViewer.module.css */ .scenarioViewer { display: flex; gap: 1rem; } .scenarioList, .selectedScenarioDetails, .resultsColumn { - flex: 1 1 auto; /* Each column takes up equal space initially */ + flex: 1 1 auto; /* Initializes with each column taking up equal space */ box-sizing: border-box; padding: 1rem; border-radius: 4px; } .scenarioList { - min-width: 300px; /* Preferable width of 300px */ - max-width: 300px; /* Stable width of 300px */ + min-width: 300px; + max-width: 300px; .selected { background-color: rgba(209, 230, 255, 0.5); - border-radius: 5px; /* Adjust value for desired corner roundness */ + border-radius: 5px; } } .scenarioList ol { - list-style-type: none; /* Remove default bullets */ - counter-reset: item; /* Create a counter for the list items */ - padding: 0; /* Remove default padding */ + list-style-type: none; + counter-reset: item; + padding: 0; } .scenarioList li { - cursor: pointer; /* Add a pointer cursor to the scenarioList */ - counter-increment: item; /* Increment the counter for each list item */ - padding-block: 1rem; /* Add some space between list items */ - padding-inline: 0.75rem; /* Add some space between list items */ - margin-block: 0.5rem; /* Add some space between list items */ + cursor: pointer; + counter-increment: item; + padding-block: 1rem; + padding-inline: 0.75rem; + margin-block: 0.5rem; } .scenarioList li:before { - content: counter(item) ""; /* Add a counter before each list item */ - /* font-weight: bold; Make the counter bold */ - color: white; /* Change the color of the counter */ - background-color: black; /* Change the background color of the counter */ - margin-right: 10px; /* Add some space after the counter */ - border-radius: 50%; /* Add border-radius to make it circular */ + content: counter(item) ""; + color: white; + background-color: black; + margin-right: 10px; + border-radius: 50%; width:30px; height: 20px; padding-block: 5px; @@ -48,9 +46,9 @@ } .selectedScenarioDetails { - min-width: 450px; /* Preferable width of 450px */ - max-width: 450px; /* Stable width of 450px */ - border: 1px solid #ccc; /* Border for selectedScenarioDetails */ + min-width: 450px; + max-width: 450px; + border: 1px solid #ccc; } .selectedScenarioDetails button { @@ -59,10 +57,10 @@ } .resultsColumn { - flex: 1 1 300px; /* Preferable width of 300px, but can expand */ - min-width: 300px; /* Stable width of 300px */ - max-width: 300px; /* Maximum width of 300px */ - display: none; /* Initially hidden */ + flex: 1 1 300px; + min-width: 300px; + max-width: 300px; + display: none; white-space: pre-wrap; border: 1px solid #6fb1fe; background-color: rgba(209, 230, 255, 0.5); @@ -71,7 +69,7 @@ @media (min-width: 768px) { .resultsColumn { - display: block; /* Displayed when viewport width is 768px or more */ + display: block; } } diff --git a/app/components/ScenarioViewer/ScenarioViewer.tsx b/app/components/ScenarioViewer/ScenarioViewer.tsx index e82591e..c4858ae 100644 --- a/app/components/ScenarioViewer/ScenarioViewer.tsx +++ b/app/components/ScenarioViewer/ScenarioViewer.tsx @@ -1,5 +1,4 @@ -"use client"; -import React, { useState, useEffect, useRef, use } from "react"; +import React, { useState, useEffect } from "react"; import { Flex, Button, Popconfirm, message } from "antd"; import type { PopconfirmProps } from "antd"; import { DeleteOutlined } from "@ant-design/icons"; From 28ee5bc06e10a1e435b9aeebd60b39ae79cbd5b7 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 12:13:13 -0700 Subject: [PATCH 34/66] Update styling and remove unused imports. --- .../ScenarioFormatter/ScenarioFormatter.tsx | 30 ++--------------- .../ScenarioGenerator/ScenarioGenerator.tsx | 33 ++----------------- .../ScenarioTester/ScenarioTester.tsx | 11 +------ .../SimulationViewer/SimulationViewer.tsx | 3 -- app/page.tsx | 4 +-- 5 files changed, 7 insertions(+), 74 deletions(-) diff --git a/app/components/ScenarioFormatter/ScenarioFormatter.tsx b/app/components/ScenarioFormatter/ScenarioFormatter.tsx index 63403eb..4ef56f7 100644 --- a/app/components/ScenarioFormatter/ScenarioFormatter.tsx +++ b/app/components/ScenarioFormatter/ScenarioFormatter.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, FocusEvent } from "react"; +import { useState, useEffect } from "react"; import { Table, Tag, Input, Button, Radio, AutoComplete, InputNumber } from "antd"; import { Scenario } from "@/app/types/scenario"; import styles from "./ScenarioFormatter.module.css"; @@ -26,17 +26,10 @@ interface ScenarioFormatterProps { title: string; rawData: rawDataProps | null | undefined; setRawData?: (data: object) => void; - submitButtonRef?: React.RefObject; scenarios?: Scenario[]; } -export default function ScenarioFormatter({ - title, - rawData, - setRawData, - submitButtonRef, - scenarios, -}: ScenarioFormatterProps) { +export default function ScenarioFormatter({ title, rawData, setRawData, scenarios }: ScenarioFormatterProps) { const [dataSource, setDataSource] = useState([]); const [columns, setColumns] = useState(COLUMNS); const [showTable, setShowTable] = useState(true); @@ -92,19 +85,13 @@ export default function ScenarioFormatter({ handleValueChange(e.target.value, property)} - onKeyDown={(e) => handleKeyDown(e, property)} onChange={(val) => handleInputChange(val, property)} /> ); } if (value === null || value === undefined) { - return ( - handleValueChange(e.target.value, property)} - onKeyDown={(e) => handleKeyDown(e, property)} - /> - ); + return handleValueChange(e.target.value, property)} />; } } else { if (type === "boolean" || typeof value === "boolean") { @@ -138,21 +125,18 @@ export default function ScenarioFormatter({ const handleValueChange = (value: any, property: string) => { let queryValue: any = value; - // Handle booleans if (typeof value === "string") { if (value.toLowerCase() === "true") { queryValue = true; } else if (value.toLowerCase() === "false") { queryValue = false; } else if (!isNaN(Number(value))) { - // Handle numbers queryValue = Number(value); } } const updatedData = { ...rawData, [property]: queryValue } || {}; - // Ensure setRawData is defined before calling it if (typeof setRawData === "function") { setRawData(updatedData); } else { @@ -160,14 +144,6 @@ export default function ScenarioFormatter({ } }; - const handleKeyDown = (e: React.KeyboardEvent, property: string) => { - if (e.key === "Enter" && submitButtonRef) { - if (submitButtonRef.current) { - submitButtonRef.current.click(); - } - } - }; - const handleInputChange = (val: any, property: string) => { const updatedData = { ...rawData, [property]: val }; if (typeof setRawData === "function") { diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.tsx b/app/components/ScenarioGenerator/ScenarioGenerator.tsx index 0cdb3f7..eea0d8c 100644 --- a/app/components/ScenarioGenerator/ScenarioGenerator.tsx +++ b/app/components/ScenarioGenerator/ScenarioGenerator.tsx @@ -1,5 +1,4 @@ -"use client"; -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect } from "react"; import { Flex, Button, Input } from "antd"; import InputOutputTable from "../InputOutputTable"; import styles from "./ScenarioGenerator.module.css"; @@ -13,10 +12,7 @@ interface ScenarioGeneratorProps { resultsOfSimulation: Record | null | undefined; setSelectedSubmissionInputs: (data: any) => void; runSimulation: () => void; - simulateButtonRef: React.RefObject; selectedSubmissionInputs: SubmissionData; - outputSchema: Record | null; - setOutputSchema: (data: any) => void; resetTrigger: boolean; ruleId: string; jsonFile: string; @@ -27,16 +23,12 @@ export default function ScenarioGenerator({ resultsOfSimulation, setSelectedSubmissionInputs, runSimulation, - simulateButtonRef, selectedSubmissionInputs, - outputSchema, - setOutputSchema, resetTrigger, ruleId, jsonFile, }: ScenarioGeneratorProps) { const [simulationRun, setSimulationRun] = useState(false); - const [isInputsValid, setIsInputsValid] = useState(false); const [newScenarioName, setNewScenarioName] = useState(""); const handleSaveScenario = async () => { @@ -55,8 +47,6 @@ export default function ScenarioGenerator({ try { await createScenario(newScenario); - - console.log("Scenario created successfully!"); setNewScenarioName(""); // Reload the page after the scenario is successfully created window.location.reload(); @@ -71,14 +61,6 @@ export default function ScenarioGenerator({ setSimulationRun(true); }; - const validateInputs = (inputs: object) => { - return Object.values(inputs).every((value) => value !== null && value !== undefined); - }; - - useEffect(() => { - setIsInputsValid(validateInputs(selectedSubmissionInputs)); - }, [selectedSubmissionInputs]); - useEffect(() => { setSimulationRun(false); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -94,19 +76,11 @@ export default function ScenarioGenerator({ rawData={selectedSubmissionInputs} setRawData={(data) => { setSelectedSubmissionInputs(data); - setIsInputsValid(validateInputs(data)); }} - submitButtonRef={simulateButtonRef} scenarios={scenarios} /> - @@ -126,9 +100,6 @@ export default function ScenarioGenerator({ )} - {/* - {outputSchema && } - */} {resultsOfSimulation && } diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index d23cccf..b7341d9 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -131,23 +131,14 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro // eslint-disable-next-line react-hooks/exhaustive-deps }, [jsonFile]); - const handleUpload = (info: any) => { - setFile(info.file.originFileObj); - message.success(`${info.file.name} file uploaded successfully.`); - console.log("File uploaded:", info.file.originFileObj); - }; - const handleRunUploadScenarios = async () => { if (!file) { message.error("No file uploaded."); return; } - try { - console.log("Uploading file"); const csvContent = await uploadCSVAndProcess(file, jsonFile); - message.success("Scenarios processed successfully."); - console.log("Processed CSV content:", csvContent); + message.success(`Scenarios Test: ${csvContent}`); } catch (error) { message.error("Error processing scenarios."); console.error("Error:", error); diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index a5cc0de..fbd07b2 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -95,10 +95,7 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios setSelectedSubmissionInputs={setSelectedSubmissionInputs} resultsOfSimulation={resultsOfSimulation} runSimulation={runSimulation} - simulateButtonRef={simulateButtonRef} selectedSubmissionInputs={selectedSubmissionInputs} - outputSchema={outputSchema} - setOutputSchema={setOutputSchema} resetTrigger={resetTrigger} ruleId={ruleId} jsonFile={jsonFile} diff --git a/app/page.tsx b/app/page.tsx index e2ce8ee..2b99a5a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -31,9 +31,7 @@ export default function Home() { ), downloadRule: ( - - Download JSON - + Download JSON ), submissionFormLink: Submission, }; From 4feff44a73fb8137ea067a21c4233eea08e5fb93 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 12:14:24 -0700 Subject: [PATCH 35/66] Restrict uploads to csv. --- app/components/ScenarioTester/ScenarioTester.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index b7341d9..a50b7df 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -151,7 +151,7 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro Download Scenarios { From 0730117bc823961ed67c07ff3fb8f5255f315f8c Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 15:38:09 -0700 Subject: [PATCH 36/66] Add editable prop. --- app/components/InputOutputTable/InputOutputTable.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index 19f14a1..b8a554d 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -26,9 +26,16 @@ interface InputOutputTableProps { rawData: rawDataProps | null | undefined; setRawData?: (data: object) => void; submitButtonRef?: React.RefObject; + editable?: boolean; } -export default function InputOutputTable({ title, rawData, setRawData, submitButtonRef }: InputOutputTableProps) { +export default function InputOutputTable({ + title, + rawData, + setRawData, + submitButtonRef, + editable = false, +}: InputOutputTableProps) { const [dataSource, setDataSource] = useState([]); const [columns, setColumns] = useState(COLUMNS); const [showTable, setShowTable] = useState(true); @@ -106,7 +113,6 @@ export default function InputOutputTable({ title, rawData, setRawData, submitBut useEffect(() => { if (rawData) { - const editable = title === "Inputs" && rawData.rulemap === true; const newData = Object.entries(rawData) .filter(([property]) => !PROPERTIES_TO_IGNORE.includes(property)) .sort(([propertyA], [propertyB]) => propertyA.localeCompare(propertyB)) From 679baada53b63ad22af8f42f5db5cc74864af726 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 15:38:30 -0700 Subject: [PATCH 37/66] Update type to include expectedResults for scenarios. --- app/types/scenario.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/types/scenario.d.ts b/app/types/scenario.d.ts index 79b0e69..0cc672b 100644 --- a/app/types/scenario.d.ts +++ b/app/types/scenario.d.ts @@ -4,6 +4,7 @@ export interface Scenario { ruleID: string; goRulesJSONFilename: string; variables: any[]; + expectedResults: any[]; } export interface Variable { From 76b3941490f141007286f6c988a6031cbad1356f Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 16:00:30 -0700 Subject: [PATCH 38/66] Refactor code to clarify use of table. --- .../InputOutputTable/InputOutputTable.tsx | 49 +++++++------------ 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index b8a554d..30ab64d 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -18,13 +18,13 @@ const COLUMNS = [ const PROPERTIES_TO_IGNORE = ["submit", "lateEntry", "rulemap"]; interface rawDataProps { - rulemap?: boolean; + [key: string]: any; } interface InputOutputTableProps { title: string; rawData: rawDataProps | null | undefined; - setRawData?: (data: object) => void; + setRawData?: (data: rawDataProps) => void; submitButtonRef?: React.RefObject; editable?: boolean; } @@ -45,31 +45,25 @@ export default function InputOutputTable({ }; const convertAndStyleValue = (value: any, property: string, editable: boolean) => { - let displayValue = value; - if (editable) { return ( handleValueChange(e, property)} onKeyDown={(e) => handleKeyDown(e)} /> ); } - // Custom formatting for non-editable booleans and numbers if (typeof value === "boolean") { return value ? TRUE : FALSE; } - if (typeof value === "number" && property.toLowerCase().includes("amount")) { - displayValue = `$${value}`; - } - if (value === null || value === undefined) { - return; + if (typeof value === "number" && property.toLowerCase().includes("amount")) { + return `$${value}`; } - return {displayValue}; + return {value}; }; const handleValueChange = (e: FocusEvent, property: string) => { @@ -77,21 +71,16 @@ export default function InputOutputTable({ const newValue = (e.target as HTMLInputElement).value; let queryValue: any = newValue; - // Handle booleans if (newValue.toLowerCase() === "true") { queryValue = true; } else if (newValue.toLowerCase() === "false") { queryValue = false; - } - - // Handle numbers - if (!isNaN(Number(newValue))) { + } else if (!isNaN(Number(newValue))) { queryValue = Number(newValue); } const updatedData = { ...rawData, [property]: queryValue } || {}; - // Ensure setRawData is defined before calling it if (typeof setRawData === "function") { setRawData(updatedData); } else { @@ -107,10 +96,6 @@ export default function InputOutputTable({ } }; - const showColumn = (data: any[], columnKey: string) => { - return data.some((item) => item[columnKey] !== null && item[columnKey] !== undefined); - }; - useEffect(() => { if (rawData) { const newData = Object.entries(rawData) @@ -128,21 +113,23 @@ export default function InputOutputTable({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [rawData]); + const showColumn = (data: any[], columnKey: string) => { + return data.some((item) => item[columnKey] !== null && item[columnKey] !== undefined); + }; + return (

{title} {title === "Outputs" && }

{showTable && ( - <> -
- +
)} ); From 5a320d1f8f0fb1c990326e45af0bec39c17e844b Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 16:31:42 -0700 Subject: [PATCH 39/66] Add expected results field. --- .../ScenarioGenerator/ScenarioGenerator.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.tsx b/app/components/ScenarioGenerator/ScenarioGenerator.tsx index eea0d8c..4c20c4c 100644 --- a/app/components/ScenarioGenerator/ScenarioGenerator.tsx +++ b/app/components/ScenarioGenerator/ScenarioGenerator.tsx @@ -30,6 +30,7 @@ export default function ScenarioGenerator({ }: ScenarioGeneratorProps) { const [simulationRun, setSimulationRun] = useState(false); const [newScenarioName, setNewScenarioName] = useState(""); + const [scenarioExpectedOutput, setScenarioExpectedOutput] = useState({}); const handleSaveScenario = async () => { if (!simulationRun || !selectedSubmissionInputs || !newScenarioName) return; @@ -38,11 +39,16 @@ export default function ScenarioGenerator({ .filter(([name, value]) => name !== "rulemap") .map(([name, value]) => ({ name, value })); + const expectedResults = Object.entries(scenarioExpectedOutput) + .filter(([name, value]) => name !== "rulemap") + .map(([name, value]) => ({ name, value })); + const newScenario: Scenario = { title: newScenarioName, ruleID: ruleId, goRulesJSONFilename: jsonFile, variables, + expectedResults, }; try { @@ -66,6 +72,14 @@ export default function ScenarioGenerator({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [resetTrigger]); + // Update scenarioExpectedOutput on first render to display full rulemap possible results + useEffect(() => { + if (resultsOfSimulation) { + setScenarioExpectedOutput(resultsOfSimulation); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( @@ -103,6 +117,18 @@ export default function ScenarioGenerator({ {resultsOfSimulation && } + + {scenarioExpectedOutput && ( + { + setScenarioExpectedOutput(data); + }} + title="Expected Results" + rawData={scenarioExpectedOutput} + editable + /> + )} + ); From b8c1f5b91c472e968794293b03552f3d004912fe Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 09:26:35 -0700 Subject: [PATCH 40/66] Update testing to reference results instead of outputs. --- .../ScenarioTester/ScenarioTester.tsx | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index a50b7df..9766d2f 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -21,22 +21,29 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro }; const formatData = ( - data: Record; outputs: Record }> + data: Record< + string, + { + result: Record; + inputs: Record; + outputs: Record; + } + > ): { formattedData: DataType[]; columns: TableProps["columns"] } => { const uniqueInputKeys = new Set(); - const uniqueOutputKeys = new Set(); + const uniqueResultKeys = new Set(); - // Collect unique input and output keys + // Collect unique input and result keys for (const entry of Object.values(data)) { Object.keys(entry.inputs).forEach((key) => uniqueInputKeys.add(key)); - Object.keys(entry.outputs).forEach((key) => uniqueOutputKeys.add(key)); + Object.keys(entry.result).forEach((key) => uniqueResultKeys.add(key)); } const sortKeys = (keys: string[]) => keys.sort((a, b) => a.localeCompare(b)); // Convert sets to arrays for easier iteration const inputKeys = sortKeys(Array.from(uniqueInputKeys)); - const outputKeys = sortKeys(Array.from(uniqueOutputKeys)); + const resultKeys = sortKeys(Array.from(uniqueResultKeys)); const applyConditionalStyling = (value: any, property: string): React.ReactNode => { // Handle null or undefined values @@ -75,9 +82,9 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro }); // Add outputs - outputKeys.forEach((key) => { + resultKeys.forEach((key) => { formattedEntry[`output_${key}`] = - entry.outputs[key] !== undefined ? applyConditionalStyling(entry.outputs[key], key) : null; + entry.result[key] !== undefined ? applyConditionalStyling(entry.result[key], key) : null; }); return formattedEntry; @@ -93,7 +100,7 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro }; const inputColumns = generateColumns(inputKeys, "input"); - const outputColumns = generateColumns(outputKeys, "output"); + const outputColumns = generateColumns(resultKeys, "output"); const columns: TableProps["columns"] = [ { @@ -108,7 +115,7 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro children: inputColumns, }, { - title: "Outputs", + title: "Results", children: outputColumns, }, ]; @@ -119,6 +126,7 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro const updateScenarioResults = async (goRulesJSONFilename: string) => { try { const results = await runDecisionsForScenarios(goRulesJSONFilename); + console.log(results, "these are unformatted?"); const formattedResults = formatData(results); setScenarioResults(formattedResults); } catch (error) { From 3523dd328caeddc877f132d19a63ce6c378f9d64 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 09:56:40 -0700 Subject: [PATCH 41/66] Add expected results table display. --- .../ScenarioTester/ScenarioTester.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index 9766d2f..351c13b 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -27,16 +27,19 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro result: Record; inputs: Record; outputs: Record; + expectedResults: Record; } > ): { formattedData: DataType[]; columns: TableProps["columns"] } => { const uniqueInputKeys = new Set(); const uniqueResultKeys = new Set(); + const uniqueExpectedKeys = new Set(); // Collect unique input and result keys for (const entry of Object.values(data)) { Object.keys(entry.inputs).forEach((key) => uniqueInputKeys.add(key)); Object.keys(entry.result).forEach((key) => uniqueResultKeys.add(key)); + Object.keys(entry.expectedResults).forEach((key) => uniqueExpectedKeys.add(key)); } const sortKeys = (keys: string[]) => keys.sort((a, b) => a.localeCompare(b)); @@ -44,6 +47,8 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro // Convert sets to arrays for easier iteration const inputKeys = sortKeys(Array.from(uniqueInputKeys)); const resultKeys = sortKeys(Array.from(uniqueResultKeys)); + const expectedKeys = sortKeys(Array.from(uniqueExpectedKeys)); + console.log(expectedKeys, "expected keys"); const applyConditionalStyling = (value: any, property: string): React.ReactNode => { // Handle null or undefined values @@ -83,10 +88,16 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro // Add outputs resultKeys.forEach((key) => { - formattedEntry[`output_${key}`] = + formattedEntry[`result_${key}`] = entry.result[key] !== undefined ? applyConditionalStyling(entry.result[key], key) : null; }); + // Add expected results + expectedKeys.forEach((key) => { + formattedEntry[`expected_result_${key}`] = + entry.expectedResults[key] !== undefined ? applyConditionalStyling(entry.expectedResults[key], key) : null; + }); + return formattedEntry; }); @@ -100,7 +111,8 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro }; const inputColumns = generateColumns(inputKeys, "input"); - const outputColumns = generateColumns(resultKeys, "output"); + const outputColumns = generateColumns(resultKeys, "result"); + const expectedColumns = generateColumns(expectedKeys, "expected_result"); const columns: TableProps["columns"] = [ { @@ -118,6 +130,10 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro title: "Results", children: outputColumns, }, + { + title: "Expected Results", + children: expectedColumns, + }, ]; return { formattedData, columns }; @@ -126,8 +142,8 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro const updateScenarioResults = async (goRulesJSONFilename: string) => { try { const results = await runDecisionsForScenarios(goRulesJSONFilename); - console.log(results, "these are unformatted?"); const formattedResults = formatData(results); + console.log(formattedResults, "these are formatted?"); setScenarioResults(formattedResults); } catch (error) { console.error("Error fetching scenario results:", error); From 2be39c1ece493c5a8bd37dd51725e367fa38b325 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 15:35:17 -0700 Subject: [PATCH 42/66] Scenarios updated with status and expected results. --- .../ScenarioTester/ScenarioTester.tsx | 127 ++++++++++++------ 1 file changed, 89 insertions(+), 38 deletions(-) diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index 351c13b..1d4895f 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { Table, Tag, Button, TableProps, Flex, Upload, message } from "antd"; +import { CheckCircleOutlined, CloseCircleOutlined } from "@ant-design/icons"; import { UploadOutlined } from "@ant-design/icons"; import styles from "./ScenarioTester.module.css"; import { runDecisionsForScenarios, uploadCSVAndProcess } from "@/app/utils/api"; @@ -20,6 +21,46 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro [key: string]: any; }; + const applyConditionalStyling = (value: any, property: string): React.ReactNode => { + if (value === null || value === undefined) { + return null; + } + + if (typeof value === "boolean" && property === "resultMatch") { + return value ? ( + + }> + + ) : ( + + }> + + ); + } else if (typeof value === "boolean") { + return value ? TRUE : FALSE; + } + + // Handle numbers with "amount" in the property name + let displayValue = value; + if (typeof value === "number" && property.toLowerCase().includes("amount")) { + displayValue = `$${value}`; + } else if (typeof value === "number") { + displayValue = {value}; + } + + // Default formatting for other values + return {displayValue}; + }; + + const generateColumns = (keys: string[], prefix: string) => { + return keys.map((key) => ({ + title: key, + dataIndex: `${prefix.toLowerCase()}_${key}`, + key: `${prefix.toLowerCase()}_${key}`, + render: (value: any) => applyConditionalStyling(value, key), + })); + }; + const formatData = ( data: Record< string, @@ -28,6 +69,7 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro inputs: Record; outputs: Record; expectedResults: Record; + resultMatch: boolean; } > ): { formattedData: DataType[]; columns: TableProps["columns"] } => { @@ -48,30 +90,6 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro const inputKeys = sortKeys(Array.from(uniqueInputKeys)); const resultKeys = sortKeys(Array.from(uniqueResultKeys)); const expectedKeys = sortKeys(Array.from(uniqueExpectedKeys)); - console.log(expectedKeys, "expected keys"); - - const applyConditionalStyling = (value: any, property: string): React.ReactNode => { - // Handle null or undefined values - if (value === null || value === undefined) { - return null; - } - - // Handle booleans - if (typeof value === "boolean") { - return value ? TRUE : FALSE; - } - - // Handle numbers with "amount" in the property name - let displayValue = value; - if (typeof value === "number" && property.toLowerCase().includes("amount")) { - displayValue = `$${value}`; - } else if (typeof value === "number") { - displayValue = {value}; - } - - // Default formatting for other values - return {displayValue}; - }; // Format the data const formattedData: DataType[] = Object.entries(data).map(([name, entry], index) => { @@ -98,23 +116,23 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro entry.expectedResults[key] !== undefined ? applyConditionalStyling(entry.expectedResults[key], key) : null; }); + formattedEntry.resultMatch = applyConditionalStyling(entry.resultMatch, "resultMatch"); + return formattedEntry; }); - const generateColumns = (keys: string[], prefix: string) => { - return keys.map((key) => ({ - title: key, - dataIndex: `${prefix.toLowerCase()}_${key}`, - key: `${prefix.toLowerCase()}_${key}`, - render: (value: any) => applyConditionalStyling(value, key), - })); - }; - const inputColumns = generateColumns(inputKeys, "input"); const outputColumns = generateColumns(resultKeys, "result"); + //Unused columns for now, unless we'd like to display the expected results as columns on the frontend const expectedColumns = generateColumns(expectedKeys, "expected_result"); const columns: TableProps["columns"] = [ + { + title: "Status", + dataIndex: "resultMatch", + key: "resultMatch", + fixed: "left", + }, { title: "Name", dataIndex: "name", @@ -130,20 +148,49 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro title: "Results", children: outputColumns, }, - { - title: "Expected Results", - children: expectedColumns, - }, ]; return { formattedData, columns }; }; + const expandedRowRender = (record: { name: string }) => { + const expandedData = Object.entries(record || {}) + .map(([property, value], index) => ({ + key: index.toString(), + property, + value, + })) + .filter((entry) => entry.property.includes("expected_result")); + const expandedDataColumns = expandedData.map((entry) => ({ + title: entry.property.replace("expected_result_", ""), + dataIndex: entry.property, + key: entry.property, + render: (value: any) => { + // Apply conditional styling or formatting here + return applyConditionalStyling(value, entry.property); + }, + })); + + return ( + + +

Expected Results:

+

{record?.name}

+
+
+ + ); + }; + + const rowExpandable = (record: { resultMatch: { props: { className: string } } }) => { + const resultStatus = record.resultMatch.props.className === "result-mismatch" ? true : false; + return resultStatus; + }; + const updateScenarioResults = async (goRulesJSONFilename: string) => { try { const results = await runDecisionsForScenarios(goRulesJSONFilename); const formattedResults = formatData(results); - console.log(formattedResults, "these are formatted?"); setScenarioResults(formattedResults); } catch (error) { console.error("Error fetching scenario results:", error); @@ -217,6 +264,10 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro bordered dataSource={scenarioResults.formattedData} columns={scenarioResults.columns} + expandable={{ + expandedRowRender: (record: any) => expandedRowRender(record), + rowExpandable: (record: any) => rowExpandable(record), + }} /> From 41038eff824641e45a6deb74dd1b843385d727b3 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 15:56:38 -0700 Subject: [PATCH 43/66] Update csv testing form and styling. --- .../ScenarioTester/ScenarioTester.module.css | 71 ++++++++++++++++++ .../ScenarioTester/ScenarioTester.tsx | 74 +++++++++++-------- 2 files changed, 115 insertions(+), 30 deletions(-) diff --git a/app/components/ScenarioTester/ScenarioTester.module.css b/app/components/ScenarioTester/ScenarioTester.module.css index e69de29..09e85a4 100644 --- a/app/components/ScenarioTester/ScenarioTester.module.css +++ b/app/components/ScenarioTester/ScenarioTester.module.css @@ -0,0 +1,71 @@ +.instructionsList { + list-style: none; + padding: 20px; + margin: 20px 0; + background-color: #f4f4f9; + border-radius: 8px; + border: 1px solid #eaeaea; +} + +.instructionsList li { + margin-bottom: 15px; + padding: 10px; + background: #fff; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-size: 16px; + line-height: 1.5; + position: relative; +} + +.instructionsList li::before { + content: counter(li); + counter-increment: li; + position: absolute; + left: -30px; + top: 50%; + transform: translateY(-50%); + background: #007bff; + color: white; + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.instructionsList a { + color: #007bff; + text-decoration: none; +} + +.instructionsList a:hover { + text-decoration: underline; +} + +.instructionsList { + counter-reset: li; +} +.downloadLink { + font-size: 16px; + color: #007bff; + text-decoration: none; + padding-right: 10px; +} + +.upload { + margin-right: 10px; +} + +.runButton { + margin-left: 10px; +} + +.ant-upload-list-item { + margin-top: 10px; +} + +.ant-upload-list-item-name { + color: #333; +} diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index 1d4895f..de228ec 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -220,36 +220,50 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro
{uploader ? ( - Download Scenarios - { - setFile(file as File); - message.success(`${(file as File).name} file uploaded successfully.`); - onSuccess && onSuccess("ok"); - setUploadedFile(true); - }} - onRemove={() => { - setFile(null); - setUploadedFile(false); - }} - showUploadList={true} - > - - - +
    +
  1. + Download a template CSV file:{" "} + Download Scenarios/Template +
  2. +
  3. Add additional scenarios to the CSV file
  4. +
  5. + Upload your edited CSV file with scenarios:{" "} + { + setFile(file as File); + message.success(`${(file as File).name} file uploaded successfully.`); + onSuccess && onSuccess("ok"); + setUploadedFile(true); + }} + onRemove={() => { + setFile(null); + setUploadedFile(false); + }} + showUploadList={true} + className={styles.upload} + > + + +
  6. +
  7. + Run the scenarios against the GO Rules JSON file:{" "} + +
  8. +
  9. Receive a csv file with the results! 🎉
  10. +
) : ( <> From 3f1bcc07489fd29d182ba502702d733492b6a624 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 16:21:52 -0700 Subject: [PATCH 44/66] Update styling and formatting. --- .../ScenarioTester/ScenarioTester.module.css | 52 +++++++++++++++++++ .../ScenarioTester/ScenarioTester.tsx | 23 ++++---- .../SimulationViewer/SimulationViewer.tsx | 13 +++-- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/app/components/ScenarioTester/ScenarioTester.module.css b/app/components/ScenarioTester/ScenarioTester.module.css index 09e85a4..edb26d6 100644 --- a/app/components/ScenarioTester/ScenarioTester.module.css +++ b/app/components/ScenarioTester/ScenarioTester.module.css @@ -69,3 +69,55 @@ .ant-upload-list-item-name { color: #333; } + +.scenarioContainer { + padding: 20px; + background-color: #f9f9f9; + border-radius: 8px; + max-width: 100%; + overflow: auto; +} + +.scenarioTable { + max-width: 100%; + overflow: auto; +} + +.ant-table { + font-size: 14px; +} + +.ant-table th, .ant-table td { + padding: 8px; + white-space: nowrap; +} + +.ant-table-bordered .ant-table-container { + border: 1px solid #ddd; +} + +.ant-table-bordered .ant-table-header, .ant-table-bordered .ant-table-body { + border: 1px solid #ddd; +} + +.ant-table-thead > tr > th { + background-color: #fafafa; +} + +.ant-table-tbody > tr > td { + background-color: #fff; +} + +.ant-pagination { + margin: 16px 0; + text-align: right; +} + +.ant-btn { + font-size: 14px; +} + +.expectedResultsExpanded { + margin-top: 20px; + margin-bottom: 20px; +} \ No newline at end of file diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index de228ec..206e06d 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -166,19 +166,22 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro dataIndex: entry.property, key: entry.property, render: (value: any) => { - // Apply conditional styling or formatting here return applyConditionalStyling(value, entry.property); }, })); return ( - - -

Expected Results:

-

{record?.name}

+
+ +
`Expected results for scenario: ${record?.name}`} + columns={expandedDataColumns} + dataSource={[record]} + pagination={false} + bordered + /> -
- + ); }; @@ -266,7 +269,7 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro ) : ( - <> +
)} ); diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index fbd07b2..f793b6f 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -76,20 +76,19 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios const scenarioTab = ( <> - + - Export Scenario Results ); const scenarioGenerator = ( - + ); - const scenarioTests = ; + const scenarioTests = ( + + + + ); const csvScenarioTests = ( - + ); From b02b7bae290d73a645feb2482fcb37695fc2396b Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 16:38:23 -0700 Subject: [PATCH 45/66] Update embedded page with new viewer. --- app/rule/[ruleId]/embedded/page.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/rule/[ruleId]/embedded/page.tsx b/app/rule/[ruleId]/embedded/page.tsx index 3cfcea5..631d076 100644 --- a/app/rule/[ruleId]/embedded/page.tsx +++ b/app/rule/[ruleId]/embedded/page.tsx @@ -1,12 +1,26 @@ import SimulationViewer from "../../../components/SimulationViewer"; -import { getRuleDataById } from "../../../utils/api"; +import { getRuleDataById, getRuleMapByName, getScenariosByFilename } from "../../../utils/api"; +import { RuleMap } from "@/app/types/rulemap"; +import { Scenario } from "@/app/types/scenario"; -export default async function RuleEmbedded({ params: { ruleId } }: { params: { ruleId: string } }) { +export default async function Rule({ params: { ruleId } }: { params: { ruleId: string } }) { const { _id, goRulesJSONFilename, chefsFormId } = await getRuleDataById(ruleId); + const rulemap: RuleMap = await getRuleMapByName(goRulesJSONFilename); + const scenarios: Scenario[] = await getScenariosByFilename(goRulesJSONFilename); if (!_id) { return

Rule not found

; } - return ; + return ( + <> + + + ); } From ab9b81d076c3c8ba1c48dc5e06fcd084cbd880df Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 24 Jun 2024 10:56:34 -0700 Subject: [PATCH 46/66] Update to handle lack of input in variables or expected results. --- app/components/ScenarioGenerator/ScenarioGenerator.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.tsx b/app/components/ScenarioGenerator/ScenarioGenerator.tsx index 4c20c4c..16c49e7 100644 --- a/app/components/ScenarioGenerator/ScenarioGenerator.tsx +++ b/app/components/ScenarioGenerator/ScenarioGenerator.tsx @@ -36,11 +36,11 @@ export default function ScenarioGenerator({ if (!simulationRun || !selectedSubmissionInputs || !newScenarioName) return; const variables = Object.entries(selectedSubmissionInputs) - .filter(([name, value]) => name !== "rulemap") + .filter(([name, value]) => name !== "rulemap" && value !== null && value !== undefined) .map(([name, value]) => ({ name, value })); const expectedResults = Object.entries(scenarioExpectedOutput) - .filter(([name, value]) => name !== "rulemap") + .filter(([name, value]) => name !== "rulemap" && value !== null && value !== undefined) .map(([name, value]) => ({ name, value })); const newScenario: Scenario = { From 82f956b6ec10b301b03b5466fe39ad6d3ea8e867 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 24 Jun 2024 11:29:10 -0700 Subject: [PATCH 47/66] Remove unused styling. --- .../ScenarioTester/ScenarioTester.module.css | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/app/components/ScenarioTester/ScenarioTester.module.css b/app/components/ScenarioTester/ScenarioTester.module.css index edb26d6..1646bfb 100644 --- a/app/components/ScenarioTester/ScenarioTester.module.css +++ b/app/components/ScenarioTester/ScenarioTester.module.css @@ -62,14 +62,6 @@ margin-left: 10px; } -.ant-upload-list-item { - margin-top: 10px; -} - -.ant-upload-list-item-name { - color: #333; -} - .scenarioContainer { padding: 20px; background-color: #f9f9f9; @@ -83,40 +75,6 @@ overflow: auto; } -.ant-table { - font-size: 14px; -} - -.ant-table th, .ant-table td { - padding: 8px; - white-space: nowrap; -} - -.ant-table-bordered .ant-table-container { - border: 1px solid #ddd; -} - -.ant-table-bordered .ant-table-header, .ant-table-bordered .ant-table-body { - border: 1px solid #ddd; -} - -.ant-table-thead > tr > th { - background-color: #fafafa; -} - -.ant-table-tbody > tr > td { - background-color: #fff; -} - -.ant-pagination { - margin: 16px 0; - text-align: right; -} - -.ant-btn { - font-size: 14px; -} - .expectedResultsExpanded { margin-top: 20px; margin-bottom: 20px; From 3fb10f1f5a4ace04757da65db8f89577c388c6e2 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 24 Jun 2024 14:59:38 -0700 Subject: [PATCH 48/66] Add interception of json download to attach scenarios to rule file for testing. --- .../RulesDecisionGraph/RulesDecisionGraph.tsx | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx b/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx index a2d8966..bbcbf52 100644 --- a/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx +++ b/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx @@ -6,7 +6,8 @@ import { DecisionGraphType } from "@gorules/jdm-editor/dist/components/decision- import type { ReactFlowInstance } from "reactflow"; import { Spin } from "antd"; import { SubmissionData } from "../../types/submission"; -import { getDocument, postDecision, getRuleRunSchema } from "../../utils/api"; +import { Scenario } from "@/app/types/scenario"; +import { getDocument, postDecision, getRuleRunSchema, getScenariosByFilename } from "../../utils/api"; import styles from "./RulesDecisionGraph.module.css"; interface RulesViewerProps { @@ -24,7 +25,6 @@ export default function RulesDecisionGraph({ }: RulesViewerProps) { const decisionGraphRef: any = useRef(); const [graphJSON, setGraphJSON] = useState(); - useEffect(() => { const fetchData = async () => { @@ -70,6 +70,74 @@ export default function RulesDecisionGraph({ return { result: {} }; }; + const downloadJSON = (jsonData: any, filename: string) => { + const jsonString = JSON.stringify(jsonData, null, 2); + const blob = new Blob([jsonString], { type: "application/json" }); + const url = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + + // Clean up and remove the link + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + const handleScenarioInsertion = async () => { + try { + const jsonData = await getDocument(jsonFile); + const scenarios: Scenario[] = await getScenariosByFilename(jsonFile); + const scenarioObject = { + tests: scenarios.map((scenario) => ({ + name: scenario.title || "Default name", + input: scenario.variables.reduce((obj, each) => { + obj[each.name] = each.value; + return obj; + }, {}), + output: scenario.expectedResults.reduce((obj, each) => { + obj[each.name] = each.value; + return obj; + }, {}), + })), + }; + const updatedJSON = { + ...jsonData, + ...scenarioObject, + }; + return downloadJSON(updatedJSON, jsonFile); + } catch (error) { + console.error("Error fetching JSON:", error); + throw error; + } + }; + + const interceptJSONDownload = async (event: any) => { + if (decisionGraphRef.current && event.target?.download === "graph.json") { + event.preventDefault(); + try { + await handleScenarioInsertion(); + } catch (error) { + console.error("Error intercepting JSON download:", error); + } + } + }; + + useEffect(() => { + const clickHandler = (event: any) => { + interceptJSONDownload(event); + }; + + document.addEventListener("click", clickHandler); + + return () => { + document.removeEventListener("click", clickHandler); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + if (!graphJSON) { return ( From 7cfec42bf1eb5d50bc61845a199b85409e639dc0 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 24 Jun 2024 16:04:31 -0700 Subject: [PATCH 49/66] Change tab title. --- app/components/SimulationViewer/SimulationViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index f793b6f..6fbfa0e 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -130,7 +130,7 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios }, { key: "3", - label: "Export Scenario Results", + label: "Scenario Results", children: scenarioTests, }, { From cbb633cb836558886d4e69aa8fd7df4e747db46a Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 26 Jun 2024 10:44:51 -0700 Subject: [PATCH 50/66] Update error handling with message logging. --- .../RulesDecisionGraph/RulesDecisionGraph.tsx | 47 +++++++++++++------ .../ScenarioFormatter/ScenarioFormatter.tsx | 4 ++ .../ScenarioTester/ScenarioTester.tsx | 10 +++- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx b/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx index bbcbf52..9d902a3 100644 --- a/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx +++ b/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx @@ -4,7 +4,7 @@ import "@gorules/jdm-editor/dist/style.css"; import { JdmConfigProvider, DecisionGraph, DecisionGraphRef } from "@gorules/jdm-editor"; import { DecisionGraphType } from "@gorules/jdm-editor/dist/components/decision-graph/context/dg-store.context"; import type { ReactFlowInstance } from "reactflow"; -import { Spin } from "antd"; +import { Spin, message } from "antd"; import { SubmissionData } from "../../types/submission"; import { Scenario } from "@/app/types/scenario"; import { getDocument, postDecision, getRuleRunSchema, getScenariosByFilename } from "../../utils/api"; @@ -44,27 +44,44 @@ export default function RulesDecisionGraph({ }; useEffect(() => { - // Run the simulator when the context updates - decisionGraphRef?.current?.runSimulator(contextToSimulate); + try { + // Run the simulator when the context updates + decisionGraphRef?.current?.runSimulator(contextToSimulate); + } catch (e: any) { + message.error("An error occurred while running the simulator: " + e); + console.error("Error running the simulator:", e); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [contextToSimulate]); const simulateRun = async ({ context }: { context: unknown }) => { if (contextToSimulate) { console.info("Simulate:", context); - const data = await postDecision(jsonFile, context); - console.info("Simulation Results:", data, data?.result); - setResultsOfSimulation(data?.result); - const ruleRunSchema = await getRuleRunSchema(data); - // Filter out properties from ruleRunSchema outputs that are also present in data.result - const uniqueOutputs = Object.keys(ruleRunSchema?.result?.output || {}).reduce((acc: any, key: string) => { - if (!(key in data?.result)) { - acc[key] = ruleRunSchema?.result[key]; + try { + const data = await postDecision(jsonFile, context); + console.info("Simulation Results:", data, data?.result); + // Check if data.result is an array and throw error as object is required + if (Array.isArray(data?.result)) { + throw new Error("Please update your rule and ensure that outputs are on one line."); } - return acc; - }, {}); - setOutputsOfSimulation(uniqueOutputs); - return { result: data }; + setResultsOfSimulation(data?.result); + const ruleRunSchema = await getRuleRunSchema(data); + // Filter out properties from ruleRunSchema outputs that are also present in data.result + const uniqueOutputs = Object.keys(ruleRunSchema?.result?.output || {}).reduce((acc: any, key: string) => { + if (!(key in data?.result)) { + acc[key] = ruleRunSchema?.result[key]; + } + return acc; + }, {}); + + setOutputsOfSimulation(uniqueOutputs); + return { result: data }; + } catch (e: any) { + message.error("Error during simulation run: " + e); + console.error("Error during simulation run:", e); + return { result: {} }; + } } // Reset the result if there is no contextToSimulate (used to reset the trace) return { result: {} }; diff --git a/app/components/ScenarioFormatter/ScenarioFormatter.tsx b/app/components/ScenarioFormatter/ScenarioFormatter.tsx index 4ef56f7..9496f11 100644 --- a/app/components/ScenarioFormatter/ScenarioFormatter.tsx +++ b/app/components/ScenarioFormatter/ScenarioFormatter.tsx @@ -166,6 +166,10 @@ export default function ScenarioFormatter({ title, rawData, setRawData, scenario value: convertAndStyleValue(value, property, editable), key: index, })); + // Check if data.result is an array + if (Array.isArray(rawData)) { + throw new Error("Please update your rule and ensure that outputs are on one line."); + } setDataSource(newData); const newColumns = COLUMNS.filter((column) => showColumn(newData, column.dataIndex)); setColumns(newColumns); diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index 206e06d..28283b6 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -193,10 +193,18 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro const updateScenarioResults = async (goRulesJSONFilename: string) => { try { const results = await runDecisionsForScenarios(goRulesJSONFilename); + // Loop through object and check if data.result is an array + for (const key in results) { + if (Array.isArray(results[key].result)) { + throw new Error( + `Error with results for: ${key}. Please update your rule and ensure that outputs are on one line.` + ); + } + } const formattedResults = formatData(results); setScenarioResults(formattedResults); } catch (error) { - console.error("Error fetching scenario results:", error); + message.error("Error fetching scenario results: " + error); } }; From 2b5dbc61853066ceb90210ffb3af8df12e2de138 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 26 Jun 2024 15:32:17 -0700 Subject: [PATCH 51/66] Update error handling to limit duplication. --- app/components/ScenarioTester/ScenarioTester.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/components/ScenarioTester/ScenarioTester.tsx b/app/components/ScenarioTester/ScenarioTester.tsx index 28283b6..ae9e140 100644 --- a/app/components/ScenarioTester/ScenarioTester.tsx +++ b/app/components/ScenarioTester/ScenarioTester.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { Table, Tag, Button, TableProps, Flex, Upload, message } from "antd"; import { CheckCircleOutlined, CloseCircleOutlined } from "@ant-design/icons"; import { UploadOutlined } from "@ant-design/icons"; @@ -14,6 +14,7 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro const [scenarioResults, setScenarioResults] = useState({}); const [file, setFile] = useState(null); const [uploadedFile, setUploadedFile] = useState(false); + const hasError = useRef(false); type DataType = { key: string; @@ -204,11 +205,15 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro const formattedResults = formatData(results); setScenarioResults(formattedResults); } catch (error) { - message.error("Error fetching scenario results: " + error); + if (!hasError.current) { + hasError.current = true; + message.error("Error fetching scenario results: " + error); + } } }; useEffect(() => { + hasError.current = false; updateScenarioResults(jsonFile); // eslint-disable-next-line react-hooks/exhaustive-deps }, [jsonFile]); From ca1317090ddbadd44999aeb1bccbe5a9ca81b7b5 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 27 Jun 2024 15:05:15 -0700 Subject: [PATCH 52/66] Update with naming convention to match column names for user input. --- app/components/InputOutputTable/InputOutputTable.tsx | 6 +++++- app/components/ScenarioFormatter/ScenarioFormatter.tsx | 7 +++++-- app/components/ScenarioGenerator/ScenarioGenerator.tsx | 7 ++++++- app/components/ScenarioViewer/ScenarioViewer.tsx | 6 +++++- app/components/SimulationViewer/SimulationViewer.tsx | 2 ++ 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/components/InputOutputTable/InputOutputTable.tsx b/app/components/InputOutputTable/InputOutputTable.tsx index 30ab64d..4ffc63d 100644 --- a/app/components/InputOutputTable/InputOutputTable.tsx +++ b/app/components/InputOutputTable/InputOutputTable.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, FocusEvent } from "react"; import { Table, Tag, Input, Button } from "antd"; +import { RuleMap } from "@/app/types/rulemap"; import styles from "./InputOutputTable.module.css"; const COLUMNS = [ @@ -27,6 +28,7 @@ interface InputOutputTableProps { setRawData?: (data: rawDataProps) => void; submitButtonRef?: React.RefObject; editable?: boolean; + rulemap?: RuleMap; } export default function InputOutputTable({ @@ -35,6 +37,7 @@ export default function InputOutputTable({ setRawData, submitButtonRef, editable = false, + rulemap, }: InputOutputTableProps) { const [dataSource, setDataSource] = useState([]); const [columns, setColumns] = useState(COLUMNS); @@ -98,11 +101,12 @@ export default function InputOutputTable({ useEffect(() => { if (rawData) { + const propertyRuleMap = Object.values(rulemap || {}).flat(); const newData = Object.entries(rawData) .filter(([property]) => !PROPERTIES_TO_IGNORE.includes(property)) .sort(([propertyA], [propertyB]) => propertyA.localeCompare(propertyB)) .map(([property, value], index) => ({ - property, + property: propertyRuleMap?.find((item) => item.property === property)?.name || property, value: convertAndStyleValue(value, property, editable), key: index, })); diff --git a/app/components/ScenarioFormatter/ScenarioFormatter.tsx b/app/components/ScenarioFormatter/ScenarioFormatter.tsx index 9496f11..f4a522d 100644 --- a/app/components/ScenarioFormatter/ScenarioFormatter.tsx +++ b/app/components/ScenarioFormatter/ScenarioFormatter.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { Table, Tag, Input, Button, Radio, AutoComplete, InputNumber } from "antd"; import { Scenario } from "@/app/types/scenario"; import styles from "./ScenarioFormatter.module.css"; +import { RuleMap } from "@/app/types/rulemap"; const COLUMNS = [ { @@ -27,9 +28,10 @@ interface ScenarioFormatterProps { rawData: rawDataProps | null | undefined; setRawData?: (data: object) => void; scenarios?: Scenario[]; + rulemap: RuleMap; } -export default function ScenarioFormatter({ title, rawData, setRawData, scenarios }: ScenarioFormatterProps) { +export default function ScenarioFormatter({ title, rawData, setRawData, scenarios, rulemap }: ScenarioFormatterProps) { const [dataSource, setDataSource] = useState([]); const [columns, setColumns] = useState(COLUMNS); const [showTable, setShowTable] = useState(true); @@ -158,11 +160,12 @@ export default function ScenarioFormatter({ title, rawData, setRawData, scenario useEffect(() => { if (rawData) { const editable = title === "Inputs" && rawData.rulemap === true; + const propertyRuleMap = Object.values(rulemap || {}).flat(); const newData = Object.entries(rawData) .filter(([property]) => !PROPERTIES_TO_IGNORE.includes(property)) .sort(([propertyA], [propertyB]) => propertyA.localeCompare(propertyB)) .map(([property, value], index) => ({ - property, + property: propertyRuleMap?.find((item) => item.property === property)?.name || property, value: convertAndStyleValue(value, property, editable), key: index, })); diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.tsx b/app/components/ScenarioGenerator/ScenarioGenerator.tsx index 16c49e7..e5f5308 100644 --- a/app/components/ScenarioGenerator/ScenarioGenerator.tsx +++ b/app/components/ScenarioGenerator/ScenarioGenerator.tsx @@ -6,6 +6,7 @@ import { Scenario } from "@/app/types/scenario"; import { SubmissionData } from "@/app/types/submission"; import { createScenario } from "@/app/utils/api"; import ScenarioFormatter from "../ScenarioFormatter"; +import { RuleMap } from "@/app/types/rulemap"; interface ScenarioGeneratorProps { scenarios: Scenario[]; @@ -16,6 +17,7 @@ interface ScenarioGeneratorProps { resetTrigger: boolean; ruleId: string; jsonFile: string; + rulemap: RuleMap; } export default function ScenarioGenerator({ @@ -27,6 +29,7 @@ export default function ScenarioGenerator({ resetTrigger, ruleId, jsonFile, + rulemap, }: ScenarioGeneratorProps) { const [simulationRun, setSimulationRun] = useState(false); const [newScenarioName, setNewScenarioName] = useState(""); @@ -92,6 +95,7 @@ export default function ScenarioGenerator({ setSelectedSubmissionInputs(data); }} scenarios={scenarios} + rulemap={rulemap} /> - +
  • Run the scenarios against the GO Rules JSON file:{" "} @@ -297,6 +300,7 @@ export default function ScenarioTester({ jsonFile, uploader }: ScenarioTesterPro expandable={{ expandedRowRender: (record: any) => expandedRowRender(record), rowExpandable: (record: any) => rowExpandable(record), + columnTitle: "View Expected Results", }} className={styles.scenarioTable} size="middle" diff --git a/app/styles/globals.css b/app/styles/globals.css index 9ce74ab..560220a 100644 --- a/app/styles/globals.css +++ b/app/styles/globals.css @@ -2,3 +2,23 @@ .grl-dg { min-height: 500px !important; } + +.labelsmall { + font-size: 12px; + color: #666; + display: flex; + flex-direction: column; +} + +.labelsmall input { + margin-top: 4px; +} + +.labelsmall:focus-within { + color: #333; +} + +.labelsmall input:focus { + outline: 2px solid #007BFF; + outline-offset: 2px; +} From a7bafe931c93e271704043371e2a98676b392303 Mon Sep 17 00:00:00 2001 From: timwekkenbc Date: Fri, 28 Jun 2024 11:09:26 -0700 Subject: [PATCH 54/66] Added new RuleHeader component that allows a rule name to be changed, removed unneeded functionality of admin panel --- app/admin/page.tsx | 77 ++++--------------- .../RuleHeader/RuleHeader.module.css | 13 ++++ app/components/RuleHeader/RuleHeader.test.js | 37 +++++++++ app/components/RuleHeader/RuleHeader.tsx | 70 +++++++++++++++++ app/components/RuleHeader/index.ts | 1 + app/rule/[ruleId]/page.tsx | 14 +--- app/types/ruleInfo.d.ts | 2 +- 7 files changed, 142 insertions(+), 72 deletions(-) create mode 100644 app/components/RuleHeader/RuleHeader.module.css create mode 100644 app/components/RuleHeader/RuleHeader.test.js create mode 100644 app/components/RuleHeader/RuleHeader.tsx create mode 100644 app/components/RuleHeader/index.ts diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 8dbaf41..51634ce 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -22,15 +22,7 @@ export default function Admin() { // Get rules that are already defined in the DB const existingRules = await getAllRuleData(); setInitialRules(existingRules); - // Get rules that exist in the rules repository, but aren't yet defined in the DB - const existingRuleDocuments = await getAllRuleDocuments(); - const undefinedRules = existingRuleDocuments - .filter((ruleJSON: string) => { - return !existingRules.find((rule: RuleInfo) => rule.goRulesJSONFilename === ruleJSON); - }) - .map((ruleJSON: string) => ({ goRulesJSONFilename: ruleJSON })); - const ruleData = [...existingRules, ...undefinedRules]; - setRules(JSON.parse(JSON.stringify(ruleData))); // JSON.parse(JSON.stringify(data)) is a hacky way to deep copy the data - needed for comparison later + setRules(JSON.parse(JSON.stringify([...existingRules]))); // JSON.parse(JSON.stringify(data)) is a hacky way to deep copy the data - needed for comparison later setIsLoading(false); }; @@ -38,18 +30,6 @@ export default function Admin() { getOrRefreshRuleList(); }, []); - const addNewRule = async () => { - const newRules = [...rules]; - newRules.push({ - _id: "", - title: "", - goRulesJSONFilename: "", - chefsFormId: "", - chefsFormAPIKey: "", - }); - setRules(newRules); - }; - const updateRule = (e: React.ChangeEvent, index: number, property: keyof RuleInfo) => { const newRules = [...rules]; newRules[index][property] = e.target.value; @@ -72,9 +52,7 @@ export default function Admin() { } else if ( initialRule._id !== rule._id || initialRule.title !== rule.title || - initialRule.goRulesJSONFilename !== rule.goRulesJSONFilename || - initialRule.chefsFormId !== rule.chefsFormId || - initialRule.chefsFormAPIKey !== rule.chefsFormAPIKey + initialRule.goRulesJSONFilename !== rule.goRulesJSONFilename ) { return { rule, action: ACTION_STATUS.UPDATE }; } @@ -92,19 +70,19 @@ export default function Admin() { const entriesToUpdate = getRulesToUpdate(); await Promise.all( entriesToUpdate.map(async ({ rule, action }) => { - try { - if (action === ACTION_STATUS.NEW) { - await postRuleData(rule); - } else if (rule?._id) { - if (action === ACTION_STATUS.UPDATE) { - await updateRuleData(rule._id, rule); - } else if (action === ACTION_STATUS.DELETE) { - await deleteRuleData(rule._id); - } + try { + if (action === ACTION_STATUS.NEW) { + await postRuleData(rule); + } else if (rule?._id) { + if (action === ACTION_STATUS.UPDATE) { + await updateRuleData(rule._id, rule); + } else if (action === ACTION_STATUS.DELETE) { + await deleteRuleData(rule._id); } - } catch (error) { - console.error(`Error performing action ${action} on rule ${rule._id}: ${error}`); } + } catch (error) { + console.error(`Error performing action ${action} on rule ${rule._id}: ${error}`); + } }) ); getOrRefreshRuleList(); @@ -123,28 +101,13 @@ export default function Admin() { title: "Title", dataIndex: "title", render: renderInputField("title"), - width: "220px" - }, - { - title: "GoRules Id", - dataIndex: "_id", - render: renderInputField("_id"), + width: "220px", }, { title: "GoRules JSON Filename", dataIndex: "goRulesJSONFilename", render: renderInputField("goRulesJSONFilename"), - width: "260px" - }, - { - title: "CHEFS Form Id", - dataIndex: "chefsFormId", - render: renderInputField("chefsFormId"), - }, - { - title: "CHEFS Form API Key", - dataIndex: "chefsFormAPIKey", - render: renderInputField("chefsFormAPIKey"), + width: "260px", }, { dataIndex: "delete", @@ -180,15 +143,7 @@ export default function Admin() { {isLoading ? (

    Loading...

    ) : ( -
  • ({ key, ...rule }))} - footer={() => ( - - )} - /> +
    ({ key, ...rule }))} /> )} ); diff --git a/app/components/RuleHeader/RuleHeader.module.css b/app/components/RuleHeader/RuleHeader.module.css new file mode 100644 index 0000000..9d5367f --- /dev/null +++ b/app/components/RuleHeader/RuleHeader.module.css @@ -0,0 +1,13 @@ +.titleInput { + font-size: inherit; + width: 100%; + padding: 0; +} + +.editButton { + background: none; + border: none; + cursor: pointer; + padding: 0; + font-size: 18px; +} \ No newline at end of file diff --git a/app/components/RuleHeader/RuleHeader.test.js b/app/components/RuleHeader/RuleHeader.test.js new file mode 100644 index 0000000..970b6b5 --- /dev/null +++ b/app/components/RuleHeader/RuleHeader.test.js @@ -0,0 +1,37 @@ +import { render, fireEvent, waitFor } from "@testing-library/react"; +import { toBeInTheDocument } from "@testing-library/jest-dom"; +import RuleHeader from "./RuleHeader"; +import api from "@/app/utils/api"; + +jest.mock("../../utils/api", () => ({ + updateRuleData: jest.fn(), +})); + +describe("RuleHeader - doneEditingTitle", () => { + const ruleInfoMock = { _id: "1", title: "Original Title", goRulesJSONFilename: "filename.json" }; + + it("updates title on success", async () => { + api.updateRuleData.mockResolvedValue({}); // Mock success + const { getByLabelText, getByText } = render(); + fireEvent.click(getByText("Original Title")); // Start editing + fireEvent.change(getByLabelText("Edit title"), { target: { value: "New Title" } }); + fireEvent.blur(getByLabelText("Edit title")); // Done editing + await waitFor(() => expect(getByText("New Title")).toBeInTheDocument()); + }); + + it("reverts title on update failure", async () => { + api.updateRuleData.mockRejectedValue(new Error("Failed to update")); // Mock failure + const { getByLabelText, getByText } = render(); + fireEvent.click(getByText("Original Title")); // Start editing + fireEvent.change(getByLabelText("Edit title"), { target: { value: "Failed Title" } }); + fireEvent.blur(getByLabelText("Edit title")); // Done editing + await waitFor(() => expect(getByText("Original Title")).toBeInTheDocument()); + }); + + it("does nothing if title is unchanged", async () => { + const { getByLabelText, getByText } = render(); + fireEvent.click(getByText("Original Title")); // Start editing + fireEvent.blur(getByLabelText("Edit title")); // Done editing without change + await waitFor(() => expect(getByText("Original Title")).toBeInTheDocument()); + }); +}); diff --git a/app/components/RuleHeader/RuleHeader.tsx b/app/components/RuleHeader/RuleHeader.tsx new file mode 100644 index 0000000..c174e51 --- /dev/null +++ b/app/components/RuleHeader/RuleHeader.tsx @@ -0,0 +1,70 @@ +"use client"; +import { useState, useRef, useEffect } from "react"; +import Link from "next/link"; +import { Flex } from "antd"; +import { HomeOutlined, EditOutlined, CheckOutlined } from "@ant-design/icons"; +import { RuleInfo } from "@/app/types/ruleInfo"; +import { updateRuleData } from "@/app/utils/api"; +import styles from "./RuleHeader.module.css"; + +export default function RuleHeader({ ruleInfo }: { ruleInfo: RuleInfo }) { + const { title, goRulesJSONFilename } = ruleInfo; + + const [savedTitle, setSavedTitle] = useState(title || goRulesJSONFilename); + const [isEditingTitle, setIsEditingTitle] = useState(false); + const [currTitle, setCurrTitle] = useState(savedTitle); + const inputRef = useRef(null); + + useEffect(() => { + if (isEditingTitle) { + inputRef.current?.focus(); + } + }, [isEditingTitle]); + + const startEditingTitle = () => { + setIsEditingTitle(true); + }; + + const updateTitle = (e: React.ChangeEvent) => { + if (e.target.value !== currTitle) { + setCurrTitle(e.target.value); + } + }; + + const doneEditingTitle = async () => { + setIsEditingTitle(false); + const updatedRuleInfo = { ...ruleInfo, title: currTitle }; + try { + await updateRuleData(ruleInfo._id, updatedRuleInfo); + setSavedTitle(currTitle); + } catch (e) { + // If updating fails, revert to previous title name + setCurrTitle(title || goRulesJSONFilename); + } + }; + + return ( + + + + +

    + {isEditingTitle ? ( + + ) : ( + currTitle + )} +

    + +
    + ); +} diff --git a/app/components/RuleHeader/index.ts b/app/components/RuleHeader/index.ts new file mode 100644 index 0000000..6879f98 --- /dev/null +++ b/app/components/RuleHeader/index.ts @@ -0,0 +1 @@ +export { default } from "./RuleHeader"; diff --git a/app/rule/[ruleId]/page.tsx b/app/rule/[ruleId]/page.tsx index 74b1d6b..123a6b2 100644 --- a/app/rule/[ruleId]/page.tsx +++ b/app/rule/[ruleId]/page.tsx @@ -1,11 +1,10 @@ -import Link from "next/link"; -import { Flex } from "antd"; -import { HomeOutlined } from "@ant-design/icons"; +import RuleHeader from "@/app/components/RuleHeader"; import SimulationViewer from "../../components/SimulationViewer"; import { getRuleDataById } from "../../utils/api"; export default async function Rule({ params: { ruleId } }: { params: { ruleId: string } }) { - const { title, _id, goRulesJSONFilename, chefsFormId } = await getRuleDataById(ruleId); + const ruleInfo = await getRuleDataById(ruleId); + const { _id, goRulesJSONFilename, chefsFormId } = ruleInfo; if (!_id) { return

    Rule not found

    ; @@ -13,12 +12,7 @@ export default async function Rule({ params: { ruleId } }: { params: { ruleId: s return ( <> - - - - -

    {title || goRulesJSONFilename}

    -
    + ); diff --git a/app/types/ruleInfo.d.ts b/app/types/ruleInfo.d.ts index 7c5daf3..a062297 100644 --- a/app/types/ruleInfo.d.ts +++ b/app/types/ruleInfo.d.ts @@ -2,6 +2,6 @@ export interface RuleInfo { _id: string; title: string; goRulesJSONFilename: string; - chefsFormId: string; + chefsFormId?: string; chefsFormAPIKey?: string; } From 3f0a77239813e3b79933d657ca4796380812adf0 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Tue, 2 Jul 2024 12:28:45 -0700 Subject: [PATCH 55/66] Remove extra semicolon. --- app/components/ScenarioFormatter/ScenarioFormatter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/ScenarioFormatter/ScenarioFormatter.tsx b/app/components/ScenarioFormatter/ScenarioFormatter.tsx index 1ab1327..1299490 100644 --- a/app/components/ScenarioFormatter/ScenarioFormatter.tsx +++ b/app/components/ScenarioFormatter/ScenarioFormatter.tsx @@ -111,7 +111,7 @@ export default function ScenarioFormatter({ title, rawData, setRawData, scenario return ( <> From d6ec7a187b8bf5089835b1d50363ee4bb1da2a3a Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Tue, 2 Jul 2024 13:58:45 -0700 Subject: [PATCH 56/66] Remove chefs integration. --- app/components/SimulationViewer/SimulationViewer.tsx | 1 - app/page.tsx | 7 +------ app/rule/[ruleId]/embedded/page.tsx | 10 ++-------- app/rule/[ruleId]/page.tsx | 3 +-- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index ec78fae..63e07aa 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -17,7 +17,6 @@ const RulesDecisionGraph = dynamic(() => import("../RulesDecisionGraph"), { ssr: interface SimulationViewerProps { ruleId: string; jsonFile: string; - chefsFormId: string; rulemap: RuleMap; scenarios: Scenario[]; } diff --git a/app/page.tsx b/app/page.tsx index 2b99a5a..7441e7c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -22,7 +22,7 @@ export default function Home() { getRules(); }, []); - const mappedRules = rules.map(({ _id, title, goRulesJSONFilename, chefsFormId }) => { + const mappedRules = rules.map(({ _id, title, goRulesJSONFilename }) => { return { key: _id, titleLink: ( @@ -33,7 +33,6 @@ export default function Home() { downloadRule: ( Download JSON ), - submissionFormLink: Submission, }; }); @@ -46,10 +45,6 @@ export default function Home() { title: "Download Rule", dataIndex: "downloadRule", }, - { - title: "Submission Form", - dataIndex: "submissionFormLink", - }, ]; return ( diff --git a/app/rule/[ruleId]/embedded/page.tsx b/app/rule/[ruleId]/embedded/page.tsx index 631d076..957ba72 100644 --- a/app/rule/[ruleId]/embedded/page.tsx +++ b/app/rule/[ruleId]/embedded/page.tsx @@ -4,7 +4,7 @@ import { RuleMap } from "@/app/types/rulemap"; import { Scenario } from "@/app/types/scenario"; export default async function Rule({ params: { ruleId } }: { params: { ruleId: string } }) { - const { _id, goRulesJSONFilename, chefsFormId } = await getRuleDataById(ruleId); + const { _id, goRulesJSONFilename } = await getRuleDataById(ruleId); const rulemap: RuleMap = await getRuleMapByName(goRulesJSONFilename); const scenarios: Scenario[] = await getScenariosByFilename(goRulesJSONFilename); @@ -14,13 +14,7 @@ export default async function Rule({ params: { ruleId } }: { params: { ruleId: s return ( <> - + ); } diff --git a/app/rule/[ruleId]/page.tsx b/app/rule/[ruleId]/page.tsx index f0a4472..ced8a81 100644 --- a/app/rule/[ruleId]/page.tsx +++ b/app/rule/[ruleId]/page.tsx @@ -6,7 +6,7 @@ import { Scenario } from "@/app/types/scenario"; export default async function Rule({ params: { ruleId } }: { params: { ruleId: string } }) { const ruleInfo = await getRuleDataById(ruleId); - const { _id, goRulesJSONFilename, chefsFormId } = ruleInfo; + const { _id, goRulesJSONFilename } = ruleInfo; const rulemap: RuleMap = await getRuleMapByName(goRulesJSONFilename); const scenarios: Scenario[] = await getScenariosByFilename(goRulesJSONFilename); @@ -21,7 +21,6 @@ export default async function Rule({ params: { ruleId } }: { params: { ruleId: s ruleId={ruleId} rulemap={rulemap} jsonFile={goRulesJSONFilename} - chefsFormId={chefsFormId} scenarios={scenarios} /> From caf5b7b7ad7d8fd300fe15cd47bb91c3f25a7084 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 12:28:23 -0700 Subject: [PATCH 57/66] Remove unused styles and update typing on variables object. --- app/components/RulesDecisionGraph/RulesDecisionGraph.tsx | 6 +++--- .../ScenarioGenerator/ScenarioGenerator.module.css | 0 app/components/ScenarioGenerator/ScenarioGenerator.tsx | 5 ++--- app/types/scenario.d.ts | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 app/components/ScenarioGenerator/ScenarioGenerator.module.css diff --git a/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx b/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx index 9d902a3..f05c136 100644 --- a/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx +++ b/app/components/RulesDecisionGraph/RulesDecisionGraph.tsx @@ -6,7 +6,7 @@ import { DecisionGraphType } from "@gorules/jdm-editor/dist/components/decision- import type { ReactFlowInstance } from "reactflow"; import { Spin, message } from "antd"; import { SubmissionData } from "../../types/submission"; -import { Scenario } from "@/app/types/scenario"; +import { Scenario, Variable } from "@/app/types/scenario"; import { getDocument, postDecision, getRuleRunSchema, getScenariosByFilename } from "../../utils/api"; import styles from "./RulesDecisionGraph.module.css"; @@ -108,9 +108,9 @@ export default function RulesDecisionGraph({ const jsonData = await getDocument(jsonFile); const scenarios: Scenario[] = await getScenariosByFilename(jsonFile); const scenarioObject = { - tests: scenarios.map((scenario) => ({ + tests: scenarios.map((scenario: Scenario) => ({ name: scenario.title || "Default name", - input: scenario.variables.reduce((obj, each) => { + input: scenario.variables.reduce((obj: any, each: Variable) => { obj[each.name] = each.value; return obj; }, {}), diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.module.css b/app/components/ScenarioGenerator/ScenarioGenerator.module.css deleted file mode 100644 index e69de29..0000000 diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.tsx b/app/components/ScenarioGenerator/ScenarioGenerator.tsx index e5f5308..b632d99 100644 --- a/app/components/ScenarioGenerator/ScenarioGenerator.tsx +++ b/app/components/ScenarioGenerator/ScenarioGenerator.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from "react"; import { Flex, Button, Input } from "antd"; import InputOutputTable from "../InputOutputTable"; -import styles from "./ScenarioGenerator.module.css"; import { Scenario } from "@/app/types/scenario"; import { SubmissionData } from "@/app/types/submission"; import { createScenario } from "@/app/utils/api"; @@ -84,10 +83,10 @@ export default function ScenarioGenerator({ }, []); return ( - + {selectedSubmissionInputs && ( - + Date: Thu, 4 Jul 2024 13:23:56 -0700 Subject: [PATCH 58/66] Update naming convention of rulemap and typing. --- app/components/SimulationViewer/SimulationViewer.tsx | 4 ++-- app/types/rulemap.d.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 63e07aa..f0dbc9b 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -31,7 +31,7 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios const ruleMapInputs = createRuleMap(rulemap.inputs, { rulemap: true }); const ruleMapOutputs = createRuleMap(rulemap.outputs, { rulemap: true }); - const ruleMapFinalOutputs = createRuleMap(rulemap.finalOutputs, { rulemap: true }); + const ruleMapResultOutputs = createRuleMap(rulemap.resultOutputs, { rulemap: true }); const [selectedSubmissionInputs, setSelectedSubmissionInputs] = useState(ruleMapInputs); const [contextToSimulate, setContextToSimulate] = useState(); @@ -43,7 +43,7 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios const resetContextAndResults = () => { setContextToSimulate(null); setOutputSchema(ruleMapOutputs); - setResultsOfSimulation(ruleMapFinalOutputs); + setResultsOfSimulation(ruleMapResultOutputs); }; const runSimulation = () => { diff --git a/app/types/rulemap.d.ts b/app/types/rulemap.d.ts index 8c525ea..f853ead 100644 --- a/app/types/rulemap.d.ts +++ b/app/types/rulemap.d.ts @@ -1,5 +1,6 @@ +import { Variable } from "./scenario"; export interface RuleMap { - inputs: Array; - outputs: Array; - finalOutputs: Array; + inputs: Variable[]; + outputs: Variable[]; + resultOutputs: Variable[]; } From a4d7db9a807e05c3ff43f8cdcb4136434a3180d6 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 13:28:23 -0700 Subject: [PATCH 59/66] Remove unused empty component. --- app/rule/[ruleId]/embedded/page.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/rule/[ruleId]/embedded/page.tsx b/app/rule/[ruleId]/embedded/page.tsx index 957ba72..aad2318 100644 --- a/app/rule/[ruleId]/embedded/page.tsx +++ b/app/rule/[ruleId]/embedded/page.tsx @@ -12,9 +12,5 @@ export default async function Rule({ params: { ruleId } }: { params: { ruleId: s return

    Rule not found

    ; } - return ( - <> - - - ); + return ; } From be0d0d4d4278411ab0b74ac69741c2923131abcc Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 13:46:17 -0700 Subject: [PATCH 60/66] Update naming conventions of tabs and remove unused empty components. --- .../SimulationViewer/SimulationViewer.tsx | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index f0dbc9b..29ba656 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect } from "react"; import dynamic from "next/dynamic"; import { Flex, Button, Tabs } from "antd"; import type { TabsProps } from "antd"; @@ -38,7 +38,6 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios const [outputSchema, setOutputSchema] = useState | null>(ruleMapOutputs); const [resultsOfSimulation, setResultsOfSimulation] = useState | null>(); const [resetTrigger, setResetTrigger] = useState(false); - const simulateButtonRef = useRef(null); const resetContextAndResults = () => { setContextToSimulate(null); @@ -74,20 +73,18 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios }; const scenarioTab = ( - <> - - - - + + + ); - const scenarioGenerator = ( + const scenarioGeneratorTab = ( ); - const scenarioTests = ( + const scenarioTestsTab = ( ); - const csvScenarioTests = ( + const csvScenarioTestsTab = ( @@ -127,17 +124,17 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios { key: "2", label: "Simulate inputs manually and create new scenarios", - children: scenarioGenerator, + children: scenarioGeneratorTab, }, { key: "3", label: "Scenario Results", - children: scenarioTests, + children: scenarioTestsTab, }, { key: "4", label: "CSV Tests", - children: csvScenarioTests, + children: csvScenarioTestsTab, }, ]; From 312bce4df20844a6f5b25d1663249429343dd7b3 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 14:12:06 -0700 Subject: [PATCH 61/66] Refactor inputs on ScenarioFormatter into subcomponent. --- app/components/InputStyler/InputStyler.tsx | 138 +++++++++++++++++ app/components/InputStyler/index.ts | 0 .../ScenarioFormatter/ScenarioFormatter.tsx | 144 +----------------- 3 files changed, 142 insertions(+), 140 deletions(-) create mode 100644 app/components/InputStyler/InputStyler.tsx create mode 100644 app/components/InputStyler/index.ts diff --git a/app/components/InputStyler/InputStyler.tsx b/app/components/InputStyler/InputStyler.tsx new file mode 100644 index 0000000..efd843e --- /dev/null +++ b/app/components/InputStyler/InputStyler.tsx @@ -0,0 +1,138 @@ +import { Tag, Input, Radio, AutoComplete, InputNumber, Flex } from "antd"; +import { Scenario } from "@/app/types/scenario"; + +export default function inputStyler( + value: any, + property: string, + editable: boolean, + scenarios: Scenario[] = [], + rawData: object = {}, + setRawData: any +) { + const getAutoCompleteOptions = (property: string) => { + if (!scenarios) return []; + const optionsSet = new Set(); + + scenarios.forEach((scenario) => { + scenario.variables + .filter((variable) => variable.name === property) + .forEach((variable) => optionsSet.add(variable.value)); + }); + + return Array.from(optionsSet).map((value) => ({ value, type: typeof value })); + }; + + const handleValueChange = (value: any, property: string) => { + let queryValue: any = value; + if (typeof value === "string") { + if (value.toLowerCase() === "true") { + queryValue = true; + } else if (value.toLowerCase() === "false") { + queryValue = false; + } else if (!isNaN(Number(value))) { + queryValue = Number(value); + } + } + + const updatedData = { ...rawData, [property]: queryValue } || {}; + + if (typeof setRawData === "function") { + setRawData(updatedData); + } else { + console.error("setRawData is not a function or is undefined"); + } + }; + + const handleInputChange = (val: any, property: string) => { + const updatedData = { ...rawData, [property]: val }; + if (typeof setRawData === "function") { + setRawData(updatedData); + } + }; + + const valuesArray = getAutoCompleteOptions(property); + let type = typeof value; + if (valuesArray.length > 0) { + type = typeof valuesArray[0].value; + } + + if (editable) { + if (type === "boolean" || typeof value === "boolean") { + return ( + + + + ); + } + + if (type === "string" || typeof value === "string") { + return ( + + ); + } + + if (type === "number" || typeof value === "number") { + return ( + + ); + } + + if (value === null || value === undefined) { + return ( + + ); + } + } else { + if (type === "boolean" || typeof value === "boolean") { + return ( + null} value={value}> + Yes + No + + ); + } + + if (type === "string" || typeof value === "string") { + return {value}; + } + + if (type === "number" || typeof value === "number") { + if (property.toLowerCase().includes("amount")) { + return ${value}; + } else { + return {value}; + } + } + + if (value === null || value === undefined) { + return null; + } + } + + return {value}; +} diff --git a/app/components/InputStyler/index.ts b/app/components/InputStyler/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/components/ScenarioFormatter/ScenarioFormatter.tsx b/app/components/ScenarioFormatter/ScenarioFormatter.tsx index 1299490..63d5b30 100644 --- a/app/components/ScenarioFormatter/ScenarioFormatter.tsx +++ b/app/components/ScenarioFormatter/ScenarioFormatter.tsx @@ -1,8 +1,9 @@ import { useState, useEffect } from "react"; -import { Table, Tag, Input, Button, Radio, AutoComplete, InputNumber, Flex } from "antd"; +import { Table, Button } from "antd"; import { Scenario } from "@/app/types/scenario"; import styles from "./ScenarioFormatter.module.css"; import { RuleMap } from "@/app/types/rulemap"; +import inputStyler from "../InputStyler/InputStyler"; const COLUMNS = [ { @@ -40,141 +41,6 @@ export default function ScenarioFormatter({ title, rawData, setRawData, scenario setShowTable(!showTable); }; - const getAutoCompleteOptions = (property: string) => { - if (!scenarios) return []; - const optionsSet = new Set(); - - scenarios.forEach((scenario) => { - scenario.variables - .filter((variable) => variable.name === property) - .forEach((variable) => optionsSet.add(variable.value)); - }); - - return Array.from(optionsSet).map((value) => ({ value, type: typeof value })); - }; - - const convertAndStyleValue = (value: any, property: string, editable: boolean) => { - const valuesArray = getAutoCompleteOptions(property); - let type = typeof value; - if (getAutoCompleteOptions(property).length > 0) { - type = typeof valuesArray[0].value; - } - - if (editable) { - if (type === "boolean" || typeof value === "boolean") { - return ( - - - - ); - } - - if (type === "string" || typeof value === "string") { - return ( - <> - - - ); - } - - if (type === "number" || typeof value === "number") { - return ( - <> - - - ); - } - - if (value === null || value === undefined) { - return ( - <> - - - ); - } - } else { - if (type === "boolean" || typeof value === "boolean") { - return ( - null} value={value}> - Yes - No - - ); - } - - if (type === "string" || typeof value === "string") { - return {value}; - } - - if (type === "number" || typeof value === "number") { - if (property.toLowerCase().includes("amount")) { - return ${value}; - } else { - return {value}; - } - } - - if (value === null || value === undefined) { - return null; - } - } - - return {value}; - }; - - const handleValueChange = (value: any, property: string) => { - let queryValue: any = value; - if (typeof value === "string") { - if (value.toLowerCase() === "true") { - queryValue = true; - } else if (value.toLowerCase() === "false") { - queryValue = false; - } else if (!isNaN(Number(value))) { - queryValue = Number(value); - } - } - - const updatedData = { ...rawData, [property]: queryValue } || {}; - - if (typeof setRawData === "function") { - setRawData(updatedData); - } else { - console.error("setRawData is not a function or is undefined"); - } - }; - - const handleInputChange = (val: any, property: string) => { - const updatedData = { ...rawData, [property]: val }; - if (typeof setRawData === "function") { - setRawData(updatedData); - } - }; - const showColumn = (data: any[], columnKey: string) => { return data.some((item) => item[columnKey] !== null && item[columnKey] !== undefined); }; @@ -188,7 +54,7 @@ export default function ScenarioFormatter({ title, rawData, setRawData, scenario .sort(([propertyA], [propertyB]) => propertyA.localeCompare(propertyB)) .map(([property, value], index) => ({ property: propertyRuleMap?.find((item) => item.property === property)?.name || property, - value: convertAndStyleValue(value, property, editable), + value: inputStyler(value, property, editable, scenarios, rawData, setRawData), key: index, })); // Check if data.result is an array @@ -208,9 +74,7 @@ export default function ScenarioFormatter({ title, rawData, setRawData, scenario {title} {title === "Outputs" && } {showTable && ( - <> -
    - +
    )} ); From 5a127a82b4657b4b09f62ed5acb2806b48ea43f9 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 14:17:16 -0700 Subject: [PATCH 62/66] Refactor to reduce code duplication. --- .../SimulationViewer/SimulationViewer.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index 29ba656..e8acce8 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -22,16 +22,19 @@ interface SimulationViewerProps { } export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios }: SimulationViewerProps) { - const createRuleMap = (array: any[], defaultObj: { rulemap: boolean }) => { - return array.reduce((acc, obj) => { - acc[obj.property] = null; - return acc; - }, defaultObj); + const createRuleMap = (array: any[]) => { + return array.reduce( + (acc, obj) => { + acc[obj.property] = null; + return acc; + }, + { rulemap: true } + ); }; - const ruleMapInputs = createRuleMap(rulemap.inputs, { rulemap: true }); - const ruleMapOutputs = createRuleMap(rulemap.outputs, { rulemap: true }); - const ruleMapResultOutputs = createRuleMap(rulemap.resultOutputs, { rulemap: true }); + const ruleMapInputs = createRuleMap(rulemap.inputs); + const ruleMapOutputs = createRuleMap(rulemap.outputs); + const ruleMapResultOutputs = createRuleMap(rulemap.resultOutputs); const [selectedSubmissionInputs, setSelectedSubmissionInputs] = useState(ruleMapInputs); const [contextToSimulate, setContextToSimulate] = useState(); From 4cd070d930295636c81e5421c1c2a52f04db3547 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 15:51:17 -0700 Subject: [PATCH 63/66] Add editing filter to view for embedded pages. --- .../ScenarioGenerator/ScenarioGenerator.tsx | 6 +++-- .../ScenarioViewer/ScenarioViewer.tsx | 4 +++- .../SimulationViewer/SimulationViewer.tsx | 24 +++++++++++++++++-- app/rule/[ruleId]/embedded/page.tsx | 2 +- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/app/components/ScenarioGenerator/ScenarioGenerator.tsx b/app/components/ScenarioGenerator/ScenarioGenerator.tsx index b632d99..bbd6a2a 100644 --- a/app/components/ScenarioGenerator/ScenarioGenerator.tsx +++ b/app/components/ScenarioGenerator/ScenarioGenerator.tsx @@ -17,6 +17,7 @@ interface ScenarioGeneratorProps { ruleId: string; jsonFile: string; rulemap: RuleMap; + editing?: boolean; } export default function ScenarioGenerator({ @@ -29,6 +30,7 @@ export default function ScenarioGenerator({ ruleId, jsonFile, rulemap, + editing = true, }: ScenarioGeneratorProps) { const [simulationRun, setSimulationRun] = useState(false); const [newScenarioName, setNewScenarioName] = useState(""); @@ -101,7 +103,7 @@ export default function ScenarioGenerator({ Simulate ▶ - {simulationRun && ( + {simulationRun && editing && ( <> } - {scenarioExpectedOutput && ( + {scenarioExpectedOutput && editing && ( { setScenarioExpectedOutput(data); diff --git a/app/components/ScenarioViewer/ScenarioViewer.tsx b/app/components/ScenarioViewer/ScenarioViewer.tsx index 8814503..4c27bff 100644 --- a/app/components/ScenarioViewer/ScenarioViewer.tsx +++ b/app/components/ScenarioViewer/ScenarioViewer.tsx @@ -15,6 +15,7 @@ interface ScenarioViewerProps { setSelectedSubmissionInputs: (data: any) => void; runSimulation: () => void; rulemap: RuleMap; + editing?: boolean; } export default function ScenarioViewer({ @@ -23,6 +24,7 @@ export default function ScenarioViewer({ setSelectedSubmissionInputs, runSimulation, rulemap, + editing = true, }: ScenarioViewerProps) { const [scenariosDisplay, setScenariosDisplay] = useState(scenarios); const [selectedScenario, setSelectedScenario] = useState(null); @@ -96,7 +98,7 @@ export default function ScenarioViewer({ ))} - + {editing && } ) : (
    No scenarios available
    diff --git a/app/components/SimulationViewer/SimulationViewer.tsx b/app/components/SimulationViewer/SimulationViewer.tsx index e8acce8..62114d1 100644 --- a/app/components/SimulationViewer/SimulationViewer.tsx +++ b/app/components/SimulationViewer/SimulationViewer.tsx @@ -19,9 +19,16 @@ interface SimulationViewerProps { jsonFile: string; rulemap: RuleMap; scenarios: Scenario[]; + editing?: boolean; } -export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios }: SimulationViewerProps) { +export default function SimulationViewer({ + ruleId, + jsonFile, + rulemap, + scenarios, + editing = true, +}: SimulationViewerProps) { const createRuleMap = (array: any[]) => { return array.reduce( (acc, obj) => { @@ -83,6 +90,7 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios resultsOfSimulation={resultsOfSimulation} runSimulation={runSimulation} rulemap={rulemap} + editing={editing} />
    ); @@ -99,6 +107,7 @@ export default function SimulationViewer({ ruleId, jsonFile, rulemap, scenarios ruleId={ruleId} jsonFile={jsonFile} rulemap={rulemap} + editing={editing} />
  • Add additional scenarios to the CSV file
  • diff --git a/app/utils/api.ts b/app/utils/api.ts index 8583c85..22e7a39 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -276,6 +276,38 @@ export const runDecisionsForScenarios = async (goRulesJSONFilename: string) => { } }; +/** + * Downloads a CSV file containing scenarios for a rule run. + * @param goRulesJSONFilename The filename for the JSON rule. + * @returns The processed CSV content as a string. + * @throws If an error occurs during file upload or processing. + */ +export const getCSVForRuleRun = async (goRulesJSONFilename: string): Promise => { + try { + const response = await axiosAPIInstance.post( + "/scenario/evaluation", + { goRulesJSONFilename: goRulesJSONFilename }, + { + responseType: "blob", + headers: { "Content-Type": "application/json" }, + } + ); + + const blob = new Blob([response.data], { type: "text/csv" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${goRulesJSONFilename.replace(/\.json$/, ".csv")}`; + a.click(); + window.URL.revokeObjectURL(url); + + return "CSV downloaded successfully"; + } catch (error) { + console.error(`Error getting CSV for rule run: ${error}`); + throw new Error("Error getting CSV for rule run"); + } +}; + /** * Uploads a CSV file containing scenarios and processes the scenarios against the specified rule. * @param file The file to be uploaded.