Skip to content

Commit d970ce6

Browse files
authored
input/output test data presentation (#8027)
* cleanup * cleanup * cleanup * enable transition results * missing feature, bugfixes, cleanup
1 parent b3502ed commit d970ce6

File tree

8 files changed

+234
-126
lines changed

8 files changed

+234
-126
lines changed

designer/client/src/actions/actionTypes.ts

-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export type ActionTypes =
3030
| "TOGGLE_PROCESS_ACTION_MODAL"
3131
| "TOGGLE_CUSTOM_ACTION"
3232
| "HIDE_RUN_PROCESS_DETAILS"
33-
| "DISPLAY_TEST_RESULTS_DETAILS"
3433
| "PROCESS_STATE_LOADED"
3534
| "PROCESS_VERSIONS_LOADED"
3635
| "UPDATE_BACKEND_NOTIFICATIONS"

designer/client/src/actions/nk/displayTestResults.ts

+17-7
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,42 @@ import { displayProcessCounts } from "./displayProcessCounts";
1010

1111
export function testProcessFromFile(testDataFile: File): ThunkAction {
1212
return wrapWithTestAction((processName, scenarioGraph) =>
13-
HttpService.testProcess(processName, testDataFile, scenarioGraph).then(({ data }) => ({
13+
HttpService.testScenarioWithFile(processName, scenarioGraph, testDataFile).then(({ data }) => ({
1414
testResults: data,
1515
})),
1616
);
1717
}
1818

19-
export function testProcessWithParameters(testData: SourceWithParametersTest): ThunkAction {
19+
export function testProcessWithParameters(sourceParameters: SourceWithParametersTest): ThunkAction {
2020
return wrapWithTestAction((processName, scenarioGraph) =>
21-
HttpService.testProcessWithParameters(processName, testData, scenarioGraph).then(({ data }) => ({
21+
HttpService.testScenario(processName, scenarioGraph, {
22+
type: "WITH_PARAMETERS",
23+
sourceParameters,
24+
}).then(({ data }) => ({
2225
testResults: data,
23-
testData,
26+
testData: sourceParameters,
2427
})),
2528
);
2629
}
2730

2831
export function testScenarioWithGeneratedData(testSampleSize: string): ThunkAction {
2932
return wrapWithTestAction((processName, scenarioGraph) =>
30-
HttpService.testScenarioWithGeneratedData(processName, parseInt(testSampleSize), scenarioGraph).then(({ data }) => ({
33+
HttpService.testScenario(processName, scenarioGraph, {
34+
type: "WITH_GENERATED_DATA",
35+
numberOfSamples: parseInt(testSampleSize),
36+
}).then(({ data }) => ({
3137
testResults: data,
3238
})),
3339
);
3440
}
3541

3642
export type TestsActions =
37-
| { type: "TEST_RESULTS_LOADING" }
38-
| { type: "TEST_RESULTS_FAILED" }
43+
| {
44+
type: "TEST_RESULTS_LOADING";
45+
}
46+
| {
47+
type: "TEST_RESULTS_FAILED";
48+
}
3949
| {
4050
type: "DISPLAY_TEST_RESULTS_DETAILS";
4151
testResults: TestResults;

designer/client/src/common/TestResultUtils.ts

+7
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,17 @@ export interface TestFormParameters {
5353
parameters: UIParameter[];
5454
}
5555

56+
export type TransitionResults = {
57+
sourceNodeId: NodeId;
58+
destinationNodeId: NodeId;
59+
results: Context[];
60+
};
61+
5662
export interface TestResults {
5763
externalInvocationResults: Record<NodeId, ExternalInvocationResult[]>;
5864
invocationResults: Record<NodeId, InvocationResult[]>;
5965
nodeResults: Record<NodeId, Context[]>;
66+
nodeTransitionResults: Array<TransitionResults>;
6067
exceptions: Error[];
6168
}
6269

designer/client/src/components/graph/node-modal/io/InputOutputContext.tsx

+53-32
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import React, { createContext, PropsWithChildren, useCallback, useContext, useMemo, useReducer } from "react";
1+
import type { PropsWithChildren } from "react";
2+
import React, { createContext, useCallback, useContext, useMemo, useReducer } from "react";
23
import { useSelector } from "react-redux";
4+
5+
import type { TransitionResults } from "../../../../common/TestResultUtils";
36
import TestResultUtils from "../../../../common/TestResultUtils";
4-
import { getProcessName, getScenarioGraph, getTestResults } from "../../../../reducers/selectors/graph";
7+
import { getScenarioGraph, getTestResults } from "../../../../reducers/selectors/graph";
58
import NodeUtils from "../../NodeUtils";
6-
import { VariableContextType } from "./VariableContextTree";
9+
import type { VariableContextType } from "./VariableContextTree";
710

811
export type InputOutputState = {
912
inputDataSetId?: string | null;
@@ -23,13 +26,13 @@ type Action =
2326
context: VariableContextType;
2427
};
2528

29+
type Created = TransitionResults & { id: string };
2630
type ContextType = {
2731
state: InputOutputState;
2832
dispatch: React.Dispatch<Action>;
29-
getAvailableContexts: (nodeIds: string[], direction?: "input" | "output") => VariableContextType[];
30-
prevNodes: string[];
31-
inputNodes: string[];
32-
outputNodes: string[];
33+
getAvailableContexts: (direction?: "input" | "output") => VariableContextType[];
34+
inputNodesIds: Created[];
35+
outputNodesIds: Created[];
3336
};
3437

3538
const InputOutputContext = createContext<ContextType>(null);
@@ -67,18 +70,33 @@ export const InputOutputContextProvider = ({
6770
}: PropsWithChildren<{
6871
nodeId: string;
6972
}>) => {
70-
const [state, dispatch] = useReducer(reducer, initialState);
71-
7273
const scenario = useSelector(getScenarioGraph);
74+
const testResults = useSelector(getTestResults);
7375

74-
const [inputNodes, outputNodes, prevNodes] = useMemo(() => {
75-
if (!nodeId) throw "no NodeId provided!";
76-
return [
77-
[nodeId],
78-
NodeUtils.getNodesConnectedToOutput(nodeId, scenario).map((n) => n.id),
79-
NodeUtils.getNodesConnectedToInput(nodeId, scenario).map((n) => n.id),
80-
];
81-
}, [nodeId, scenario]);
76+
const [state, dispatch] = useReducer(reducer, initialState);
77+
78+
const nodeTransitionResults = useMemo(
79+
() => testResults?.nodeTransitionResults?.filter((r) => r.destinationNodeId === nodeId || r.sourceNodeId === nodeId),
80+
[nodeId, testResults?.nodeTransitionResults],
81+
);
82+
const inputs = useMemo(() => {
83+
return NodeUtils.getNodesConnectedToInput(nodeId, scenario).map(({ id }) => {
84+
const transitionResults = nodeTransitionResults?.find((r) => r.destinationNodeId === nodeId && r.sourceNodeId === id);
85+
return {
86+
id,
87+
...transitionResults,
88+
};
89+
});
90+
}, [nodeId, nodeTransitionResults, scenario]);
91+
const outputs = useMemo(() => {
92+
return NodeUtils.getNodesConnectedToOutput(nodeId, scenario).map(({ id }) => {
93+
const transitionResults = nodeTransitionResults?.find((r) => r.sourceNodeId === nodeId && r.destinationNodeId === id);
94+
return {
95+
id,
96+
...transitionResults,
97+
};
98+
});
99+
}, [nodeId, nodeTransitionResults, scenario]);
82100

83101
const isContextDisabled = useCallback(
84102
(id: string, direction: "input" | "output" = "input") => {
@@ -92,46 +110,49 @@ export const InputOutputContextProvider = ({
92110
[state.inputDataSetId],
93111
);
94112

95-
const results = useSelector(getTestResults);
113+
const getError = useCallback(
114+
(destinationNodeId: string, contextId: string) =>
115+
TestResultUtils.resultsForNode(testResults, destinationNodeId)?.errors?.find(({ context }) => context.id === contextId),
116+
[testResults],
117+
);
118+
96119
const getAvailableContexts = useCallback(
97-
(nodeIds: string[], direction: "input" | "output" = "input") => {
120+
(direction: "input" | "output" = "input") => {
121+
const transitionResults = direction === "input" ? inputs : outputs;
98122
const contexts: VariableContextType[] = [];
99-
100-
nodeIds.forEach((nodeId) => {
101-
const testResults = TestResultUtils.resultsForNode(results, nodeId);
102-
testResults.nodeResults.forEach(({ id, variables }) => {
123+
transitionResults.forEach(({ id: contextNodeId, destinationNodeId, results }) => {
124+
results?.forEach(({ id, variables }) => {
103125
const foundContext = contexts.find((context) => context.id === id);
104126
if (foundContext) {
105-
foundContext.nodeIds.push(nodeId);
127+
foundContext.nodeIds.push(contextNodeId);
106128
return;
107129
}
108130

109-
const error = testResults.errors?.find(({ context }) => context.id === id);
131+
const error = direction === "input" && getError(destinationNodeId, id);
110132

111133
contexts.push({
112134
id,
113135
variables,
114136
disabled: isContextDisabled(id, direction),
115-
nodeIds: [nodeId],
137+
nodeIds: [contextNodeId],
116138
error: error?.throwable,
117139
});
118140
});
119141
});
120142
return contexts;
121143
},
122-
[isContextDisabled, results],
144+
[inputs, outputs, getError, isContextDisabled],
123145
);
124146

125-
const value = useMemo(
147+
const value = useMemo<ContextType>(
126148
() => ({
127149
state,
128150
dispatch,
129151
getAvailableContexts,
130-
prevNodes,
131-
inputNodes,
132-
outputNodes,
152+
inputNodesIds: inputs,
153+
outputNodesIds: outputs,
133154
}),
134-
[getAvailableContexts, inputNodes, outputNodes, prevNodes, state],
155+
[getAvailableContexts, inputs, outputs, state],
135156
);
136157
return <InputOutputContext.Provider value={value}>{children}</InputOutputContext.Provider>;
137158
};

designer/client/src/components/graph/node-modal/io/InputOutputLayout.tsx

+58-14
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { css } from "@emotion/css";
22
import { ExpandLess, MoreHoriz } from "@mui/icons-material";
33
import { Box, styled } from "@mui/material";
4-
import { CSSObject } from "@mui/styled-engine";
4+
import type { CSSObject } from "@mui/styled-engine";
55
import { Allotment } from "allotment";
66
import type { AllotmentHandle } from "allotment/dist/types/src/allotment";
77
import { sum } from "lodash";
8-
import React, { PropsWithChildren, useCallback, useRef, useState } from "react";
8+
import type { PropsWithChildren } from "react";
9+
import React, { useCallback, useRef, useState } from "react";
910
import "allotment/dist/style.css";
11+
import { ErrorBoundary } from "react-error-boundary";
12+
1013
import { getScrollStyle } from "../node/StyledHeader";
1114
import { VariableContextTree } from "./VariableContextTree";
12-
import { ErrorBoundary } from "react-error-boundary";
1315

1416
const shadowClassName = css({
1517
boxShadow: "0px 0px 15px rgba(0,0,0,0.2)",
@@ -53,16 +55,16 @@ export const InputOutputLayout = function InputOutputWrapper({ children }: Props
5355
}, []);
5456

5557
const prevSizes = useRef<number[]>([0, 0, 0]);
56-
const togglePanel = useCallback((index: number, collapsedSize: number) => {
57-
const prevSize = prevSizes.current[index];
58+
59+
const togglePanel = useCallback((index: number, collapsedSize: number, shouldCollapse = sizes.current[index] > collapsedSize * 2) => {
5860
const currentSize = sizes.current[index];
61+
const prevSize = prevSizes.current[index];
5962
const defaultSize = 0.2 * sum(sizes.current);
60-
const shouldCollapse = currentSize > collapsedSize * 2;
6163
const newSize = shouldCollapse ? collapsedSize : Math.max(prevSize, defaultSize);
6264
const updatedCenter = sizes.current[1] + (shouldCollapse ? currentSize : collapsedSize) - newSize;
6365

6466
prevSizes.current = sizes.current;
65-
ref.current.resize(
67+
ref.current?.resize(
6668
sizes.current.map((size, i) => {
6769
if (i === index) return newSize;
6870
if (i === 1) return updatedCenter;
@@ -85,13 +87,34 @@ export const InputOutputLayout = function InputOutputWrapper({ children }: Props
8587
setRightCollapsed(right < collapsedSize * 2);
8688
}, []);
8789

90+
const [leftHidden, setLeftHidden] = useState(false);
91+
const [rightHidden, setRightHidden] = useState(false);
92+
8893
return (
8994
<Wrapper>
9095
<Allotment ref={ref} onChange={onChange} defaultSizes={[278, 820, 278]}>
91-
<Allotment.Pane preferredSize="20%" minSize={collapsedSize}>
92-
<SidePanelBox sx={{ alignItems: "flex-start", overflowY: leftCollapsed ? "hidden" : "auto" }}>
96+
<Allotment.Pane
97+
preferredSize="20%"
98+
minSize={leftHidden ? 0 : collapsedSize}
99+
maxSize={leftHidden ? 0 : Infinity}
100+
visible={!leftHidden}
101+
>
102+
<SidePanelBox
103+
sx={{
104+
alignItems: "flex-start",
105+
overflowY: leftCollapsed ? "hidden" : "auto",
106+
}}
107+
>
93108
<ErrorBoundary fallback={<div>{`ERROR`}</div>}>
94-
<VariableContextTree direction="input" />
109+
<VariableContextTree
110+
direction="input"
111+
onIsEmptyChange={(isEmpty) => {
112+
setLeftHidden(isEmpty);
113+
if (isEmpty) {
114+
togglePanel(0, 0, true);
115+
}
116+
}}
117+
/>
95118
</ErrorBoundary>
96119
</SidePanelBox>
97120
<PanelButton side="left" collapsed={leftCollapsed} onClick={() => togglePanel(0, collapsedSize)}>
@@ -101,10 +124,28 @@ export const InputOutputLayout = function InputOutputWrapper({ children }: Props
101124
<Allotment.Pane preferredSize="60%" minSize={820} className={shadowClassName}>
102125
{children}
103126
</Allotment.Pane>
104-
<Allotment.Pane preferredSize="20%" minSize={collapsedSize}>
105-
<SidePanelBox sx={{ alignItems: "flex-end", overflowY: rightCollapsed ? "hidden" : "auto" }}>
127+
<Allotment.Pane
128+
preferredSize="20%"
129+
minSize={rightHidden ? 0 : collapsedSize}
130+
maxSize={rightHidden ? 0 : Infinity}
131+
visible={!rightHidden}
132+
>
133+
<SidePanelBox
134+
sx={{
135+
alignItems: "flex-end",
136+
overflowY: rightCollapsed ? "hidden" : "auto",
137+
}}
138+
>
106139
<ErrorBoundary fallback={<div>{`ERROR`}</div>}>
107-
<VariableContextTree direction="output" />
140+
<VariableContextTree
141+
direction="output"
142+
onIsEmptyChange={(isEmpty) => {
143+
setRightHidden(isEmpty);
144+
if (isEmpty) {
145+
togglePanel(2, 0, isEmpty);
146+
}
147+
}}
148+
/>
108149
</ErrorBoundary>
109150
</SidePanelBox>
110151
<PanelButton side="right" collapsed={rightCollapsed} onClick={() => togglePanel(2, collapsedSize)}>
@@ -132,7 +173,10 @@ const SidePanelBox = styled(Box)(({ theme }) => ({
132173

133174
const PanelButton = styled("button", {
134175
shouldForwardProp: (prop) => prop !== "side" && prop !== "collapsed",
135-
})<{ side?: "center" | "left" | "right"; collapsed?: boolean }>(({ side, collapsed, theme }) => {
176+
})<{
177+
side?: "center" | "left" | "right";
178+
collapsed?: boolean;
179+
}>(({ side, collapsed, theme }) => {
136180
const styles: CSSObject = {
137181
position: "absolute",
138182
bottom: "50%",

0 commit comments

Comments
 (0)