Skip to content

Commit 235603f

Browse files
authored
Merge pull request #131 from FlowTestAI/multi-tab
feat: multi tab flow management
2 parents a9b32fa + 5a92053 commit 235603f

16 files changed

Lines changed: 295 additions & 258 deletions

File tree

.changeset/nice-bats-suffer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'flowtestai': minor
3+
---
4+
5+
Ability to manage multiple flow tabs simultaneously

packages/flowtest-cli/bin/index.js

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ const argv = yargs(hideBin(process.argv))
5656
});
5757
},
5858
async (argv) => {
59-
console.log(`Reading file: ${argv.file}`);
6059
if (argv.file.toLowerCase().endsWith(`.flow`)) {
6160
let content = undefined;
6261
try {
@@ -79,9 +78,9 @@ const argv = yargs(hideBin(process.argv))
7978
argv.env ? getEnvVariables(argv.env) : {},
8079
logger,
8180
);
82-
console.log(chalk.yellow('Running Flow \n'));
81+
console.log(chalk.blue('Running Flow \n'));
8382
console.log(
84-
chalk.blue(
83+
chalk.yellow(
8584
'Right now CLI commands must be run from root directory of collection. We will gradually add support to run commands from anywhere inside the collection. \n',
8685
),
8786
);
@@ -109,27 +108,43 @@ const argv = yargs(hideBin(process.argv))
109108
};
110109
const accessId = process.env.FLOWTEST_ACCESS_ID;
111110
const accessKey = process.env.FLOWTEST_ACCESS_KEY;
112-
try {
113-
const response = await axiosClient.post('/upload', data, {
114-
headers: {
115-
'Content-Type': 'application/json',
116-
'x-access-id': accessId,
117-
'x-access-key': accessKey,
118-
},
119-
});
120-
console.log(chalk.bold('Flow Scan: ') + chalk.dim(`${baseUrl}/scan/${response.data.data[0].id}`));
121-
} catch (error) {
122-
if (error?.response) {
123-
if (error.response?.status >= 400 && error.response?.status < 500) {
124-
console.log(chalk.red(` ${JSON.stringify(error.response?.data)}`));
125-
}
111+
if (!accessId || accessId.trim() === '' || !accessKey || accessKey.trim() === '') {
112+
console.log(chalk.red(` ✕ `) + chalk.dim('Unable to upload flow scan'));
113+
console.log(
114+
chalk.yellow(`Failed to detect access key pairs. Make sure to set environment variables properly.`),
115+
);
116+
console.log(chalk.yellow(` export FLOWTEST_ACCESS_ID="<<FLOWTEST_ACCESS_ID>>"`));
117+
console.log(chalk.yellow(` export FLOWTEST_ACCESS_KEY="<<FLOWTEST_ACCESS_KEY>>"`));
118+
} else {
119+
try {
120+
const response = await axiosClient.post('/upload', data, {
121+
headers: {
122+
'Content-Type': 'application/json',
123+
'x-access-id': accessId,
124+
'x-access-key': accessKey,
125+
},
126+
});
127+
console.log(chalk.bold('Flow Scan: ') + chalk.dim(`${baseUrl}/scan/${response.data.data[0].id}`));
128+
} catch (error) {
129+
if (error?.response) {
130+
if (error.response?.status >= 400 && error.response?.status < 500) {
131+
console.log(chalk.red(` ${JSON.stringify(error.response?.data)}`));
132+
}
126133

127-
if (error.response?.status === 500) {
128-
console.log(chalk.red(' Internal Server Error'));
134+
if (error.response?.status === 500) {
135+
console.log(chalk.red(' Internal Server Error'));
136+
}
129137
}
138+
console.log(chalk.red(` ✕ `) + chalk.dim('Unable to upload flow scan'));
130139
}
131-
console.log(chalk.red(` ✕ `) + chalk.dim('Unable to upload flow scan'));
132140
}
141+
} else {
142+
console.log('\n');
143+
console.log(
144+
chalk.yellow(
145+
'Enable flow scans today to get more value our of your APIs. Get your access key pairs at https://flowtest-ai.vercel.app/ \n',
146+
),
147+
);
133148
}
134149

135150
process.exit(1);

packages/flowtest-cli/graph/compute/authnode.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,33 @@ const chalk = require('chalk');
44
const { LogLevel } = require('../GraphLogger');
55

66
class authNode extends Node {
7-
constructor(auth, envVariables, logger) {
7+
constructor(nodeData, envVariables, logger) {
88
super('authNode');
9-
(this.auth = auth), (this.envVariables = envVariables);
9+
(this.nodeData = nodeData), (this.envVariables = envVariables);
1010
this.logger = logger;
1111
}
1212

1313
evaluate() {
1414
//console.log('Evaluating an auth node');
15-
if (this.auth.type === 'basic-auth') {
15+
if (this.nodeData.type === 'basic-auth') {
1616
console.log(chalk.green(` ✓ `) + chalk.dim('.....setting basic authentication'));
1717
this.logger.add(LogLevel.INFO, '', { type: 'authNode', data: { authType: 'Basic Authentication' } });
18-
this.auth.username = computeVariables(this.auth.username, this.envVariables);
19-
this.auth.password = computeVariables(this.auth.password, this.envVariables);
18+
const username = computeVariables(this.nodeData.username, this.envVariables);
19+
const password = computeVariables(this.nodeData.password, this.envVariables);
2020

21-
return this.auth;
22-
} else if (this.auth.type === 'no-auth') {
21+
return {
22+
type: 'basic-auth',
23+
username,
24+
password,
25+
};
26+
} else if (this.nodeData.type === 'no-auth') {
2327
console.log(chalk.green(` ✓ `) + chalk.dim('.....using no authentication'));
2428
this.logger.add(LogLevel.INFO, '', { type: 'authNode', data: { authType: 'No Authentication' } });
25-
return this.auth;
29+
return {
30+
type: 'no-auth',
31+
};
2632
} else {
27-
throw Error(`auth type: ${this.auth.type} is not valid`);
33+
throw Error(`auth type: ${this.nodeData.type} is not valid`);
2834
}
2935
}
3036
}

packages/flowtest-cli/graph/compute/requestnode.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ class requestNode extends Node {
161161
headers: request.headers,
162162
// form data obj gets serialized here so that it can be sent over wire
163163
// otherwise ipc communication errors out
164-
data: JSON.parse(JSON.stringify(request.data)),
164+
data: request.data ? JSON.parse(JSON.stringify(request.data)) : request.data,
165165
};
166166

167167
const result = await axios({

packages/flowtest-electron/src/ipc/collection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
317317
headers: request.headers,
318318
// form data obj gets serialized here so that it can be sent over wire
319319
// otherwise ipc communication errors out
320-
data: JSON.parse(JSON.stringify(request.data)),
320+
data: request.data ? JSON.parse(JSON.stringify(request.data)) : request.data,
321321
};
322322

323323
const result = await axios({

src/components/molecules/environment/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const Env = ({ tab }) => {
3333
});
3434

3535
return (
36-
<div className='p-4'>
36+
<div className='p-4' key={tab.id}>
3737
<table className='w-full leading-normal'>
3838
<thead>
3939
<tr className='bg-ghost-50 text-ghost-600 text-left text-xs font-bold uppercase tracking-wider'>

src/components/molecules/flow/graph/Graph.js

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,27 @@
22

33
import { cloneDeep } from 'lodash';
44
import { readFlowTestSync } from 'service/collection';
5-
import useCanvasStore from 'stores/CanvasStore';
65
import authNode from './compute/authnode';
76
import nestedFlowNode from './compute/nestedflownode';
87
import assertNode from './compute/assertnode';
98
import requestNode from './compute/requestnode';
109
import setVarNode from './compute/setvarnode';
1110
import { LogLevel } from './GraphLogger';
11+
import { useTabStore } from 'stores/TabStore';
1212

1313
class Graph {
14-
constructor(nodes, edges, startTime, initialEnvVars, logger, caller, collectionPath) {
14+
constructor(nodes, edges, startTime, initialEnvVars, logger, collectionPath, timeout, tab) {
1515
this.nodes = nodes;
1616
this.edges = edges;
1717
this.logger = logger;
18-
this.timeout = useCanvasStore.getState().timeout;
18+
this.timeout = timeout;
1919
this.startTime = startTime;
2020
this.graphRunNodeOutput = {};
2121
this.auth = undefined;
2222
this.envVariables = initialEnvVars;
23-
this.caller = caller;
23+
//this.caller = caller;
2424
this.collectionPath = collectionPath;
25+
this.tab = tab;
2526
}
2627

2728
#checkTimeout() {
@@ -71,8 +72,21 @@ class Graph {
7172

7273
if (node.type === 'outputNode') {
7374
this.logger.add(LogLevel.INFO, '', { type: 'outputNode', data: { output: prevNodeOutputData } });
74-
if (this.caller === 'main') {
75-
useCanvasStore.getState().setOutputNode(node.id, prevNodeOutputData);
75+
if (this.tab) {
76+
const updatedNodes = this.nodes.map((nd) => {
77+
if (nd.id === node.id) {
78+
return {
79+
...nd,
80+
data: {
81+
...nd.data,
82+
output: prevNodeOutputData,
83+
},
84+
};
85+
}
86+
87+
return nd;
88+
});
89+
useTabStore.getState().updateFlowTestNodes(this.tab.id, updatedNodes);
7690
}
7791
result = {
7892
status: 'Success',
@@ -153,8 +167,9 @@ class Graph {
153167
this.startTime,
154168
this.envVariables,
155169
this.logger,
156-
node.type,
170+
//node.type,
157171
this.collectionPath,
172+
this.timeout,
158173
);
159174
result = await cNode.evaluate();
160175
this.envVariables = result.envVars;
@@ -228,14 +243,20 @@ class Graph {
228243
}
229244

230245
async run() {
231-
// reset every output node for a fresh run
232-
if (this.caller === 'main') {
233-
this.nodes.forEach((node) => {
246+
if (this.tab) {
247+
const updatedNodes = this.nodes.map((node) => {
234248
if (node.type === 'outputNode') {
235-
useCanvasStore.getState().unSetOutputNode(node.id);
249+
if (node.data.output) {
250+
const { ['output']: _, ...data } = node.data;
251+
node.data = data;
252+
}
236253
}
254+
255+
return node;
237256
});
257+
useTabStore.getState().updateFlowTestNodes(this.tab.id, updatedNodes);
238258
}
259+
239260
this.graphRunNodeOutput = {};
240261

241262
this.logger.add(LogLevel.INFO, 'Start Flowtest');
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import GraphLogger, { LogLevel } from './GraphLogger';
2+
import Graph from './Graph';
3+
import { useTabStore } from 'stores/TabStore';
4+
import { cloneDeep } from 'lodash';
5+
import { uploadGraphRunLogs } from 'service/collection';
6+
import { toast } from 'react-toastify';
7+
8+
const postResult = async (tab, status, time, logs) => {
9+
const response = await uploadGraphRunLogs(tab.name, status, time, logs);
10+
useTabStore.getState().updateFlowTestLogs(tab.id, status, logs, response);
11+
useTabStore.getState().updateFlowTestRunStatus(tab.id, false);
12+
if (status == 'Success') {
13+
toast.success(`FlowTest Run Success!`);
14+
} else if (status == 'Failed') {
15+
toast.error(`FlowTest Run Failed!`);
16+
}
17+
};
18+
19+
export const graphRun = async (tab, nodes, edges, timeout, collectionPath, selectedEnv) => {
20+
useTabStore.getState().updateFlowTestRunStatus(tab.id, true);
21+
22+
const startTime = Date.now();
23+
const logger = new GraphLogger();
24+
try {
25+
let envVariables = {};
26+
27+
if (selectedEnv) {
28+
envVariables = cloneDeep(selectedEnv.variables);
29+
}
30+
31+
// ============= flow =====================
32+
const g = new Graph(nodes, edges, startTime, envVariables, logger, collectionPath, timeout, tab);
33+
const result = await g.run();
34+
const time = Date.now() - startTime;
35+
logger.add(LogLevel.INFO, `Total time: ${time} ms`);
36+
37+
await postResult(tab, result.status, time, logger.get());
38+
} catch (error) {
39+
const time = Date.now() - startTime;
40+
logger.add(LogLevel.ERROR, 'Internal error running graph');
41+
logger.add(LogLevel.INFO, `Total time: ${time} ms`);
42+
await postResult(tab, 'Failed', time, logger.get());
43+
}
44+
};

src/components/molecules/flow/graph/compute/authnode.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,31 @@ import Node from './node';
33
import { LogLevel } from '../GraphLogger';
44

55
class authNode extends Node {
6-
constructor(auth, envVariables, logger) {
6+
constructor(nodeData, envVariables, logger) {
77
super('authNode');
8-
(this.auth = auth), (this.envVariables = envVariables);
8+
(this.nodeData = nodeData), (this.envVariables = envVariables);
99
this.logger = logger;
1010
}
1111

1212
evaluate() {
1313
console.log('Evaluating an auth node');
14-
if (this.auth.type === 'basic-auth') {
14+
if (this.nodeData.type === 'basic-auth') {
1515
this.logger.add(LogLevel.INFO, '', { type: 'authNode', data: { authType: 'Basic Authentication' } });
16-
this.auth.username = computeVariables(this.auth.username, this.envVariables);
17-
this.auth.password = computeVariables(this.auth.password, this.envVariables);
16+
const username = computeVariables(this.nodeData.username, this.envVariables);
17+
const password = computeVariables(this.nodeData.password, this.envVariables);
1818

19-
return this.auth;
20-
} else if (this.auth.type === 'no-auth') {
19+
return {
20+
type: 'basic-auth',
21+
username,
22+
password,
23+
};
24+
} else if (this.nodeData.type === 'no-auth') {
2125
this.logger.add(LogLevel.INFO, '', { type: 'authNode', data: { authType: 'No Authentication' } });
22-
return this.auth;
26+
return {
27+
type: 'no-auth',
28+
};
2329
} else {
24-
throw Error(`auth type: ${this.auth.type} is not valid`);
30+
throw Error(`auth type: ${this.nodeData.type} is not valid`);
2531
}
2632
}
2733
}

src/components/molecules/flow/graph/compute/nestedflownode.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import Graph1 from '../Graph';
22
import Node from './node';
33

44
class nestedFlowNode extends Node {
5-
constructor(nodes, edges, startTime, initialEnvVars, logger, caller, collectionPath) {
5+
constructor(nodes, edges, startTime, initialEnvVars, logger, collectionPath, timeout) {
66
super('flowNode');
7-
this.internalGraph = new Graph1(nodes, edges, startTime, initialEnvVars, logger, caller, collectionPath);
7+
this.internalGraph = new Graph1(nodes, edges, startTime, initialEnvVars, logger, collectionPath, timeout);
88
}
99

1010
async evaluate() {

0 commit comments

Comments
 (0)