mirror of
https://github.com/colanode/colanode.git
synced 2026-02-24 03:49:48 +01:00
Show skeletons for loading states
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
87
apps/desktop/src/renderer/components/tasks/task-details.tsx
Normal file
87
apps/desktop/src/renderer/components/tasks/task-details.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
39
apps/desktop/src/renderer/components/tasks/task-skeleton.tsx
Normal file
39
apps/desktop/src/renderer/components/tasks/task-skeleton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user