Show skeletons for loading states

This commit is contained in:
Hakan Shehu
2025-05-08 11:42:08 +02:00
parent a87cd8aa39
commit 490cbe1706
5 changed files with 172 additions and 96 deletions

View File

@@ -0,0 +1,24 @@
import { Skeleton } from '@/renderer/components/ui/skeleton';
export const TaskCardSkeleton = () => {
return (
<div className="flex flex-row items-center gap-4 rounded-lg border bg-card p-4 shadow-sm">
<div className="flex-shrink-0">
<Skeleton className="h-10 w-10 rounded-full" />
</div>
<div className="flex flex-col min-w-0 gap-1">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-3 w-64" />
</div>
<div className="flex-1" />
<div className="flex-shrink-0 px-4">
<Skeleton className="h-6 w-20" />
</div>
<div className="flex-1" />
<div className="flex flex-col items-end gap-1 min-w-[90px]">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-3 w-12" />
</div>
</div>
);
};

View File

@@ -1,18 +1,9 @@
import {
formatDuration,
formatTaskStatus,
formatTaskType,
timeAgo,
} from '@colanode/core';
import { Clock, Calendar } from 'lucide-react';
import { Container, ContainerBody } from '@/renderer/components/ui/container';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
import { TaskLogs } from '@/renderer/components/tasks/task-logs';
import { TaskArtifacts } from '@/renderer/components/tasks/task-artifacts';
import { TaskNotFound } from '@/renderer/components/tasks/task-not-found';
import { TaskStatusBadge } from '@/renderer/components/tasks/task-status-badge';
import { TaskDetails } from '@/renderer/components/tasks/task-details';
import { TaskSkeleton } from '@/renderer/components/tasks/task-skeleton';
interface TaskContainerProps {
taskId: string;
@@ -28,89 +19,20 @@ export const TaskContainer = ({ taskId }: TaskContainerProps) => {
taskId: taskId,
});
if (isPending) {
return <div>Loading...</div>;
}
if (!data) {
return <TaskNotFound />;
}
const duration = formatDuration(data.task.createdAt, data.task.completedAt);
const createdAtAgo = timeAgo(data.task.createdAt);
return (
<Container>
<ContainerBody>
<div className="grid grid-cols-5 gap-4">
<div className="col-span-3 flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div className="flex flex-row items-center gap-4">
<TaskStatusBadge status={data.task.status} className="size-7" />
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold">{data.task.name}</h1>
<p className="text-sm text-muted-foreground">
{data.task.description}
</p>
</div>
</div>
<div className="mt-4 pt-4 pb-4 border-t border-b border-border/40">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 text-sm">
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">Type</span>
<span className="font-semibold">
{formatTaskType(data.task.attributes.type)}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">
Status
</span>
<span className="font-semibold">
{formatTaskStatus(data.task.status)}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">
Started
</span>
<div className="flex items-center gap-1">
<Calendar className="size-4" />
<span className="font-semibold">{createdAtAgo}</span>
</div>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">
Duration
</span>
<div className="flex items-center gap-1">
<Clock className="size-4" />
<span className="font-semibold">{duration}</span>
</div>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">
Artifacts
</span>
<span className="font-semibold">
{data.artifacts.length}
</span>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<h2 className="text-lg font-bold">Logs</h2>
<TaskLogs logs={data.logs} />
</div>
</div>
<div className="col-span-2">
<div className="flex flex-col gap-2">
<h2 className="text-lg font-bold">Artifacts</h2>
<TaskArtifacts artifacts={data.artifacts} />
</div>
</div>
</div>
{isPending ? (
<TaskSkeleton />
) : data ? (
<TaskDetails
task={data.task}
logs={data.logs}
artifacts={data.artifacts}
/>
) : (
<TaskNotFound />
)}
</ContainerBody>
</Container>
);

View File

