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: saving workflow + feat: skeleton, do not reload page on delete, etc #3015

Merged
merged 18 commits into from
Jan 14, 2025
Merged
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
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 (
Kiryous marked this conversation as resolved.
Show resolved Hide resolved
<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
Loading