Skip to content

Commit

Permalink
Merge branch 'main' into GlebBerjoskin/push_notification_update
Browse files Browse the repository at this point in the history
  • Loading branch information
GlebBerjoskin committed Sep 1, 2024
2 parents 689241a + f65e85b commit 96e6861
Show file tree
Hide file tree
Showing 28 changed files with 2,706 additions and 329 deletions.
5 changes: 0 additions & 5 deletions .github/workflows/test-pr-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,6 @@ jobs:
# create the state directory
# mkdir -p ./state && chown -R root:root ./state && chmod -R 777 ./state
- name: Testing if API docs are actual
run: |
# Keeping this test here to avoid spinning Keep's backend it in the docs-test job
./scripts/docs_validate_openapi_is_actual.sh
- name: Run e2e tests and report coverage
run: |
poetry run coverage run --branch -m pytest -s tests/e2e_tests/
Expand Down
1 change: 1 addition & 0 deletions docs/providers/documentation/jira-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The `notify` function take following parameters as inputs:
- `summary` (required): Incident/issue name or short description.
- `description` (optional): Additional details related to the incident/issue.
- `issue_type` (optional): Issue type name. For example: `Story`, `Bug` etc
- `issue_id` (optional): When you want to update an existing issue, provide the issue id.

## Outputs

Expand Down
15 changes: 15 additions & 0 deletions examples/workflows/update_jira_ticket.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
workflow:
id: update-jira-ticket
triggers:
- type: manual
actions:
- name: jira-action
provider:
config: '{{ providers.Jira }}'
type: jira
with:
board_name: ''
description: Update description of an issue
issue_id: 10023
project_key: ''
summary: Update summary of an issue
49 changes: 49 additions & 0 deletions keep-ui/app/workflows/builder/BuilderChanagesTracker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react'
import useStore from './builder-store';
import { Button } from '@tremor/react';
import { reConstructWorklowToDefinition } from 'utils/reactFlow';

export default function BuilderChanagesTracker({onDefinitionChange}:{onDefinitionChange:(def: Record<string,any>) => void}) {
const {
nodes,
edges,
setEdges,
setNodes,
isLayouted,
setIsLayouted,
v2Properties,
changes,
setChanges,
lastSavedChanges,
setLastSavedChanges
} = useStore();
const handleDiscardChanges = (e: React.MouseEvent<HTMLButtonElement>) => {
if(!isLayouted) return;
setEdges(lastSavedChanges.edges || []);
setNodes(lastSavedChanges.nodes || []);
setChanges(0);
setIsLayouted(false);
}

const handleSaveChanges = (e: React.MouseEvent<HTMLButtonElement>) =>{
e.preventDefault();
e.stopPropagation();
setLastSavedChanges({nodes: nodes, edges: edges});
const value = reConstructWorklowToDefinition({nodes: nodes, edges: edges, properties: v2Properties});
onDefinitionChange(value);
setChanges(0);
}

return (
<div className='flex gap-2.5'>
<Button
onClick={handleDiscardChanges}
disabled={changes === 0}
>Discard{changes ? `(${changes})`: ""}</Button>
<Button
onClick={handleSaveChanges}
disabled={changes === 0}
>Save</Button>
</div>
)
}
124 changes: 124 additions & 0 deletions keep-ui/app/workflows/builder/CustomEdge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from "react";
import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from "@xyflow/react";
import type { Edge, EdgeProps } from "@xyflow/react";
import useStore from "./builder-store";
import { PlusIcon } from "@radix-ui/react-icons";
import { Button } from "@tremor/react";
import '@xyflow/react/dist/style.css';

interface CustomEdgeProps extends EdgeProps {
label?: string;
type?: string;
data?: any;
}

const CustomEdge: React.FC<CustomEdgeProps> = ({
id,
sourceX,
sourceY,
targetX,
targetY,
label,
source,
target,
data,
style,
}: CustomEdgeProps) => {
const { deleteEdges, edges, setSelectedEdge, selectedEdge } = useStore();

// Calculate the path and midpoint
const [edgePath, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
borderRadius: 10,
});

