Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔫 Manual Workflow Trigger #1097

Merged
merged 2 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions next/src/components/PrimaryButton.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import Button from "../ui/button";
import type { ReactNode } from "react";
import React from "react";
import clsx from "clsx";

type PrimaryButtonProps = {
className?: string;
children: ReactNode | string;
icon?: React.ReactNode;
onClick?: () => void;
onClick?: () => void | Promise<void>;
};

export default function PrimaryButton({ children, onClick, icon }: PrimaryButtonProps) {
export default function PrimaryButton({ children, onClick, icon, className }: PrimaryButtonProps) {
return (
<Button
onClick={onClick}
className="group rounded-full border border-black bg-white text-black transition duration-200 ease-in-out hover:hover:bg-neutral-200 focus-visible:bg-white/90 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-white/30"
className={clsx(
"group rounded-full border border-black bg-white text-black transition duration-200 ease-in-out hover:hover:bg-neutral-200 focus-visible:bg-white/90 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-white/30",
className
)}
>
{icon}
{children}
Expand Down
24 changes: 19 additions & 5 deletions next/src/components/workflow/nodes/TriggerNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ import { type NodeProps, Position } from "reactflow";
import type { WorkflowNode } from "../../../types/workflow";
import { getNodeBlockDefinitions } from "../../../services/workflow/node-block-definitions";
import AbstractNode from "./AbstractNode";
import PrimaryButton from "../../PrimaryButton";
import { useWorkflowStore } from "../../../stores/workflowStore";
import WorkflowApi from "../../../services/workflow/workflowApi";
import { useSession } from "next-auth/react";

function TriggerNode({ data, selected }: NodeProps<WorkflowNode>) {
const { data: session } = useSession();
const workflow = useWorkflowStore().workflow;
const api = new WorkflowApi(session?.accessToken);

const definition = getNodeBlockDefinitions().find((d) => d.type === data.block.type);

return (
Expand All @@ -13,11 +21,17 @@ function TriggerNode({ data, selected }: NodeProps<WorkflowNode>) {
status={data.status}
handles={[{ position: Position.Bottom, type: "source" }]}
>
<div className="flex items-center">
<div className="ml-2">
<div className="text-lg font-bold text-gray-100">{definition?.name}</div>
<div className="text-md text-sm font-thin">{definition?.description}</div>
</div>
<div className="flex flex-col">
<div className="text-lg font-bold text-gray-100">{definition?.name}</div>
<div className="text-md text-sm font-thin">{definition?.description}</div>
{workflow?.id && (
<PrimaryButton
onClick={async () => void (await api.execute(workflow?.id))}
className="mt-2 bg-orange-500 text-lg font-light"
>
<span className="text-xs">Execute</span>
</PrimaryButton>
)}
</div>
</AbstractNode>
);
Expand Down
23 changes: 15 additions & 8 deletions next/src/hooks/useWorkflow.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Edge, Node } from "reactflow";
import type { Dispatch, SetStateAction} from "react";
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useState } from "react";
import { nanoid } from "nanoid";
import { useMutation, useQuery } from "@tanstack/react-query";
Expand All @@ -9,6 +9,7 @@ import WorkflowApi from "../services/workflow/workflowApi";
import useSocket from "./useSocket";
import { z } from "zod";
import type { Session } from "next-auth";
import { useWorkflowStore } from "../stores/workflowStore";

const eventSchema = z.object({
nodeId: z.string(),
Expand Down Expand Up @@ -49,9 +50,19 @@ export const useWorkflow = (workflowId: string, session: Session | null) => {
async (data: Workflow) => await api.update(workflowId, data)
);

const { data: workflow } = useQuery(
const workflowStore = useWorkflowStore();

useQuery(
["workflow", workflowId],
async () => await api.get(workflowId),
async () => {
const workflow = await api.get(workflowId);

workflowStore.setWorkflow(workflow);
setNodes(workflow?.nodes.map(toReactFlowNode) ?? []);
setEdges(workflow?.edges.map(toReactFlowEdge) ?? []);

return workflow;
},
{
enabled: !!workflowId && !!session?.accessToken,
}
Expand All @@ -62,11 +73,6 @@ export const useWorkflow = (workflowId: string, session: Session | null) => {
const [nodes, setNodes] = nodesModel;
const [edges, setEdges] = edgesModel;

useEffect(() => {
setNodes(workflow?.nodes.map(toReactFlowNode) ?? []);
setEdges(workflow?.edges.map(toReactFlowEdge) ?? []);
}, [setNodes, setEdges, workflow]);

useEffect(() => {
const selectedNodes = nodes.filter((n) => n.selected);
if (selectedNodes.length == 0) setSelectedNode(undefined);
Expand Down Expand Up @@ -121,6 +127,7 @@ export const useWorkflow = (workflowId: string, session: Session | null) => {

const onSave = async () => {
await updateWorkflow({
id: workflowId,
nodes: nodes.map((n) => ({
id: n.data.id,
ref: n.data.ref,
Expand Down
10 changes: 1 addition & 9 deletions next/src/pages/workflow/[workflow].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import nextI18NextConfig from "../../../next-i18next.config";
import { getWorkflowSidebar } from "../../components/drawer/WorkflowSidebar";
import { useAuth } from "../../hooks/useAuth";
import PrimaryButton from "../../components/PrimaryButton";
import { FaPlay, FaSave } from "react-icons/fa";
import { FaSave } from "react-icons/fa";

const WorkflowPage: NextPage = () => {
const { session } = useAuth({ protectedRoute: true });
Expand Down Expand Up @@ -53,14 +53,6 @@ const WorkflowPage: NextPage = () => {
>
Save
</PrimaryButton>
<PrimaryButton
icon={<FaPlay size="15" />}
onClick={() => {
executeWorkflow().catch(console.error);
}}
>
Execute
</PrimaryButton>
</div>
</div>
</DashboardLayout>
Expand Down
16 changes: 15 additions & 1 deletion next/src/services/workflow/node-block-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,22 @@ const IfBlockDefinition: NodeBlockDefinition = {
output_fields: [],
};

const TriggerBlockDefinition: NodeBlockDefinition = {
name: "Manual Trigger",
type: "ManualTriggerBlock",
description: "Trigger a block manually",
image_url: "/tools/web.png",
input_fields: [],
output_fields: [],
};

export const getNodeBlockDefinitions = () => {
return [UrlStatusCheckBlockDefinition, SlackWebhookBlockDefinition, IfBlockDefinition];
return [
UrlStatusCheckBlockDefinition,
SlackWebhookBlockDefinition,
IfBlockDefinition,
TriggerBlockDefinition,
];
};

export const getNodeBlockDefinitionFromNode = (node: Node<WorkflowNode>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const createAgentInputSlice: StateCreator<AgentInputSlice> = (set) => {
},
};
};

export const useAgentInputStore = createSelectors(
create<AgentInputSlice>()((...a) => ({
...createAgentInputSlice(...a),
Expand Down
33 changes: 33 additions & 0 deletions next/src/stores/workflowStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createSelectors } from "./helpers";
import type { StateCreator } from "zustand";
import { create } from "zustand";

interface Workflow {
id: string;
}

interface WorkflowSlice {
workflow: Workflow | null;
setWorkflow: (workflow: Workflow) => void;
}

const initialState = {
workflow: null,
};

const createWorkflowSlice: StateCreator<WorkflowSlice> = (set, get) => {
return {
...initialState,
setWorkflow: (workflow: Workflow) => {
set(() => ({
workflow,
}));
},
};
};

export const useWorkflowStore = createSelectors(
create<WorkflowSlice>()((...a) => ({
...createWorkflowSlice(...a),
}))
);
3 changes: 3 additions & 0 deletions next/src/types/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const WorkflowEdgeSchema = z.object({
status: z.enum(["running", "success", "failure"]).optional(),
});
export const WorkflowSchema = z.object({
id: z.string(),
nodes: z.array(WorkflowNodeSchema),
edges: z.array(WorkflowEdgeSchema),
});
Expand Down Expand Up @@ -57,6 +58,8 @@ export const toReactFlowEdge = (edge: WorkflowEdge) =>

export const getNodeType = (block: NodeBlock) => {
switch (block.type) {
case "ManualTriggerBlock":
return "trigger";
case "IfBlock":
return "if";
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from reworkd_platform.schemas.workflow.base import Block, BlockIOBase


class ManualTriggerBlock(Block):
type = "ManualTriggerBlock"
description = "Outputs the status code of a GET request to a URL"
image_url = ""

async def run(self) -> BlockIOBase:
return BlockIOBase()
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from reworkd_platform.schemas.workflow.base import Block
from reworkd_platform.schemas.workflow.blocks.manual_trigger import ManualTriggerBlock
from reworkd_platform.schemas.workflow.blocks.slack_webhook import SlackWebhook
from reworkd_platform.schemas.workflow.blocks.status_check import UrlStatusCheckBlock


def get_block_runner(block: Block) -> Block:
if block.type == "ManualTriggerBlock":
return ManualTriggerBlock(**block.dict())
if block.type == "UrlStatusCheck":
return UrlStatusCheckBlock(**block.dict())
if block.type == "SlackWebhook":
Expand Down
Loading