Skip to content

Commit

Permalink
fix: saving workflow + feat: skeleton, do not reload page on delete, …
Browse files Browse the repository at this point in the history
…etc (#3015)

Signed-off-by: Tal <tal@keephq.dev>
Co-authored-by: Tal <tal@keephq.dev>
  • Loading branch information
Kiryous and talboren authored Jan 14, 2025
1 parent 449d3d3 commit 613fc25
Show file tree
Hide file tree
Showing 36 changed files with 932 additions and 1,123 deletions.
20 changes: 16 additions & 4 deletions keep-ui/app/(keep)/incidents/[id]/getIncidentWithErrorHandling.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,33 @@ import { createServerApiClient } from "@/shared/api/server";
import { notFound } from "next/navigation";
import { KeepApiError } from "@/shared/api";
import { IncidentDto } from "@/entities/incidents/model";
import { cache } from "react";

export async function getIncidentWithErrorHandling(
id: string,
redirect = true
/**
* Fetches an incident by ID with error handling for 404 cases
* @param id - The unique identifier of the incident to retrieve
* @returns Promise containing the incident data
* @throws {Error} If the API request fails for reasons other than 404
* @throws {never} If 404 error occurs (handled by Next.js notFound)
*/
async function _getIncidentWithErrorHandling(
id: string
// @ts-ignore ignoring since not found will be handled by nextjs
): Promise<IncidentDto> {
try {
const api = await createServerApiClient();
const incident = await getIncident(api, id);
return incident;
} catch (error) {
if (error instanceof KeepApiError && error.statusCode === 404 && redirect) {
if (error instanceof KeepApiError && error.statusCode === 404) {
notFound();
} else {
throw error;
}
}
}

// cache the function for server side, so we can use it in the layout, metadata and in the page itself
export const getIncidentWithErrorHandling = cache(
_getIncidentWithErrorHandling
);
2 changes: 1 addition & 1 deletion keep-ui/app/(keep)/incidents/[id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default async function Layout({
const AIEnabled =
!!process.env.OPEN_AI_API_KEY || !!process.env.OPENAI_API_KEY;
try {
const incident = await getIncidentWithErrorHandling(serverParams.id, false);
const incident = await getIncidentWithErrorHandling(serverParams.id);
return (
<IncidentLayoutClient initialIncident={incident} AIEnabled={AIEnabled}>
{children}
Expand Down
2 changes: 1 addition & 1 deletion keep-ui/app/(keep)/incidents/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { IncidentFilterContextProvider } from "../../../features/incident-list/ui/incident-table-filters-context";
import { IncidentFilterContextProvider } from "@/features/incident-list/ui/incident-table-filters-context";

export default function Layout({ children }: { children: any }) {
return (
Expand Down
35 changes: 10 additions & 25 deletions keep-ui/app/(keep)/workflows/[workflow_id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,22 @@
"use client";

import { Link } from "@/components/ui";
import { ArrowRightIcon } from "@heroicons/react/16/solid";
import { Icon, Subtitle } from "@tremor/react";
import { useParams } from "next/navigation";
import { getWorkflowWithRedirectSafe } from "@/shared/api/workflows";
import { WorkflowBreadcrumbs } from "./workflow-breadcrumbs";
import WorkflowDetailHeader from "./workflow-detail-header";

export default function Layout({
export default async function Layout({
children,
params,
}: {
children: any;
children: React.ReactNode;
params: { workflow_id: string };
}) {
const clientParams = useParams();
const workflow = await getWorkflowWithRedirectSafe(params.workflow_id);
return (
<div className="flex flex-col mb-4 h-full gap-6">
<Subtitle className="text-sm">
<Link href="/workflows">All Workflows</Link>{" "}
<Icon icon={ArrowRightIcon} color="gray" size="xs" />{" "}
{clientParams.workflow_execution_id ? (
<>
<Link href={`/workflows/${params.workflow_id}`}>
Workflow Details
</Link>
<Icon icon={ArrowRightIcon} color="gray" size="xs" /> Workflow
Execution Details
</>
) : (
"Workflow Details"
)}
</Subtitle>
<WorkflowDetailHeader workflow_id={params.workflow_id} />
<WorkflowBreadcrumbs workflowId={params.workflow_id} />
<WorkflowDetailHeader
workflowId={params.workflow_id}
initialData={workflow}
/>
<div className="flex-1 h-full">{children}</div>
</div>
);
Expand Down
10 changes: 8 additions & 2 deletions keep-ui/app/(keep)/workflows/[workflow_id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Metadata } from "next";
import WorkflowDetailPage from "./workflow-detail-page";
import { getWorkflowWithRedirectSafe } from "@/shared/api/workflows";

export default function Page({ params }: { params: { workflow_id: string } }) {
return <WorkflowDetailPage params={params} />;
export default async function Page({
params,
}: {
params: { workflow_id: string };
}) {
const initialData = await getWorkflowWithRedirectSafe(params.workflow_id);
return <WorkflowDetailPage params={params} initialData={initialData} />;
}

export const metadata: Metadata = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { Icon } from "@tremor/react";
import { useParams } from "next/navigation";
import { Link } from "@/components/ui";
import { Subtitle } from "@tremor/react";
import { ArrowRightIcon } from "@heroicons/react/16/solid";

export function WorkflowBreadcrumbs({ workflowId }: { workflowId: string }) {
const clientParams = useParams();

return (
<Subtitle className="text-sm">
<Link href="/workflows">All Workflows</Link>{" "}
<Icon icon={ArrowRightIcon} color="gray" size="xs" />{" "}
{clientParams.workflow_execution_id ? (
<>
<Link href={`/workflows/${workflowId}`}>Workflow Details</Link>
<Icon icon={ArrowRightIcon} color="gray" size="xs" /> Workflow
Execution Details
</>
) : (
"Workflow Details"
)}
</Subtitle>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import { useWorkflowRun } from "@/utils/hooks/useWorkflowRun";
import AlertTriggerModal from "../workflow-run-with-alert-modal";

export default function WorkflowDetailHeader({
workflow_id,
workflowId: workflow_id,
initialData,
}: {
workflow_id: string;
workflowId: string;
initialData?: Workflow;
}) {
const api = useApi();
const {
Expand All @@ -20,10 +22,10 @@ export default function WorkflowDetailHeader({
error,
} = useSWR<Partial<Workflow>>(
api.isReady() ? `/workflows/${workflow_id}` : null,
(url: string) => api.get(url)
(url: string) => api.get(url),
{ fallbackData: initialData, revalidateOnMount: false }
);


const {
isRunning,
handleRunClick,
Expand All @@ -36,14 +38,18 @@ export default function WorkflowDetailHeader({
return <div>Error loading workflow</div>;
}

if (isLoading || !workflow) {
if (!workflow) {
return (
<div>
<h1 className="text-2xl line-clamp-2 font-extrabold">
<Skeleton className="w-1/2 h-4" />
</h1>
<Skeleton className="w-3/4 h-4" />
<Skeleton className="w-1/2 h-4" />
<div className="flex flex-col gap-2">
<div className="!w-1/2 h-8">
<Skeleton className="w-full h-full" />
</div>
<div className="!w-3/4 h-4">
<Skeleton className="w-full h-full" />
</div>
<div className="!w-2/5 h-4">
<Skeleton className="w-full h-full" />
</div>
</div>
);
}
Expand All @@ -52,10 +58,12 @@ export default function WorkflowDetailHeader({
<div>
<div className="flex justify-between items-end text-sm gap-2">
<div>
<h1 className="text-2xl line-clamp-2 font-bold">{workflow.name}</h1>
<h1 className="text-2xl line-clamp-2 font-bold" data-testid="wf-name">
{workflow.name}
</h1>
{workflow.description && (
<Text className="line-clamp-5">
<span>{workflow.description}</span>
<span data-testid="wf-description">{workflow.description}</span>
</Text>
)}
</div>
Expand Down
47 changes: 28 additions & 19 deletions keep-ui/app/(keep)/workflows/[workflow_id]/workflow-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
CodeBracketIcon,
WrenchIcon,
} from "@heroicons/react/24/outline";
import Loading from "@/app/(keep)/loading";
import { Workflow } from "@/shared/api/workflows";
import useSWR from "swr";
import { WorkflowBuilderPageClient } from "../builder/page.client";
Expand All @@ -23,11 +22,14 @@ import { useApi } from "@/shared/lib/hooks/useApi";
import { useConfig } from "utils/hooks/useConfig";
import { AiOutlineSwap } from "react-icons/ai";
import { ErrorComponent, TabNavigationLink, YAMLCodeblock } from "@/shared/ui";
import Skeleton from "react-loading-skeleton";

export default function WorkflowDetailPage({
params,
initialData,
}: {
params: { workflow_id: string };
initialData?: Workflow;
}) {
const api = useApi();
const { data: configData } = useConfig();
Expand All @@ -37,9 +39,12 @@ export default function WorkflowDetailPage({
data: workflow,
isLoading,
error,
} = useSWR<Partial<Workflow>>(
} = useSWR<Workflow>(
api.isReady() ? `/workflows/${params.workflow_id}` : null,
(url: string) => api.get(url)
(url: string) => api.get(url),
{
fallbackData: initialData,
}
);

const docsUrl = configData?.KEEP_DOCS_URL || "https://docs.keephq.dev";
Expand All @@ -48,10 +53,6 @@ export default function WorkflowDetailPage({
return <ErrorComponent error={error} />;
}

if (isLoading || !workflow) {
return <Loading />;
}

// TODO: change url to /workflows/[workflow_id]/[tab] or use the file-based routing
const handleTabChange = (index: number) => {
setTabIndex(index);
Expand Down Expand Up @@ -84,20 +85,28 @@ export default function WorkflowDetailPage({
<WorkflowOverview workflow_id={params.workflow_id} />
</TabPanel>
<TabPanel>
<Card className="h-[calc(100vh-150px)]">
<WorkflowBuilderPageClient
workflowRaw={workflow.workflow_raw}
workflowId={workflow.id}
/>
</Card>
{!workflow ? (
<Skeleton className="w-full h-full" />
) : (
<Card className="h-[calc(100vh-150px)]">
<WorkflowBuilderPageClient
workflowRaw={workflow.workflow_raw}
workflowId={workflow.id}
/>
</Card>
)}
</TabPanel>
<TabPanel>
<Card>
<YAMLCodeblock
yamlString={workflow.workflow_raw!}
filename={workflow.id ?? "workflow"}
/>
</Card>
{!workflow ? (
<Skeleton className="w-full h-full" />
) : (
<Card>
<YAMLCodeblock
yamlString={workflow.workflow_raw!}
filename={workflow.id ?? "workflow"}
/>
</Card>
)}
</TabPanel>
</TabPanels>
</TabGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Skeleton from "react-loading-skeleton";

export function WorkflowOverviewSkeleton() {
return (
<div className="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">
<div>
<Skeleton className="h-24" />
</div>
<div>
<Skeleton className="h-24" />
</div>
<div>
<Skeleton className="h-24" />
</div>
<div>
<Skeleton className="h-24" />
</div>
<div>
<Skeleton className="h-24" />
</div>
</div>
<div className="flex flex-col gap-4">
<Skeleton className="h-32" />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Workflow } from "@/shared/api/workflows";
import WorkflowGraph from "../workflow-graph";
import { TableFilters } from "./table-filters";
import { ExecutionTable } from "./workflow-execution-table";
import { WorkflowOverviewSkeleton } from "./workflow-overview-skeleton";

interface Pagination {
limit: number;
Expand Down Expand Up @@ -80,7 +81,7 @@ export default function WorkflowOverview({
return (
<div className="flex flex-col gap-4">
{/* TODO: Add a working time filter */}
{!data || isLoading || (isValidating && <Loading />)}
{(!data || isLoading || isValidating) && <WorkflowOverviewSkeleton />}
{data?.items && (
<div className="flex flex-col gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
Expand Down
5 changes: 5 additions & 0 deletions keep-ui/app/(keep)/workflows/builder/CustomEdge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ const CustomEdge: React.FC<CustomEdgeProps> = ({
onClick={(e) => {
setSelectedEdge(id);
}}
data-testid={
source === "trigger_start"
? "wf-add-trigger-button"
: "wf-add-step-button"
}
>
<PlusIcon
className={`size-7 hover:text-black rounded text-sm bg-white border text-gray-700 ${
Expand Down
Loading

0 comments on commit 613fc25

Please sign in to comment.