Skip to content

Commit

Permalink
Merge pull request #12 from bcgov/feature/linking-rules
Browse files Browse the repository at this point in the history
JDM Update and Linking Rules
  • Loading branch information
timwekkenbc authored Jul 10, 2024
2 parents 6eef5a3 + c40625e commit e030941
Show file tree
Hide file tree
Showing 11 changed files with 1,550 additions and 965 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.ruleSelect {
min-width: 350px;
margin-bottom: 16px;
}

.linkDrawer {
width: 85% !important;
}
89 changes: 89 additions & 0 deletions app/components/RulesDecisionGraph/LinkRuleComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useState, useEffect } from "react";
import { Button, Drawer, Flex, Select, Spin } from "antd";
import { DefaultOptionType } from "antd/es/cascader";
import { EditOutlined } from "@ant-design/icons";
import { GraphNode, useDecisionGraphActions, useDecisionGraphState } from "@gorules/jdm-editor";
import type { GraphNodeProps } from "@gorules/jdm-editor";
import { getAllRuleData } from "@/app/utils/api";
import SimulationViewer from "../SimulationViewer";
import styles from "./LinkRuleComponent.module.css";

export default function LinkRuleComponent({ specification, id, isSelected, name }: GraphNodeProps) {
const { updateNode } = useDecisionGraphActions();
const node = useDecisionGraphState((state) => (state.decisionGraph?.nodes || []).find((n) => n.id === id));
const goRulesJSONFilename = node?.content?.key;

const [openRuleDrawer, setOpenRuleDrawer] = useState(false);
const [ruleOptions, setRuleOptions] = useState<DefaultOptionType[]>([]);

useEffect(() => {
const getRuleOptions = async () => {
const ruleData = await getAllRuleData();
setRuleOptions(
ruleData.map(({ title, goRulesJSONFilename }) => ({
label: title || goRulesJSONFilename,
value: goRulesJSONFilename,
}))
);
};
if (openRuleDrawer) {
getRuleOptions();
}
}, [openRuleDrawer]);

const showRuleDrawer = () => {
setOpenRuleDrawer(true);
};

const closeRuleDrawer = () => {
setOpenRuleDrawer(false);
};

const onChangeSelection = (jsonFilename: string) => {
// Update the graph with the jsonFilename. We use "key" to keep in line with how goRules handing linking rules
updateNode(id, (draft) => {
draft.content = { key: jsonFilename };
return draft;
});
};

const getShortFilenameOnly = (filepath: string, maxLength: number = 25) => {
const filepathSections = filepath.split("/");
const filename = filepathSections[filepathSections.length - 1];
return filename.length > maxLength ? `${filename.substring(0, maxLength - 3)}...` : filename;
};

return (
<GraphNode id={id} specification={specification} name={name} isSelected={isSelected}>
<Button onClick={showRuleDrawer}>
{goRulesJSONFilename ? getShortFilenameOnly(goRulesJSONFilename) : "Add rule"}
<EditOutlined />
</Button>
<Drawer title={name} onClose={closeRuleDrawer} open={openRuleDrawer} width="80%">
{ruleOptions ? (
<>
<Flex gap="small">
<Select
showSearch
placeholder="Select rule"
filterOption={(input, option) =>
(option?.label ?? "").toString().toLowerCase().includes(input.toLowerCase())
}
options={ruleOptions}
onChange={onChangeSelection}
value={goRulesJSONFilename}
className={styles.ruleSelect}
/>
<Button onClick={closeRuleDrawer}>Done</Button>
</Flex>
{goRulesJSONFilename && <SimulationViewer ruleId={id} jsonFile={goRulesJSONFilename} />}
</>
) : (
<Spin tip="Loading rules..." size="large" className={styles.spinner}>
<div className="content" />
</Spin>
)}
</Drawer>
</GraphNode>
);
}

This file was deleted.

171 changes: 89 additions & 82 deletions app/components/RulesDecisionGraph/RulesDecisionGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,90 +1,67 @@
"use client";
import { useState, useEffect, useRef } from "react";
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 { useState, useMemo, useEffect, useRef } from "react";
import type { ReactFlowInstance } from "reactflow";
import { Spin, message } from "antd";
import { SubmissionData } from "../../types/submission";
import "@gorules/jdm-editor/dist/style.css";
import {
JdmConfigProvider,
DecisionGraph,
NodeSpecification,
DecisionGraphType,
DecisionGraphRef,
Simulation,
} from "@gorules/jdm-editor";
import { ApartmentOutlined, PlayCircleOutlined } from "@ant-design/icons";
import { Scenario, Variable } from "@/app/types/scenario";
import { getDocument, postDecision, getRuleRunSchema, getScenariosByFilename } from "../../utils/api";
import styles from "./RulesDecisionGraph.module.css";
import { SubmissionData } from "../../types/submission";
import LinkRuleComponent from "./LinkRuleComponent";
import SimulatorPanel from "./SimulatorPanel";
import { getScenariosByFilename } from "../../utils/api";

interface RulesViewerProps {
jsonFile: string;
jsonFilename: string;
graphJSON: DecisionGraphType;
contextToSimulate?: SubmissionData | null;
setResultsOfSimulation: (results: Record<string, any>) => void;
setOutputsOfSimulation: (outputs: Record<string, any>) => void;
setContextToSimulate: (results: Record<string, any>) => void;
simulation?: Simulation;
runSimulation: (results: unknown) => void;
isEditable?: boolean;
}

