Skip to content

Commit

Permalink
Merge pull request #35 from bcgov/feature/generate-scenarios
Browse files Browse the repository at this point in the history
Generate scenarios for isolation testing.
  • Loading branch information
brysonjbest authored Oct 8, 2024
2 parents 397a5c8 + 09d8977 commit fefbbf8
Show file tree
Hide file tree
Showing 11 changed files with 324 additions and 22 deletions.
20 changes: 19 additions & 1 deletion app/components/InputStyler/InputStyler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ export default function InputStyler(
ruleProperties: any
) {
const updateFieldValue = (field: string, value: any) => {
console.log(field, value, "this is input change");
const updatedData = { ...rawData, [field]: value };
if (typeof setRawData === "function") {
setRawData(updatedData);
Expand Down Expand Up @@ -161,6 +160,17 @@ export default function InputStyler(
handleInputChange={handleInputChange}
/>
);
case "multiselect":
return (
<SelectInput
show={validationRules?.type === "multiselect"}
value={value}
field={field}
options={validationRules?.options}
handleInputChange={handleInputChange}
multiple
/>
);
case "text":
return (
<TextInput
Expand Down Expand Up @@ -207,6 +217,14 @@ export default function InputStyler(
);
}
} else {
//Check if all the items in an array are objects. If not, display as a string
// This is used to specifically generate the multiselect values without rendering nested objects
const allObjects = Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null);
if (Array.isArray(value) && !allObjects) {
const stringValue = value.filter((item) => typeof item !== "object" || item === null).join(", ");
return <ReadOnlyStringDisplay show value={stringValue} />;
}

return (
<>
<ReadOnlyArrayDisplay
Expand Down
21 changes: 11 additions & 10 deletions app/components/InputStyler/subcomponents/InputComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,19 @@ export const ChildFieldInput = ({
rawData,
value,
}: ChildFieldInputProps) => (
<div key={each.field}>
<div key={each.name}>
{each.label}
{InputStyler(
item[each.name],
`${field}[${index}].${each.name}`,
item[each.field],
`${field}[${index}].${each.field}`,
true,
scenarios,
rawData,
(newData: { [x: string]: any }) => {
const updatedArray = [...value];
updatedArray[index] = {
...updatedArray[index],
[each.name]: newData[`${field}[${index}].${each.name}`],
[each.field]: newData[`${field}[${index}].${each.field}`],
};
handleInputChange?.(updatedArray, field);
},
Expand All @@ -76,8 +76,8 @@ export const ObjectArrayInput = ({

const customName = parsedPropertyName.charAt(0).toUpperCase() + parsedPropertyName.slice(1);
const childFields = ruleProperties?.childFields || [];
const childFieldMap = childFields.reduce((acc: { [x: string]: null }, field: { name: string | number }) => {
acc[field.name] = null;
const childFieldMap = childFields.reduce((acc: { [x: string]: null }, field: { field: string | number }) => {
acc[field.field] = null;
return acc;
}, {});

Expand All @@ -95,9 +95,9 @@ export const ObjectArrayInput = ({
{customName} {index + 1}
</h4>
<label className="labelsmall">
{childFields.map((each: { name: any }) => (
{childFields.map((each: { field: any }) => (
<ChildFieldInput
key={`child-field-${each.name ?? null}-${index}`}
key={`child-field-${each.field ?? null}-${index}`}
item={item}
each={each}
index={index}
Expand Down Expand Up @@ -151,7 +151,7 @@ export const BooleanInput = ({ show, value, field, handleInputChange }: BooleanI
);
};

export const SelectInput = ({ show, value, field, options, handleInputChange }: SelectInputProps) => {
export const SelectInput = ({ show, value, field, options, handleInputChange, multiple }: SelectInputProps) => {
if (!show) return null;
return (
<label className="labelsmall">
Expand All @@ -162,6 +162,7 @@ export const SelectInput = ({ show, value, field, options, handleInputChange }:
defaultValue={value}
style={{ width: 200 }}
onChange={(val) => handleInputChange(val, field)}
mode={multiple ? "multiple" : undefined}
/>
</Flex>
<span className="label-text">{parsePropertyName(field)}</span>
Expand Down Expand Up @@ -237,7 +238,7 @@ export const ReadOnlyBooleanDisplay = ({ show, value }: ReadOnlyProps) => {

export const ReadOnlyStringDisplay = ({ show, value }: ReadOnlyProps) => {
if (!show) return null;
return <Tag color="blue">{value}</Tag>;
return value.toString();
};

export const ReadOnlyNumberDisplay = ({ show, value, field }: ReadOnlyNumberDisplayProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ export default function RuleInputOutputFieldsComponent({
klammData?.child_fields &&
klammData?.child_fields.map((child) => ({
id: child.id,
name: child.name,
field: child.label,
name: child.label,
field: child.name,
description: child.description,
dataType: child?.bre_data_type?.name,
validationCriteria: child?.bre_data_validation?.validation_criteria,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.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;
}

.upload {
margin-right: 10px;
}

.runButton {
margin-left: 10px;
}

.scenarioGenerator {
width: 100%;
}

@media (max-width: 1100px) {
.scenarioGenerator {
flex-wrap: wrap;
}
}

@media (max-width: 768px) {
.scenarioGenerator {
flex-direction: column;
width: 100%;
}
}
114 changes: 114 additions & 0 deletions app/components/ScenariosManager/IsolationTester/IsolationTester.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { useState, useEffect } from "react";
import { Flex, Button, message, InputNumber, Collapse } from "antd";
import { Scenario } from "@/app/types/scenario";
import { getCSVTests } from "@/app/utils/api";
import { RuleMap } from "@/app/types/rulemap";
import ScenarioFormatter from "../ScenarioFormatter";
import styles from "./IsolationTester.module.css";
import { DecisionGraphType } from "@gorules/jdm-editor";
import { valueType } from "antd/es/statistic/utils";

interface IsolationTesterProps {
scenarios: Scenario[];
simulationContext?: Record<string, any>;
setSimulationContext: (data: any) => void;
resetTrigger: boolean;
jsonFile: string;
rulemap: RuleMap;
ruleContent?: DecisionGraphType;
ruleVersion?: string | boolean;
}

export default function IsolationTester({
scenarios,
simulationContext,
setSimulationContext,
resetTrigger,
jsonFile,
rulemap,
ruleContent,
ruleVersion,
}: IsolationTesterProps) {
const [testScenarioCount, setTestScenarioCount] = useState<valueType | null>(10);

const handleCSVTests = async () => {
const ruleName = ruleVersion === "draft" ? "Draft" : ruleVersion === "inreview" ? "In Review" : "Published";
try {
const csvContent = await getCSVTests(jsonFile, ruleName, ruleContent, simulationContext, testScenarioCount);
message.success(`Scenario Tests: ${csvContent}`);
} catch (error) {
message.error("Error downloading scenarios.");
console.error("Error:", error);
}
};

useEffect(() => {
const editScenario = { ...simulationContext, rulemap: true };
setSimulationContext(editScenario);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resetTrigger]);

return (
<Flex gap={"small"}>
<div>
<Flex gap={"small"}>
<ol className={styles.instructionsList}>
<li>
This tab allows you to test your rule by defining specific variables you would like to remain unchanged
while generating possible scenarios that combine the possibilities of the other variables you leave blank.
</li>
<li>
Define any variables you would like to remain unchanged. The more that you define, the more specific the
tests will be.
<Collapse
items={[
{
key: "1",
label: "Input Variables",
children: (
<Flex gap="middle" className={styles.IsolationTester}>
{simulationContext && (
<Flex gap={"small"} align="end">
<ScenarioFormatter
title="Inputs"
rawData={simulationContext}
setRawData={(data) => setSimulationContext(data)}
scenarios={scenarios}
rulemap={rulemap}
/>
</Flex>
)}
</Flex>
),
},
]}
/>
</li>
<li>
{" "}
Any undefined variables will be randomly generated based on the validation values defined in klamm for
these inputs.
</li>
<li>
Enter the maximum number of scenarios you would like to generate (there is a maximum of 1000):{" "}
<InputNumber
value={testScenarioCount}
onChange={(value) => setTestScenarioCount(value)}
changeOnBlur
defaultValue={10}
min={1}
max={1000}
/>
</li>
<li>
Generate a CSV file with your created tests:{" "}
<Button onClick={handleCSVTests} size="large" type="primary">
Generate Tests
</Button>
</li>
</ol>
</Flex>
</div>
</Flex>
);
}
1 change: 1 addition & 0 deletions app/components/ScenariosManager/IsolationTester/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./IsolationTester";
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,22 @@ export default function ScenarioResults({ scenarios, jsonFile, ruleContent }: Sc
const hasError = useRef(false);
const { isMobile, isTablet } = useResponsiveSize();

const styleArray = (arr: any[]): string | number => {
const allObjects = arr.every((item) => typeof item === "object" && item !== null);
if (allObjects) {
return arr.length;
} else {
return arr.filter((item) => typeof item !== "object" || item === null).join(", ");
}
};

const applyConditionalStyling = (value: any, field: string): React.ReactNode => {
if (value === null || value === undefined) {
return null;
}

if (Array.isArray(value)) {
return value.length > 0 ? value.length : null;
return styleArray(value);
}

if (typeof value === "boolean" && field === "resultMatch") {
Expand Down
26 changes: 26 additions & 0 deletions app/components/ScenariosManager/ScenariosManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ScenarioGenerator from "./ScenarioGenerator";
import ScenarioResults from "./ScenarioResults";
import ScenarioCSV from "./ScenarioCSV";
import styles from "./ScenariosManager.module.css";
import IsolationTester from "./IsolationTester";

interface ScenariosManagerProps {
ruleId: string;
Expand Down Expand Up @@ -44,6 +45,7 @@ export default function ScenariosManager({
InputsTab = "2",
ResultsTab = "3",
CSVTab = "4",
IsolationTesterTab = "5",
}

const [resetTrigger, setResetTrigger] = useState<boolean>(false);
Expand Down Expand Up @@ -123,6 +125,24 @@ export default function ScenariosManager({
</Flex>
);

const isolationTestTab = (
<Flex gap="small">
<IsolationTester
scenarios={activeScenarios}
simulationContext={simulationContext}
setSimulationContext={setSimulationContext}
resetTrigger={resetTrigger}
jsonFile={jsonFile}
rulemap={rulemap}
ruleContent={ruleContent}
ruleVersion={isEditing}
/>
<Button onClick={handleReset} size="large" type="primary">
Reset ↻
</Button>
</Flex>
);

const items: TabsProps["items"] = [
{
key: ScenariosManagerTabs.ScenariosTab,
Expand All @@ -148,6 +168,12 @@ export default function ScenariosManager({
children: csvTab,
disabled: !showAllScenarioTabs,
},
{
key: ScenariosManagerTabs.IsolationTesterTab,
label: "Isolation Tester",
children: isolationTestTab,
disabled: !showAllScenarioTabs,
},
];

const filteredItems = showAllScenarioTabs ? items : items?.filter((item) => item.disabled !== true) || [];
Expand Down
Loading

0 comments on commit fefbbf8

Please sign in to comment.