Skip to content

Commit

Permalink
feat: List Workflows UI (#771)
Browse files Browse the repository at this point in the history
* chore: Remove listWorkflowEvents endpoint

* chore: Create cluster and job workflow relations

* feat: Add listWorkflows, listWorkflowExecutions routes

* feat: Add initial list workflows page

* chore: Group runs by workflow
  • Loading branch information
johnjcsmith authored Feb 13, 2025
1 parent 68025da commit de3e726
Show file tree
Hide file tree
Showing 19 changed files with 2,549 additions and 321 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export default function WorkflowExecutionDetailsPage({
params
}: {
params: {
clusterId: string,
workflowName: string,
executionId: string
}
}) {
return (
<div className="p-6">
<h1 className="text-2xl mb-2"><pre>{params.workflowName} - {params.executionId}</pre></h1>
<div className="text-center text-gray-600 mt-4">
<div className="bg-yellow-50 border border-yellow-200 p-4 rounded-md inline-block">
<p className="text-yellow-700 font-medium">
🚧 In Development
</p>
<p className="text-yellow-600 text-sm mt-2">
Detailed workflow execution view coming soon
</p>
</div>
</div>
</div>
);
}
120 changes: 120 additions & 0 deletions app/app/clusters/[clusterId]/workflows/[workflowName]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"use client";

import { client } from "@/client/client";
import { contract } from "@/client/contract";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Skeleton } from "@/components/ui/skeleton";
import { createErrorToast } from "@/lib/utils";
import { useAuth, useUser } from "@clerk/nextjs";
import { useCallback, useEffect, useState } from "react";
import { ServerConnectionStatus } from "@/components/server-connection-pane";
import { useRouter } from "next/navigation";

export default function WorkflowDetailsPage({
params
}: {
params: {
clusterId: string,
workflowName: string
}
}) {
const router = useRouter();
const { getToken } = useAuth();
const user = useUser();
const [executions, setExecutions] = useState<{
id: string;
workflowName: string;
workflowVersion: number;
createdAt: Date;
updatedAt: Date;
}[]>([]);
const [isLoading, setIsLoading] = useState(true);

const fetchWorkflowExecutions = useCallback(async () => {
if (!params.clusterId || !user.isLoaded) {
return;
}

setIsLoading(true);
try {
const result = await client.listWorkflowExecutions({
headers: {
authorization: `Bearer ${await getToken()}`,
},
params: {
clusterId: params.clusterId,
workflowName: params.workflowName,
},
});

if (result.status === 200) {
setExecutions(result.body);
} else {
ServerConnectionStatus.addEvent({
type: "listWorkflowExecutions",
success: false,
});
}
} catch (error) {
createErrorToast(error, "Failed to load workflow executions");
} finally {
setIsLoading(false);
}
}, [params.clusterId, params.workflowName, getToken, user.isLoaded]);

useEffect(() => {
fetchWorkflowExecutions();
}, [fetchWorkflowExecutions]);

const handleExecutionClick = (executionId: string) => {
router.push(`/clusters/${params.clusterId}/workflows/${params.workflowName}/${executionId}`);
};

if (isLoading) {
return (
<div className="p-6">
<h1 className="text-2xl mb-2">{params.workflowName} Executions</h1>
<div className="space-y-3">
{[...Array(5)].map((_, index) => (
<Skeleton key={index} className="h-10 w-full" />
))}
</div>
</div>
);
}

return (
<div className="p-6">
<h1 className="text-2xl mb-2"><pre>{params.workflowName}</pre></h1>
{executions.length === 0 ? (
<p className="text-muted-foreground text-center">No workflow executions found</p>
) : (
<Table>
<TableHeader>
<TableRow header>
<TableHead>Execution ID</TableHead>
<TableHead>Version</TableHead>
<TableHead>Created At</TableHead>
<TableHead>Updated At</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{executions.map((execution, index) => (
<TableRow
key={index}
onClick={() => handleExecutionClick(execution.id)}
className="cursor-pointer"
>
<TableCell>{execution.id}</TableCell>
<TableCell>{execution.workflowVersion}</TableCell>
<TableCell>{execution.createdAt.toLocaleString()}</TableCell>
<TableCell>{execution.updatedAt.toLocaleString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
);
}
7 changes: 7 additions & 0 deletions app/app/clusters/[clusterId]/workflows/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Layout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}
103 changes: 103 additions & 0 deletions app/app/clusters/[clusterId]/workflows/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use client";

import { client } from "@/client/client";
import { contract } from "@/client/contract";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { createErrorToast } from "@/lib/utils";
import { useAuth, useUser } from "@clerk/nextjs";
import { useCallback, useEffect, useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { ServerConnectionStatus } from "@/components/server-connection-pane";
import { Skeleton } from "@/components/ui/skeleton";
import { useRouter } from "next/navigation";

export default function WorkflowsPage({ params }: { params: { clusterId: string } }) {
const router = useRouter();
const { getToken } = useAuth();
const user = useUser();
const [workflows, setWorkflows] = useState<{ name: string; version: number }[]>([]);
const [isLoading, setIsLoading] = useState(true);

const fetchWorkflows = useCallback(async () => {
if (!params.clusterId || !user.isLoaded) {
return;
}

setIsLoading(true);
try {
const result = await client.listWorkflows({
headers: {
authorization: `Bearer ${await getToken()}`,
},
params: {
clusterId: params.clusterId,
},
});

if (result.status === 200) {
setWorkflows(result.body);
} else {
ServerConnectionStatus.addEvent({
type: "listWorkflows",
success: false,
});
}
} catch (error) {
createErrorToast(error, "Failed to load workflows");
} finally {
setIsLoading(false);
}
}, [params.clusterId, getToken, user.isLoaded]);

useEffect(() => {
fetchWorkflows();
}, [fetchWorkflows]);

const handleWorkflowClick = (workflowName: string) => {
router.push(`/clusters/${params.clusterId}/workflows/${workflowName}`);
};

if (isLoading) {
return (
<div className="p-6">
<h1 className="text-2xl mb-2">Workflows</h1>
<div className="space-y-3">
{[...Array(5)].map((_, index) => (
<Skeleton key={index} className="h-10 w-full" />
))}
</div>
</div>
);
}

return (
<div className="p-6">
<h1 className="text-2xl mb-2">Workflows</h1>
{workflows.length === 0 ? (
<p className="text-muted-foreground text-center">No workflows found</p>
) : (
<Table>
<TableHeader>
<TableRow header>
<TableHead>Name</TableHead>
<TableHead>Version</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{workflows.map((workflow, index) => (
<TableRow
key={index}
onClick={() => handleWorkflowClick(workflow.name)}
className="cursor-pointer"
>
<TableCell>{workflow.name}</TableCell>
<TableCell>{workflow.version}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
);
}
Loading

0 comments on commit de3e726

Please sign in to comment.