@@ -0,0 +1,87 @@
import {
formatDuration,
formatTaskStatus,
formatTaskType,
TaskArtifactOutput,
TaskLogOutput,
TaskOutput,
timeAgo,
} from '@colanode/core';
import { Clock, Calendar } from 'lucide-react';
import { TaskLogs } from '@/renderer/components/tasks/task-logs';
import { TaskArtifacts } from '@/renderer/components/tasks/task-artifacts';
import { TaskStatusBadge } from '@/renderer/components/tasks/task-status-badge';
interface TaskDetailsProps {
task: TaskOutput;
logs: TaskLogOutput[];
artifacts: TaskArtifactOutput[];
}
export const TaskDetails = ({ task, logs, artifacts }: TaskDetailsProps) => {
const duration = formatDuration(task.createdAt, task.completedAt);
const createdAtAgo = timeAgo(task.createdAt);
return (
<div className="grid grid-cols-5 gap-4">
<div className="col-span-3 flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div className="flex flex-row items-center gap-4">
<TaskStatusBadge status={task.status} className="size-7" />
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold">{task.name}</h1>
<p className="text-sm text-muted-foreground">
{task.description}
</p>
</div>
</div>
<div className="mt-4 pt-4 pb-4 border-t border-b border-border/40">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 text-sm">
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">Type</span>
<span className="font-semibold">
{formatTaskType(task.attributes.type)}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">Status</span>
<span className="font-semibold">
{formatTaskStatus(task.status)}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">Started</span>
<div className="flex items-center gap-1">
<Calendar className="size-4" />
<span className="font-semibold">{createdAtAgo}</span>
</div>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">Duration</span>
<div className="flex items-center gap-1">
<Clock className="size-4" />
<span className="font-semibold">{duration}</span>
</div>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">Artifacts</span>
<span className="font-semibold">{artifacts.length}</span>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<h2 className="text-lg font-bold">Logs</h2>
<TaskLogs logs={logs} />
</div>
</div>
<div className="col-span-2">
<div className="flex flex-col gap-2">
<h2 className="text-lg font-bold">Artifacts</h2>
<TaskArtifacts artifacts={artifacts} />
</div>
</div>
</div>
);
};

View File

@@ -4,8 +4,8 @@ import { useQuery } from '@/renderer/hooks/use-query';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { Button } from '@/renderer/components/ui/button';
import { TaskCreateDialog } from '@/renderer/components/tasks/task-create-dialog';
import { Spinner } from '@/renderer/components/ui/spinner';
import { TaskCard } from '@/renderer/components/tasks/task-card';
import { TaskCardSkeleton } from '@/renderer/components/tasks/task-card-skeleton';
export const TaskList = () => {
const workspace = useWorkspace();
@@ -30,10 +30,14 @@ export const TaskList = () => {
<Button onClick={() => setShowCreateModal(true)}>Create task</Button>
</div>
<div className="flex flex-col gap-2">
{isPending && <Spinner className="h-4 w-4" />}
{tasks.map((t) => (
<TaskCard key={t.id} task={t} />
))}
{isPending && (
<>
<TaskCardSkeleton />
<TaskCardSkeleton />
<TaskCardSkeleton />
</>
)}
{!isPending && tasks.map((t) => <TaskCard key={t.id} task={t} />)}
</div>
</div>
<TaskCreateDialog

View File

@@ -0,0 +1,39 @@
import { Skeleton } from '@/renderer/components/ui/skeleton';
export const TaskSkeleton = () => {
return (
<div className="grid grid-cols-5 gap-4">
<div className="col-span-3 flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div className="flex flex-row items-center gap-4">
<Skeleton className="size-7 rounded-full" />
<div className="flex flex-col gap-1">
<Skeleton className="h-7 w-48" />
<Skeleton className="h-4 w-64" />
</div>
</div>
<div className="mt-4 pt-4 pb-4 border-t border-b border-border/40">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 text-sm">
{Array.from({ length: 5 }).map((_, index) => (
<div className="flex flex-col gap-1" key={index}>
<Skeleton className="h-3 w-16" />
<Skeleton className="h-5 w-24" />
</div>
))}
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-6 w-20" />
<Skeleton className="h-32 w-full" />
</div>
</div>
<div className="col-span-2">
<div className="flex flex-col gap-2">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-48 w-full" />
</div>
</div>
</div>
);
};