Skip to content

Commit

Permalink
fix: refactor the workflow execution component to reduce repaint/rend… (
Browse files Browse the repository at this point in the history
keephq#2211)

Co-authored-by: Tal <[email protected]>
Co-authored-by: Kirill Chernakov <[email protected]>
  • Loading branch information
3 people authored Oct 31, 2024
1 parent 23d4c65 commit add75fb
Show file tree
Hide file tree
Showing 10 changed files with 531 additions and 313 deletions.
17 changes: 16 additions & 1 deletion keep-ui/app/alerts/alert-quality-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import { GenericTable } from "@/components/table/GenericTable";
import { useAlertQualityMetrics } from "utils/hooks/useAlertQuality";
import { useProviders } from "utils/hooks/useProviders";
import { Provider, ProvidersResponse } from "app/providers/providers";
import { TabGroup, TabList, Tab } from "@tremor/react";
import { TabGroup, TabList, Tab, Callout } from "@tremor/react";
import { GenericFilters } from "@/components/filters/GenericFilters";
import { useSearchParams } from "next/navigation";
import { AlertKnownKeys } from "./models";
import { createColumnHelper, DisplayColumnDef } from "@tanstack/react-table";
import { ExclamationCircleIcon } from "@heroicons/react/20/solid";

const tabs = [
{ name: "All", value: "all" },
Expand Down Expand Up @@ -340,6 +341,20 @@ const AlertQuality = ({
isDashBoard ? (fieldsValue as string | string[]) : ""
);


if (error) {
return (
<Callout
className="mt-4"
title="Error"
icon={ExclamationCircleIcon}
color="rose"
>
Failed to load Alert Quality Metrics
</Callout>
);
}

return (
<QualityTable
providersMeta={providersMeta}
Expand Down
264 changes: 59 additions & 205 deletions keep-ui/app/workflows/[workflow_id]/executions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,128 +2,56 @@
import {
Callout,
Card,
Title,
Tab,
TabGroup,
TabList,
Button,
} from "@tremor/react";
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
import Loading from "../../loading";
import { useRouter, useSearchParams } from "next/navigation";
import { useWorkflowExecutionsV2 } from "utils/hooks/useWorkflowExecutions";

import WorkflowGraph from "../workflow-graph";
import { useRouter } from "next/navigation";
import { Workflow } from "../models";
import { WorkflowSteps } from "../mockworkflows";
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";

const tabs = [
{ name: "All Time", value: "alltime" },
{ name: "Last 30d", value: "last_30d" },
{ name: "Last 7d", value: "last_7d" },
{ name: "Today", value: "today" },
];

export const FilterTabs = ({
tabs,
setTab,
tab,
}: {
tabs: { name: string; value: string }[];
setTab: Dispatch<SetStateAction<number>>;
tab: number;
}) => {
return (
<div className="max-w-lg space-y-12 pt-6">
<TabGroup
index={tab}
onIndexChange={(index: number) => {
setTab(index);
}}
>
<TabList variant="solid" color="black" className="bg-gray-300">
{tabs.map((tabItem, index) => (
<Tab key={tabItem.value}>{tabItem.name}</Tab>
))}
</TabList>
</TabGroup>
</div>
);
};
import useSWR from "swr";
import { fetcher } from "@/utils/fetcher";
import BuilderModalContent from "../builder/builder-modal";
import PageClient from "../builder/page.client";
import { useApiUrl } from "@/utils/hooks/useConfig";
import WorkflowOverview from "./workflow-overview";

export function StatsCard({
children,
data,
}: {
children: any;
data?: string;
}) {
return (
<Card className="group relative container flex flex-col p-4 space-y-2 min-w-1/5">
{!!data && (
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-800 text-white rounded py-1 p-2 text-2xl font-bold">
{data}
</div>
)}
{children}
</Card>
);
}

interface Pagination {
limit: number;
offset: number;
}

export default function WorkflowDetailPage({
params,
}: {
params: { workflow_id: string };
}) {
const router = useRouter();
const { data: session, status, update } = useSession();
const { data: session, status } = useSession();
const [navlink, setNavLink] = useState("overview");

const [executionPagination, setExecutionPagination] = useState<Pagination>({
limit: 25,
offset: 0,
});
const [tab, setTab] = useState<number>(1);
const searchParams = useSearchParams();

useEffect(() => {
setExecutionPagination({
...executionPagination,
offset: 0,
});
}, [tab, searchParams]);
const apiUrl = useApiUrl();

const { data, isLoading, error } = useWorkflowExecutionsV2(
params.workflow_id,
tab,
executionPagination.limit,
executionPagination.offset
const {
data: workflow,
isLoading,
error,
} = useSWR<Partial<Workflow>>(
() => (session ? `${apiUrl}/workflows/${params.workflow_id}` : null),
(url: string) => fetcher(url, session?.accessToken)
);

const {
isRunning,
handleRunClick,
getTriggerModalProps,
isRunButtonDisabled,
message,
} = useWorkflowRun(data?.workflow!);
useEffect(() => {
if (status === "unauthenticated") {
router.push("/signin");
}
}, [status, router]);

// Render loading state if session is loading
// If the user is unauthenticated, display a loading state until the side effect is processed; after that, it will automatically redirect.
if (status === "loading" || status === "unauthenticated") return <Loading />;

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

// Handle error state for fetching workflow data
if (isLoading) return <Loading />;
if (error) {
return (
<Callout
Expand All @@ -136,115 +64,41 @@ 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,
}) as any;

const formatNumber = (num: number) => {
if (num >= 1_000_000) {
return `${(num / 1_000_000).toFixed(1)}m`;
} else if (num >= 1_000) {
return `${(num / 1_000).toFixed(1)}k`;
} else {
return num?.toString() ?? "";
}
};
if (!workflow) {
return null;
}

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>
)}
<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" && (
<WorkflowOverview workflow_id={params.workflow_id} />
)}
{navlink === "builder" && (
<div className="h-[95%]">
<PageClient
workflow={workflow.workflow_raw}
workflowId={workflow.id}
/>
</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>
<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>
)}
<div className="h-fit">
{navlink === "view_yaml" && (
<BuilderModalContent
closeModal={() => {}}
compiledAlert={workflow.workflow_raw!}
id={workflow.id}
hideCloseButton={true}
/>
)}
</div>
</Card>
{!!data.workflow && !!getTriggerModalProps && (
<AlertTriggerModal {...getTriggerModalProps()} />
)}
</>
</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

0 comments on commit add75fb

Please sign in to comment.