const midpointX = (sourceX + targetX) / 2;
const midpointY = (sourceY + targetY) / 2;

let dynamicLabel = label;
const isLayouted = !!data?.isLayouted;
let showAddButton = !source?.includes('empty') && !target?.includes('trigger_end') && source !== 'start';

if (!showAddButton) {
showAddButton = target?.includes('trigger_end') && source?.includes("trigger_start");
}

const color = dynamicLabel === "True" ? "left-0 bg-green-500" : dynamicLabel === "False" ? "bg-red-500" : "bg-orange-500";
return (
<>
<BaseEdge
id={id}
path={edgePath}
style={{
opacity: isLayouted ? 1 : 0,
...style,
strokeWidth: 2,
}}

/>
<defs>
<marker
id={`arrow-${id}`}
markerWidth="15"
markerHeight="15"
refX="10"
refY="5"
orient="auto"
markerUnits="strokeWidth"
>
<path
d="M 0,0 L 10,5 L 0,10 L 3,5 Z"
fill="currentColor"
className="text-gray-500 font-extrabold" // Tailwind class for arrow color
style={{ opacity: isLayouted ? 1 : 0 }}
/>
</marker>
</defs>
<BaseEdge
id={id}
path={edgePath}
className="stroke-gray-700 stroke-2"
style={{
markerEnd: `url(#arrow-${id})`,
opacity: isLayouted ? 1 : 0
}} // Add arrowhead
/>
<EdgeLabelRenderer>
{!!dynamicLabel && (
<div
className={`absolute ${color} text-white rounded px-3 py-1 border border-gray-700`}
style={{
transform: `translate(-50%, -50%) translate(${dynamicLabel === "True" ? labelX - 45 : labelX + 48}px, ${labelY}px)`,
pointerEvents: "none",
opacity: isLayouted ? 1 : 0
}}
>
{dynamicLabel}
</div>
)}
{showAddButton && <Button
style={{
position: "absolute",
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
pointerEvents: "all",
opacity: isLayouted ? 1 : 0
}}
className={`p-0 m-0 bg-transparent text-transparent border-none`}
// tooltip="Add node"
onClick={(e) => {
setSelectedEdge(id);
}}
>
<PlusIcon
className={`size-7 hover:text-black rounded text-sm bg-white border text-gray-700 ${selectedEdge === id ? "border-2 border-orange-500" : "border-gray-700"
}`}
/> </Button>}
</EdgeLabelRenderer>
</>
);
};

export default CustomEdge;
170 changes: 170 additions & 0 deletions keep-ui/app/workflows/builder/CustomNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import React, { memo } from "react";
import { Handle, Position } from "@xyflow/react";
import NodeMenu from "./NodeMenu";
import useStore, { FlowNode, V2Step } from "./builder-store";
import Image from "next/image";
import { GoPlus } from "react-icons/go";
import { MdNotStarted } from "react-icons/md";
import { GoSquareFill } from "react-icons/go";
import { PiDiamondsFourFill, PiSquareLogoFill } from "react-icons/pi";
import { BiSolidError } from "react-icons/bi";
import { FaHandPointer } from "react-icons/fa";
import { toast } from "react-toastify";



function IconUrlProvider(data: FlowNode["data"]) {
const { componentType, type } = data || {};
if (type === "alert" || type === "workflow" || type === "trigger" || !type) return "/keep.png";
return `/icons/${type
?.replace("step-", "")
?.replace("action-", "")
?.replace("__end", "")
?.replace("condition-", "")}-icon.png`;
}

