Skip to content

Commit

Permalink
Merge pull request #150 from softflow24/feat/request-block-config-panel
Browse files Browse the repository at this point in the history
Feat(editor): Request block
  • Loading branch information
c0rtexR authored Oct 17, 2024
2 parents 0c6bc8d + 5525b4f commit 7450c80
Show file tree
Hide file tree
Showing 47 changed files with 3,800 additions and 94 deletions.
15 changes: 13 additions & 2 deletions packages/blocks/src/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ export abstract class Block implements IBlock {
readonly type: string;
inputs: Record<string, unknown>;
outputs: Record<string, unknown>;
private inputConfigs: Record<string, { type: string; required: boolean }>;
private inputConfigs: Record<
string,
{ type: string | string[]; required: boolean }
>;
private outputConfigs: Record<string, { type: string }>;
readonly retry: number;
readonly timeout: number;
Expand Down Expand Up @@ -65,7 +68,15 @@ export abstract class Block implements IBlock {
inputs[key] !== null &&
inputs[key] !== ""
) {
if (typeof inputs[key] !== config.type) {
if (
Array.isArray(config.type) &&
!config.type.includes(typeof inputs[key])
) {
invalidFields.push(key);
} else if (
!Array.isArray(config.type) &&
typeof inputs[key] !== config.type
) {
invalidFields.push(key);
} else {
cleanedInputs[key] = inputs[key];
Expand Down
14 changes: 9 additions & 5 deletions packages/blocks/src/blockFactory/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { IBlockConfig, IBlock } from "@data-river/shared/interfaces";
import { ILogger } from "@data-river/shared/interfaces/ILogger";

import { StartBlock } from "../startBlock";
import { EndBlock } from "../endBlock";
import { InputBlock } from "../inputBlock";
import { OutputBlock } from "../outputBlock";
import { LogicBlock } from "..";
import {
RequestBlock,
StartBlock,
EndBlock,
InputBlock,
OutputBlock,
LogicBlock,
} from "..";

type BlockConstructor = new (config: IBlockConfig, logger: ILogger) => IBlock;

Check warning on line 13 in packages/blocks/src/blockFactory/index.ts

View workflow job for this annotation

GitHub Actions / build

'config' is defined but never used

Check warning on line 13 in packages/blocks/src/blockFactory/index.ts

View workflow job for this annotation

GitHub Actions / build

'logger' is defined but never used

Expand All @@ -15,6 +18,7 @@ const blockRegistry: Record<string, BlockConstructor> = {
"blocks/[email protected]": InputBlock,
"blocks/[email protected]": OutputBlock,
"blocks/[email protected]": LogicBlock,
"blocks/[email protected]": RequestBlock,
};

export function createBlock(config: IBlockConfig, logger: ILogger): IBlock {
Expand Down
5 changes: 4 additions & 1 deletion packages/blocks/src/endBlock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export class EndBlock extends Block {
{
...config,
inputConfigs: {
data: { type: "string", required: false },
data: {
type: ["string", "number", "boolean", "object", "array"],
required: false,
},
},
outputConfigs: {},
},
Expand Down
1 change: 1 addition & 0 deletions packages/blocks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export * from "./endBlock";
export * from "./inputBlock";
export * from "./outputBlock";
export * from "./logicBlock";
export * from "./requestBlock";
export * from "./blockFactory";
// Note: We're not exporting DatabaseBlock here, as it's a plugin
5 changes: 4 additions & 1 deletion packages/blocks/src/outputBlock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export class OutputBlock extends Block {
{
...config,
inputConfigs: {
data: { type: "string", required: true },
data: {
type: ["string", "number", "boolean", "object", "array"],
required: true,
},
},
outputConfigs: {},
},
Expand Down
4 changes: 0 additions & 4 deletions packages/blocks/src/plugins/PluginRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,4 @@ import { registerBlockType } from "../blockFactory";

import { DatabaseBlock } from "./DatabaseBlock";

import { RequestBlock } from "./RequestBlock";

registerBlockType("database", DatabaseBlock);

registerBlockType("request", RequestBlock);
44 changes: 0 additions & 44 deletions packages/blocks/src/plugins/RequestBlock.ts

This file was deleted.

77 changes: 77 additions & 0 deletions packages/blocks/src/requestBlock/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
RequestFormData,
RequestFormSchema,
} from "@data-river/shared/contracts/blocks/request";
import axios, { AxiosRequestConfig, Method } from "axios";
import { Block } from "..";
import { IBlockConfig } from "@data-river/shared/interfaces";
import { ILogger } from "@data-river/shared/interfaces/ILogger";

export class RequestBlock extends Block {
private config: RequestFormData;

constructor(config: IBlockConfig, logger: ILogger) {
super(config, logger);
const result = RequestFormSchema.safeParse(config.config);
if (!result.success) {
throw new Error(`Invalid request configuration: ${result.error}`);
}
this.config = result.data;
}

async execute(): Promise<any> {
const { httpMethod, url, headers, queryParams, bodyType, body } =
this.config;

const axiosConfig: AxiosRequestConfig = {
method: httpMethod as Method,
url,
headers: headers?.reduce(
(acc, { key, value }) => ({ ...acc, [key]: value }),
{},
),
params: queryParams,
};

if (body) {
let contentType: string | undefined = undefined;

switch (bodyType) {
case "json":
contentType = "application/json";
axiosConfig.data = JSON.parse(body);
break;
case "form-data":
contentType = "multipart/form-data";
axiosConfig.data = body;
break;
case "x-www-form-urlencoded":
contentType = "application/x-www-form-urlencoded";
axiosConfig.data = new URLSearchParams(body);
break;
}

if (contentType) {
axiosConfig.headers = {
...axiosConfig.headers,
"Content-Type": contentType,
};
}
}

try {
const response = await axios(axiosConfig);
return {
data: response.data,
status: response.status,
statusText: response.statusText,
};
} catch (error) {
// TODO: Add option in config to accept error and handle it manually if needed.
if (axios.isAxiosError(error)) {
throw new Error(`Request failed: ${error.message}`);
}
throw error;
}
}
}
3 changes: 2 additions & 1 deletion packages/client/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ export default defineConfig({
"../execution-engine/src",
),
},
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json"],
},
optimizeDeps: {
include: ["@data-river/editor", "@data-river/shared"],
include: ["@data-river/editor", "@data-river/shared", "@data-river/blocks"],
},
build: {
commonjsOptions: {
Expand Down
1 change: 1 addition & 0 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@data-river/blocks": "workspace:*",
"@data-river/execution-engine": "workspace:*",
"@data-river/shared": "workspace:*",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.1.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { inputNode } from "./inputNode";
import { logicNode } from "./logicNode";
import { outputNode } from "./outputNode";
import { endNode } from "./endNode";
import { requestNode } from "./requestNode";

export const blockConfigs = {
start: startNode,
input: inputNode,
logic: logicNode,
output: outputNode,
end: endNode,
request: requestNode,
};

export type BlockType = keyof typeof blockConfigs;
33 changes: 33 additions & 0 deletions packages/editor/src/blocks/requestNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Node } from "reactflow";
import { RequestNodeData } from "@/types/NodeTypes";

export const requestNode: Omit<Node<RequestNodeData>, "position"> = {
id: "0",
type: "custom",
data: {
block: "[email protected]",
label: "Request",
color: "rgb(234 179 8)",
sourceHandle: true,
targetHandle: true,
icon: "Network",
config: {
httpMethod: "GET",
url: "https://pokeapi.co/api/v2/pokemon/ditto",
headers: [
{
key: "Content-Type",
value: "application/json",
},
],
bodyType: "none",
},
controls: [
{
type: "request-info",
label: "Request Info",
name: "request-info",
},
],
},
};
7 changes: 1 addition & 6 deletions packages/editor/src/components/Flow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React from "react";
import ReactFlow, {
ConnectionMode,
Background,
Expand All @@ -11,7 +11,6 @@ import "reactflow/dist/style.css";
import { useReactFlowState } from "@hooks/useReactFlowState";
import { useReactFlowHooks } from "@hooks/useReactFlowHooks";
import { useReactFlowEventHandlers } from "@hooks/useReactFlowEventHandlers";
import useEditorState from "@/hooks/useEditorState";

import CustomNode from "./CustomNode";
import CustomNodeInfo from "./CustomNodeInfo";
Expand All @@ -30,7 +29,6 @@ const FlowChart: React.FC = () => {
const { lightTheme, nodes, edges } = useReactFlowState();
const eventHandlers = useReactFlowEventHandlers();
useReactFlowHooks();
const isPanning = useEditorState((state) => state.isPanning);

return (
<div
Expand Down Expand Up @@ -80,9 +78,6 @@ const FlowChart: React.FC = () => {
<CustomNodeInfo />
</Panel>
</ReactFlow>
<span className="absolute top-20 left-20">
{isPanning ? "true" : "false"}
</span>
</div>
);
};
Expand Down
73 changes: 73 additions & 0 deletions packages/editor/src/components/MonacoEditorWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Editor, EditorProps, OnMount } from "@monaco-editor/react";
import { MemoExoticComponent, useCallback, useEffect, useState } from "react";
import { editor } from "monaco-editor";
import darkTheme from "../themes/dark.json";
import useTheme from "@data-river/shared/ui/hooks/useTheme";
import { Skeleton } from "@data-river/shared/ui";

const EditorSkeleton = () => {
return (
<div className="flex flex-col space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
);
};

const MonacoEditorWrapper = (props: EditorProps) => {
const [MonacoEditor, setMonacoEditor] = useState<
MemoExoticComponent<typeof Editor>
>(null as unknown as MemoExoticComponent<typeof Editor>);

const [editor, setEditor] = useState<editor.IStandaloneCodeEditor | null>(
null,
);

const theme = useTheme();
useEffect(() => {
import("@monaco-editor/react").then((module) => {
setMonacoEditor(
() => module.default as unknown as MemoExoticComponent<typeof Editor>,
);
});
}, []);

const handleEditorDidMount: OnMount = useCallback(
(_editor, monaco) => {
const customTheme: editor.IStandaloneThemeData = {
base: "vs-dark",
inherit: true,
colors: darkTheme.colors,
rules: [],
};

monaco.editor.defineTheme("customTheme", customTheme);
theme === "dark"
? monaco.editor.setTheme("customTheme")
: monaco.editor.setTheme("vs-light");
setEditor(_editor);
},
[theme, setEditor],
);

useEffect(() => {
editor?.updateOptions({
theme: theme === "dark" ? "customTheme" : "vs-light",
});
}, [theme, editor]);

if (!MonacoEditor) return <EditorSkeleton />;

return (
<MonacoEditor
{...props}
options={{ ...props.options }}
onMount={handleEditorDidMount}
loading={<EditorSkeleton />}
/>
);
};

export default MonacoEditorWrapper;
Loading

0 comments on commit 7450c80

Please sign in to comment.