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

fix: refactor the workflow execution component to reduce repaint/rend… #2211

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
269 changes: 169 additions & 100 deletions keep-ui/app/workflows/[workflow_id]/executions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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<Pagination>({
limit: 25,
offset: 0,
Expand All @@ -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
Expand All @@ -122,7 +118,7 @@ export default function WorkflowDetailPage({
message,
} = useWorkflowRun(data?.workflow!);

if (isLoading || !data) return <Loading />;
if (isLoading) return <Loading />;

if (error) {
return (
Expand All @@ -136,8 +132,6 @@ export default function WorkflowDetailPage({
</Callout>
);
}
if (status === "loading" || isLoading || !data) return <Loading />;
if (status === "unauthenticated") router.push("/signin");

const parsedWorkflowFile = load(data?.workflow?.workflow_raw ?? "", {
schema: JSON_SCHEMA,
Expand All @@ -153,98 +147,173 @@ export default function WorkflowDetailPage({
}
};

const workflow = { last_executions: data.items } as Partial<Workflow>;
const workflow = { last_executions: data?.items } as Partial<Workflow>;

return (
<>
<Card className="relative flex p-4 w-full gap-3">
<SideNavBar workflow={data.workflow} />
<div className="relative overflow-auto p-0.5 flex-1 flex-shrink-1">
<div className="sticky top-0 flex justify-between items-end">
<div className="flex-1">
{/*TO DO update searchParams for these filters*/}
<FilterTabs tabs={tabs} setTab={setTab} tab={tab} />
</div>
{!!data.workflow && (
<Button
disabled={isRunning || isRunButtonDisabled}
className="p-2 px-4"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
handleRunClick?.();
}}
tooltip={message}
>
Run now
</Button>
)}
</div>
{data?.items && (
<div className="mt-2 flex flex-col gap-2">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 p-0.5">
<StatsCard data={`${data.count ?? 0}`}>
<Title>Total Executions</Title>
<div>
<h1 className="text-2xl font-bold">
{formatNumber(data.count ?? 0)}
</h1>
</div>
</StatsCard>
<StatsCard data={`${data.passCount}/${data.failCount}`}>
<Title>Pass / Fail ratio</Title>
<div>
<h1 className="text-2xl font-bold">
{formatNumber(data.passCount)}
{"/"}
{formatNumber(data.failCount)}
</h1>
</div>
</StatsCard>
<StatsCard>
<Title>Success %</Title>
<div>
<h1 className="text-2xl font-bold">
{(data.count
? (data.passCount / data.count) * 100
: 0
).toFixed(2)}
{"%"}
</h1>
</div>
</StatsCard>
<StatsCard>
<Title>Avg. duration</Title>
<div>
<h1 className="text-2xl font-bold">
{(data.avgDuration ?? 0).toFixed(2)}
</h1>
</div>
</StatsCard>
<StatsCard>
<Title>Involved Services</Title>
<WorkflowSteps workflow={parsedWorkflowFile} />
</StatsCard>
<div className="sticky top-0 flex justify-between items-end">
<div className="flex-1">
{/*TO DO update searchParams for these filters*/}
<FilterTabs tabs={tabs} setTab={setTab} tab={tab} />
</div>
{!!data?.workflow && (
<Button
disabled={isRunning || isRunButtonDisabled}
className="p-2 px-4"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
handleRunClick?.();
}}
tooltip={message}
>
Run now
</Button>
)}
</div>
{data?.items && (
<div className="mt-2 flex flex-col gap-2">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 p-0.5">
<StatsCard data={`${data.count ?? 0}`}>
<Title>Total Executions</Title>
<div>
<h1 className="text-2xl font-bold">
{formatNumber(data.count ?? 0)}
</h1>
</div>
<WorkflowGraph
showLastExecutionStatus={false}
workflow={workflow}
limit={executionPagination.limit}
showAll={true}
size="sm"
/>
<h1 className="text-xl font-bold mt-4">Execution History</h1>
<TableFilters workflowId={data.workflow.id} />
<ExecutionTable
executions={data}
setPagination={setExecutionPagination}
/>
</div>
)}
</StatsCard>
<StatsCard data={`${data.passCount}/${data.failCount}`}>
<Title>Pass / Fail ratio</Title>
<div>
<h1 className="text-2xl font-bold">
{formatNumber(data.passCount)}
{"/"}
{formatNumber(data.failCount)}
</h1>
</div>
</StatsCard>
<StatsCard>
<Title>Success %</Title>
<div>
<h1 className="text-2xl font-bold">
{(data.count
? (data.passCount / data.count) * 100
: 0
).toFixed(2)}
{"%"}
</h1>
</div>
</StatsCard>
<StatsCard>
<Title>Avg. duration</Title>
<div>
<h1 className="text-2xl font-bold">
{(data.avgDuration ?? 0).toFixed(2)}
</h1>
</div>
</StatsCard>
<StatsCard>
<Title>Involved Services</Title>
<WorkflowSteps workflow={parsedWorkflowFile} />
</StatsCard>
</div>
<WorkflowGraph
showLastExecutionStatus={false}
workflow={workflow}
limit={executionPagination.limit}
showAll={true}
size="sm"
/>
<h1 className="text-xl font-bold mt-4">Execution History</h1>
<TableFilters workflowId={data.workflow.id} />
<ExecutionTable
executions={data}
setPagination={setExecutionPagination}
/>
</div>
</Card>
{!!data.workflow && !!getTriggerModalProps && (
)}
{!!data?.workflow && !!getTriggerModalProps && (
<AlertTriggerModal {...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<Partial<Workflow>>(
() => (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 <Loading />;

// Redirect if user is not authenticated
if (status === "unauthenticated") router.push("/signin");

// Handle error state for fetching workflow data
if (isLoading) return <Loading />;
if (error) {
return (
<Callout
className="mt-4"
title="Error"
icon={ExclamationCircleIcon}
color="rose"
>
Failed to load workflow
</Callout>
);
}

if (!workflow) {
return null;
}

return (
<Card className="relative grid p-4 w-full gap-3 grid-cols-[1fr_4fr] h-full">
<SideNavBar
workflow={workflow}
handleLink={setNavLink}
navLink={navlink}
/>
<div className="relative overflow-auto p-0.5 flex-1 flex-shrink-1">
{navlink === "overview" && (
<OverViewContent workflow_id={params.workflow_id} />
)}
{navlink === "builder" && (
<div className="h-[95%]">
<PageClient
workflow={workflow.workflow_raw}
workflowId={workflow.id}
/>
</div>
)}
<div className="h-fit">
{navlink === "view_yaml" && (
<BuilderModalContent
closeModal={() => {}}
compiledAlert={workflow.workflow_raw!}
id={workflow.id}
hideClose={true}
/>
)}
</div>
</div>
</Card>
);
}
6 changes: 2 additions & 4 deletions keep-ui/app/workflows/[workflow_id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,24 @@
import { ArrowLeftIcon } from "@radix-ui/react-icons";
import Link from "next/link";


export default function Layout({
children,
params,
}: {
children: any;
params: { workflow_id: string };
}) {

return (
<>
<div className="flex items-center mb-4 max-h-full">
<div className="flex flex-col mb-4 h-full gap-6">
<Link
href="/workflows"
className="flex items-center text-gray-500 hover:text-gray-700"
>
<ArrowLeftIcon className="h-5 w-5 mr-1" /> Back to Workflows
</Link>
<div className="flex-1 overflow-auto h-full">{children}</div>
</div>
<div className="overflow-auto">{children}</div>
</>
);
}
Loading
Loading