function CustomNode({ id, data }: FlowNode) {
const { selectedNode, setSelectedNode, setOpneGlobalEditor, errorNode, synced } = useStore();
const type = data?.type
?.replace("step-", "")
?.replace("action-", "")
?.replace("condition-", "")
?.replace("__end", "")
?.replace("trigger_", "");

const isEmptyNode = !!data?.type?.includes("empty");
const specialNodeCheck = ['start', 'end', 'trigger_end', 'trigger_start'].includes(type)

function getTriggerIcon(step: any) {
const { type } = step;
switch (type) {
case "manual":
return <FaHandPointer size={32} />
case "interval":
return <PiDiamondsFourFill size={32} />
}
}

return (
<>
{!specialNodeCheck && <div
className={`p-2 flex shadow-md rounded-md bg-white border-2 w-full h-full ${id === selectedNode
? "border-orange-500"
: "border-stone-400"
}`}
onClick={(e) => {
e.stopPropagation();
if(!synced){
toast('Please save the previous step or wait while properties sync with the workflow.');
return;
}
if (type === 'start' || type === 'end' || id?.includes('end') || id?.includes('empty')) {
if (id?.includes('empty')) {
setSelectedNode(id);
}
setOpneGlobalEditor(true);
return;
}
setSelectedNode(id);
}}
style={{
opacity: data.isLayouted ? 1 : 0,
borderStyle: isEmptyNode ? 'dashed' : "",
borderColor: errorNode == id ? 'red' : ''
}}
>
{isEmptyNode && (
<div className="flex-1 flex flex-col items-center justify-center">
<GoPlus className="w-8 h-8 text-gray-600 font-bold p-0" />
{selectedNode === id && (
<div className="text-gray-600 font-bold text-center">Go to Toolbox</div>
)}
</div>
)}
{errorNode === id && <BiSolidError className="size-16 text-red-500 absolute right-[-40px] top-[-40px]" />}
{!isEmptyNode && (
<div className="container flex-1 flex flex-row items-center justify-between gap-2 flex-wrap">
{getTriggerIcon(data)}
{!!data && !['interval', 'manual'].includes(data.type) && <Image
src={IconUrlProvider(data) || "/keep.png"}
alt={data?.type}
className="object-cover w-8 h-8 rounded-full bg-gray-100"
width={32}
height={32}
/>}
<div className="flex-1 flex-col gap-2 flex-wrap truncate">
<div className="text-lg font-bold truncate">{data?.name}</div>
<div className="text-gray-500 truncate">
{type}
</div>
</div>
<div>
<NodeMenu data={data} id={id} />
</div>
</div>
)}

<Handle
type="target"
position={Position.Top}
className="w-32"
/>
<Handle
type="source"
position={Position.Bottom}
className="w-32"
/>
</div>}

{specialNodeCheck && <div
style={{
opacity: data.isLayouted ? 1 : 0
}}
onClick={(e) => {
e.stopPropagation();
if(!synced){
toast('Please save the previous step or wait while properties sync with the workflow.');
return;
}
if (type === 'start' || type === 'end' || id?.includes('end')) {
setOpneGlobalEditor(true);
return;
}
setSelectedNode(id);
}}
>
<div className={`flex flex-col items-center justify-center`}>
{type === 'start' && <MdNotStarted className="size-20 bg-orange-500 text-white rounded-full font-bold mb-2" />}
{type === 'end' && <GoSquareFill className="size-20 bg-orange-500 text-white rounded-full font-bold mb-2" />}
{['threshold', 'assert', 'foreach'].includes(type) &&
<div className={`border-2 ${id === selectedNode
? "border-orange-500"
: "border-stone-400"}`}>
{id.includes('end') ? <PiSquareLogoFill className="size-20 rounded bg-white-400 p-2" /> :
<Image
src={IconUrlProvider(data) || "/keep.png"}
alt={data?.type}
className="object-contain size-20 rounded bg-white-400 p-2"
width={32}
height={32}
/>}
</div>
}
{'start' === type && <Handle
type="source"
position={Position.Bottom}
className="w-32"
/>}

{'end' === type && <Handle
type="target"
position={Position.Top}
className="w-32"
/>}
</div>
</div>}
</>
);
}

export default memo(CustomNode);
Loading

0 comments on commit 96e6861

Please sign in to comment.