export default function RulesDecisionGraph({
jsonFile,
jsonFilename,
graphJSON,
contextToSimulate,
setResultsOfSimulation,
setOutputsOfSimulation,
setContextToSimulate,
simulation,
runSimulation,
isEditable = true,
}: RulesViewerProps) {
const [graphValue, setGraphValue] = useState<any>(graphJSON);
const decisionGraphRef: any = useRef<DecisionGraphRef>();
const [graphJSON, setGraphJSON] = useState<DecisionGraphType>();
const [reactFlowRef, setReactFlowRef] = useState<ReactFlowInstance>();

useEffect(() => {
const fetchData = async () => {
try {
const data = await getDocument(jsonFile);
setGraphJSON(data);
} catch (error) {
console.error("Error fetching JSON:", error);
setGraphValue(graphJSON);
}, [graphJSON]);

useEffect(() => {
// Ensure graph is in view
const fitGraphToView = () => {
if (reactFlowRef) {
reactFlowRef.fitView();
// Set timeout to fix issue with trying to fit view because fully loaded
setTimeout(() => {
reactFlowRef.fitView();
}, 100);
}
};
fetchData();
}, [jsonFile]);
// Fit to view
fitGraphToView();
}, [graphValue, reactFlowRef]);

// Can set additional react flow options here if we need to change how graph looks when it's loaded in
const reactFlowInit = (reactFlow: ReactFlowInstance) => {
reactFlow.fitView(); // ensure graph is in view
};

useEffect(() => {
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);
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.");
}

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: {} };
setReactFlowRef(reactFlow);
};

const downloadJSON = (jsonData: any, filename: string) => {
Expand All @@ -105,8 +82,7 @@ export default function RulesDecisionGraph({

const handleScenarioInsertion = async () => {
try {
const jsonData = await getDocument(jsonFile);
const scenarios: Scenario[] = await getScenariosByFilename(jsonFile);
const scenarios: Scenario[] = await getScenariosByFilename(jsonFilename);
const scenarioObject = {
tests: scenarios.map((scenario: Scenario) => ({
name: scenario.title || "Default name",
Expand All @@ -121,10 +97,10 @@ export default function RulesDecisionGraph({
})),
};
const updatedJSON = {
...jsonData,
...graphValue,
...scenarioObject,
};
return downloadJSON(updatedJSON, jsonFile);
return downloadJSON(updatedJSON, jsonFilename);
} catch (error) {
console.error("Error fetching JSON:", error);
throw error;
Expand Down Expand Up @@ -155,24 +131,55 @@ export default function RulesDecisionGraph({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (!graphJSON) {
return (
<Spin tip="Loading graph..." size="large" className={styles.spinner}>
<div className="content" />
</Spin>
);
}
// This is to add the decision node - note that this may be added to the DecisionGraph library
const additionalComponents: NodeSpecification[] = useMemo(
() => [
{
type: "decisionNode",
displayName: "Rule",
shortDescription: "Linked rule to execute",
icon: <ApartmentOutlined />,
generateNode: () => ({ name: "Linked Rule" }),
renderNode: ({ specification, id, selected, data }) => (
<LinkRuleComponent specification={specification} id={id} isSelected={selected} name={data?.name} />
),
},
],
[]
);

// Simulator custom panel
const panels = useMemo(
() => [
{
id: "simulator",
title: "Simulator",
icon: <PlayCircleOutlined />,
renderPanel: () => (
<SimulatorPanel
contextToSimulate={contextToSimulate}
runSimulation={runSimulation}
setContextToSimulate={setContextToSimulate}
/>
),
},
],
[contextToSimulate, runSimulation, setContextToSimulate]
);

return (
<JdmConfigProvider>
<DecisionGraph
ref={decisionGraphRef}
value={graphJSON}
disabled
value={graphValue}
defaultOpenMenu={false}
onSimulationRun={simulateRun}
simulate={simulation}
configurable
onReactFlowInit={reactFlowInit}
panels={panels}
components={additionalComponents}
onChange={(updatedGraphValue) => setGraphValue(updatedGraphValue)}
disabled={!isEditable}
/>
</JdmConfigProvider>
);
Expand Down
24 changes: 24 additions & 0 deletions app/components/RulesDecisionGraph/SimulatorPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";
import JSON5 from "json5";
import { GraphSimulator } from "@gorules/jdm-editor";
import { SubmissionData } from "@/app/types/submission";

interface SimulatorPanelProps {
contextToSimulate?: SubmissionData | null;
setContextToSimulate: (results: Record<string, any>) => void;
runSimulation: (results: unknown) => void;
}

export default function SimulatorPanel({
contextToSimulate,
runSimulation,
setContextToSimulate,
}: SimulatorPanelProps) {
return (
<GraphSimulator
defaultRequest={JSON5.stringify(contextToSimulate)}
onRun={({ context }: { context: unknown }) => runSimulation(context)}
onClear={() => setContextToSimulate({})}
/>
);
}
5 changes: 5 additions & 0 deletions app/components/SimulationViewer/SimulationViewer.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@

.inputSection button {
width: 10rem;
}

.spinner {
min-height: 500px;

}
Loading

0 comments on commit e030941

Please sign in to comment.