From 9fdcd9594cb7309f57f3303aeff063d5205f6bd9 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 16 Oct 2024 04:23:20 +0530 Subject: [PATCH 1/2] fix: refactor the workflow execution component to reduce repaint/render the entire page when filters were applied and some minor style changes --- .../workflows/[workflow_id]/executions.tsx | 274 +++++++++++------- .../app/workflows/[workflow_id]/layout.tsx | 6 +- .../workflows/[workflow_id]/side-nav-bar.tsx | 153 ++++++---- .../app/workflows/builder/builder-modal.tsx | 31 +- keep-ui/components/ui/discolsure-section.tsx | 73 +++-- keep/api/routes/workflows.py | 44 +++ 6 files changed, 392 insertions(+), 189 deletions(-) diff --git a/keep-ui/app/workflows/[workflow_id]/executions.tsx b/keep-ui/app/workflows/[workflow_id]/executions.tsx index 5aaed36be0..5477cdf5b3 100644 --- a/keep-ui/app/workflows/[workflow_id]/executions.tsx +++ b/keep-ui/app/workflows/[workflow_id]/executions.tsx @@ -22,10 +22,13 @@ import { JSON_SCHEMA, load } from "js-yaml"; import { ExecutionTable } from "./workflow-execution-table"; import SideNavBar from "./side-nav-bar"; import { useWorkflowRun } from "utils/hooks/useWorkflowRun"; -import BuilderWorkflowTestRunModalContent from "../builder/builder-workflow-testrun-modal"; -import Modal from "react-modal"; import { TableFilters } from "./table-filters"; import AlertTriggerModal from "../workflow-run-with-alert-modal"; +import useSWR from "swr"; +import { getApiURL } from "@/utils/apiUrl"; +import { fetcher } from "@/utils/fetcher"; +import BuilderModalContent from "../builder/builder-modal"; +import PageClient from "../builder/page.client"; const tabs = [ { name: "All Time", value: "alltime" }, @@ -85,14 +88,7 @@ interface Pagination { offset: number; } -export default function WorkflowDetailPage({ - params, -}: { - params: { workflow_id: string }; -}) { - const router = useRouter(); - const { data: session, status, update } = useSession(); - +const OverViewContent = ({ workflow_id }: { workflow_id: string }) => { const [executionPagination, setExecutionPagination] = useState({ limit: 25, offset: 0, @@ -108,7 +104,7 @@ export default function WorkflowDetailPage({ }, [tab, searchParams]); const { data, isLoading, error } = useWorkflowExecutionsV2( - params.workflow_id, + workflow_id, tab, executionPagination.limit, executionPagination.offset @@ -122,7 +118,7 @@ export default function WorkflowDetailPage({ message, } = useWorkflowRun(data?.workflow!); - if (isLoading || !data) return ; + if (isLoading) return ; if (error) { return ( @@ -136,8 +132,6 @@ export default function WorkflowDetailPage({ ); } - if (status === "loading" || isLoading || !data) return ; - if (status === "unauthenticated") router.push("/signin"); const parsedWorkflowFile = load(data?.workflow?.workflow_raw ?? "", { schema: JSON_SCHEMA, @@ -153,98 +147,178 @@ export default function WorkflowDetailPage({ } }; - const workflow = { last_executions: data.items } as Partial; + const workflow = { last_executions: data?.items } as Partial; + return ( <> - - -
-
-
- {/*TO DO update searchParams for these filters*/} - -
- {!!data.workflow && ( - - )} -
- {data?.items && ( -
-
- - Total Executions -
-

- {formatNumber(data.count ?? 0)} -

-
-
- - Pass / Fail ratio -
-

- {formatNumber(data.passCount)} - {"/"} - {formatNumber(data.failCount)} -

-
-
- - Success % -
-

- {(data.count - ? (data.passCount / data.count) * 100 - : 0 - ).toFixed(2)} - {"%"} -

-
-
- - Avg. duration -
-

- {(data.avgDuration ?? 0).toFixed(2)} -

-
-
- - Involved Services - - +
+
+ {/*TO DO update searchParams for these filters*/} + +
+ {!!data?.workflow && ( + + )} +
+ {data?.items && ( +
+
+ + Total Executions +
+

+ {formatNumber(data.count ?? 0)} +

- -

Execution History

- - -
- )} + + + Pass / Fail ratio +
+

+ {formatNumber(data.passCount)} + {"/"} + {formatNumber(data.failCount)} +

+
+
+ + Success % +
+

+ {(data.count + ? (data.passCount / data.count) * 100 + : 0 + ).toFixed(2)} + {"%"} +

+
+
+ + Avg. duration +
+

+ {(data.avgDuration ?? 0).toFixed(2)} +

+
+
+ + Involved Services + + +
+ +

Execution History

+ +
- - {!!data.workflow && !!getTriggerModalProps && ( + )} + {!!data?.workflow && !!getTriggerModalProps && ( )} ); +}; + +export default function WorkflowDetailPage({ + params, +}: { + params: { workflow_id: string }; +}) { + const router = useRouter(); + const { data: session, status } = useSession(); + const [navlink, setNavLink] = useState("overview"); + + const apiUrl = getApiURL(); + + const { + data: workflow, + isLoading, + error, + } = useSWR>( + () => (session ? `${apiUrl}/workflows/${params.workflow_id}/data` : null), + (url: string) => fetcher(url, session?.accessToken) + ); + + // Render loading state if session is loading + if (status === "loading") return ; + + // Redirect if user is not authenticated + if (status === "unauthenticated") { + useEffect(() => { + router.push("/signin"); + }, [router]); + return null; + } + + // Handle error state for fetching workflow data + if (isLoading) return ; + if (error) { + return ( + + Failed to load workflow + + ); + } + + if (!workflow) { + return null; + } + + return ( + + +
+ {navlink === "overview" && ( + + )} + {navlink === "builder" && ( +
+ +
+ )} +
+ {navlink === "view_yaml" && ( + {}} + compiledAlert={workflow.workflow_raw!} + id={workflow.id} + hideClose={true} + /> + )} +
+
+
+ ); } diff --git a/keep-ui/app/workflows/[workflow_id]/layout.tsx b/keep-ui/app/workflows/[workflow_id]/layout.tsx index f374dfdf1d..427aefd894 100644 --- a/keep-ui/app/workflows/[workflow_id]/layout.tsx +++ b/keep-ui/app/workflows/[workflow_id]/layout.tsx @@ -2,7 +2,6 @@ import { ArrowLeftIcon } from "@radix-ui/react-icons"; import Link from "next/link"; - export default function Layout({ children, params, @@ -10,18 +9,17 @@ export default function Layout({ children: any; params: { workflow_id: string }; }) { - return ( <> -
+
Back to Workflows +
{children}
-
{children}
); } diff --git a/keep-ui/app/workflows/[workflow_id]/side-nav-bar.tsx b/keep-ui/app/workflows/[workflow_id]/side-nav-bar.tsx index d119778c70..8e35b3252c 100644 --- a/keep-ui/app/workflows/[workflow_id]/side-nav-bar.tsx +++ b/keep-ui/app/workflows/[workflow_id]/side-nav-bar.tsx @@ -1,58 +1,109 @@ -import React, { useState } from 'react'; -import { CiUser } from 'react-icons/ci'; -import { FaSitemap } from 'react-icons/fa'; -import { AiOutlineSwap } from 'react-icons/ai'; -import { Workflow } from '../models'; +import React, { useEffect } from "react"; +import { CiUser } from "react-icons/ci"; +import { FaSitemap } from "react-icons/fa"; +import { AiOutlineSwap } from "react-icons/ai"; +import { Workflow } from "../models"; import { Text } from "@tremor/react"; -import { DisclosureSection } from '@/components/ui/discolsure-section'; -import { useWorkflowRun } from 'utils/hooks/useWorkflowRun'; -import Modal from 'react-modal'; -import BuilderWorkflowTestRunModalContent from '../builder/builder-workflow-testrun-modal'; -import BuilderModalContent from '../builder/builder-modal'; +import { DisclosureSection } from "@/components/ui/discolsure-section"; +import { useRouter, useSearchParams } from "next/navigation"; +import router from "next/router"; -export default function SideNavBar({ workflow }: { workflow: Workflow }) { - const [viewYaml, setviewYaml] = useState(false); +export default function SideNavBar({ + workflow, + handleLink, + navLink, +}: { + navLink: string; + workflow: Partial; + handleLink: React.Dispatch>; +}) { + const searchParams = useSearchParams(); + const router = useRouter(); - const analyseLinks = [ - { href: `/workflows/${workflow.id}`, icon: AiOutlineSwap, label: 'Overview', isLink: true }, - ]; + const analyseLinks = [ + { + href: "#overview", + icon: AiOutlineSwap, + label: "Overview", + key: "overview", + isLink: false, + handleClick: () => { + handleLink("overview"); + }, + }, + ]; - const manageLinks = [ - { href: `/workflows/builder/${workflow.id}`, icon: FaSitemap, label: 'Workflow Builder', isLink: true }, - { href: `/workflows/builder/${workflow.id}`, icon: CiUser, label: 'Workflow YAML definition', isLink: false, handleClick: () => { setviewYaml(true) } }, - ]; + const manageLinks = [ + { + href: `#builder`, + icon: FaSitemap, + label: "Workflow Builder", + key: "builder", + isLink: false, + handleClick: () => { + handleLink("builder"); + }, + }, + { + href: `#view_yaml`, + icon: CiUser, + label: "Workflow YAML definition", + key: "view_yaml", + isLink: false, + handleClick: () => { + handleLink("view_yaml"); + }, + }, + ]; - const learnLinks = [ - { href: `https://www.youtube.com/@keepalerting`, icon: FaSitemap, label: 'Tutorials', isLink: true , newTab:true}, - { href: `https://docs.keephq.dev`, icon: CiUser, label: 'Documentation', isLink: true, newTab: true }, - ]; + const learnLinks = [ + { + href: `https://www.youtube.com/@keepalerting`, + icon: FaSitemap, + label: "Tutorials", + isLink: true, + newTab: true, + key: "tutorials", + }, + { + href: `https://docs.keephq.dev`, + icon: CiUser, + label: "Documentation", + isLink: true, + newTab: true, + key: "documentation", + }, + ]; - return ( -
-
-

{workflow.name}

- {workflow.description && ( - - {workflow.description} - - )} -
-
- - - -
- { setviewYaml(false); }} - className="bg-gray-50 p-4 md:p-10 mx-auto max-w-7xl mt-20 border border-orange-600/50 rounded-md" - > - { setviewYaml(false) }} - compiledAlert={workflow.workflow_raw!} - id={workflow.id} - /> - -
- ); + return ( +
+
+

+ {workflow.name} +

+ {workflow.description && ( + + {workflow.description} + + )} +
+
+ + + +
+
+ ); } diff --git a/keep-ui/app/workflows/builder/builder-modal.tsx b/keep-ui/app/workflows/builder/builder-modal.tsx index f58abd1402..949afa961c 100644 --- a/keep-ui/app/workflows/builder/builder-modal.tsx +++ b/keep-ui/app/workflows/builder/builder-modal.tsx @@ -11,13 +11,15 @@ import { downloadFileFromString } from "./utils"; interface Props { closeModal: () => void; compiledAlert: Alert | string | null; - id?:string + id?: string; + hideClose?: boolean; } export default function BuilderModalContent({ closeModal, compiledAlert, id, + hideClose, }: Props) { const [isLoading, setIsLoading] = useState(true); @@ -26,10 +28,13 @@ export default function BuilderModalContent({ setIsLoading(false); }, Math.floor(Math.random() * 2500 + 1000)); - const alertYaml = typeof compiledAlert !== 'string' ? stringify(compiledAlert) : compiledAlert; + const alertYaml = + typeof compiledAlert !== "string" + ? stringify(compiledAlert) + : compiledAlert; function download() { - const fileName = typeof compiledAlert == 'string' ? id : compiledAlert!.id; + const fileName = typeof compiledAlert == "string" ? id : compiledAlert!.id; downloadFileFromString(alertYaml, `${fileName}.yaml`); } @@ -52,15 +57,17 @@ export default function BuilderModalContent({ Keep alert specification ready to use
- + {!hideClose && ( + + )}
diff --git a/keep-ui/components/ui/discolsure-section.tsx b/keep-ui/components/ui/discolsure-section.tsx index 8be3ab7edc..318d17fb6b 100644 --- a/keep-ui/components/ui/discolsure-section.tsx +++ b/keep-ui/components/ui/discolsure-section.tsx @@ -1,24 +1,31 @@ -import { Disclosure } from '@headlessui/react'; -import classNames from 'classnames'; -import { IoChevronUp } from 'react-icons/io5'; +import { Disclosure } from "@headlessui/react"; +import classNames from "classnames"; +import { IoChevronUp } from "react-icons/io5"; import { Title, Subtitle, Button } from "@tremor/react"; -import { LinkWithIcon } from '@/components/LinkWithIcon'; -import React from 'react'; -import { IconType } from 'react-icons'; +import { LinkWithIcon } from "@/components/LinkWithIcon"; +import React from "react"; +import { IconType } from "react-icons"; interface DisclosureSectionProps { title: string; + activeLink: string; links: Array<{ href: string; icon: IconType | React.ReactNode; label: string; isLink: boolean; - handleClick?: (e:any) => void; + handleClick?: (e: any) => void; newTab?: boolean; + active?: boolean; + key: string; }>; } -export function DisclosureSection({ title, links }: DisclosureSectionProps) { +export function DisclosureSection({ + title, + links, + activeLink, +}: DisclosureSectionProps) { return ( {({ open }) => ( @@ -29,8 +36,8 @@ export function DisclosureSection({ title, links }: DisclosureSectionProps) {
@@ -38,18 +45,40 @@ export function DisclosureSection({ title, links }: DisclosureSectionProps) { as="ul" className="space-y-2 overflow-auto min-w-[max-content] py-2 pr-4" > - {links.map((link, index) => ( -
  • - - {}}>{link.label} - -
  • - ))} + {links.map((link, index) => { + const CustomIcon = link.icon as IconType; + return ( +
  • + {link.isLink && ( + + {}} + > + {link.label} + + + )} + {!link.isLink && ( +
    +
    + + {link.label} +
    +
    + )} +
  • + ); + })} )} diff --git a/keep/api/routes/workflows.py b/keep/api/routes/workflows.py index 732aeeb1f0..84afa437d7 100644 --- a/keep/api/routes/workflows.py +++ b/keep/api/routes/workflows.py @@ -483,6 +483,50 @@ def get_raw_workflow_by_id( ) }, ) + +@router.get("/{workflow_id}/data", description="Get workflow by ID") +def get_workflow_by_id( + workflow_id: str, + authenticated_entity: AuthenticatedEntity = Depends( + IdentityManagerFactory.get_auth_verifier(["read:workflows"]) + ), +): + tenant_id = authenticated_entity.tenant_id + # get all workflow + workflow = get_workflow(tenant_id=tenant_id, workflow_id=workflow_id) + + if not workflow: + logger.warning( + f"Tenant tried to get workflow {workflow_id} that does not exist", + extra={"tenant_id": tenant_id}, + ) + raise HTTPException(404, "Workflow not found") + + try: + workflow_yaml = yaml.safe_load(workflow.workflow_raw) + valid_workflow_yaml = {"workflow": workflow_yaml} + final_workflow_raw = yaml.dump(valid_workflow_yaml) + workflow_dto = WorkflowDTO( + id=workflow.id, + name=workflow.name, + description=workflow.description + or "[This workflow has no description]", + created_by=workflow.created_by, + creation_time=workflow.creation_time, + interval=workflow.interval, + providers=[], + triggers=[], + workflow_raw=final_workflow_raw, + revision=workflow.revision, + last_updated=workflow.last_updated, + disabled=workflow.is_disabled, + provisioned=workflow.provisioned, + ) + return workflow_dto + except Exception as e: + logger.error(f"Error fetching workflow meta data: {e}") + raise HTTPException(status_code=500, detail="Error fetching workflow meta data") + @router.get("/executions", description="Get workflow executions by alert fingerprint") From ce373022128f9cbc8e6a280ae56d4d86b2250f13 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 16 Oct 2024 04:46:16 +0530 Subject: [PATCH 2/2] chore: resolve the bad code --- keep-ui/app/workflows/[workflow_id]/executions.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/keep-ui/app/workflows/[workflow_id]/executions.tsx b/keep-ui/app/workflows/[workflow_id]/executions.tsx index 5477cdf5b3..dfc7d95978 100644 --- a/keep-ui/app/workflows/[workflow_id]/executions.tsx +++ b/keep-ui/app/workflows/[workflow_id]/executions.tsx @@ -263,12 +263,7 @@ export default function WorkflowDetailPage({ if (status === "loading") return ; // Redirect if user is not authenticated - if (status === "unauthenticated") { - useEffect(() => { - router.push("/signin"); - }, [router]); - return null; - } + if (status === "unauthenticated") router.push("/signin"); // Handle error state for fetching workflow data if (isLoading) return ;