fix: show structured page placeholders while loading
This commit is contained in:
@@ -7,7 +7,7 @@ import { z } from "zod";
|
||||
import { useCreateWebhook, useDeleteWebhook, useTestWebhook, useWebhooks } from "@/api/queries";
|
||||
import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
|
||||
import { ErrorAlert } from "@/components/shared/ErrorAlert";
|
||||
import { PageLoader } from "@/components/shared/LoadingSpinner";
|
||||
import { Skeleton } from "@/components/shared/Skeleton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Body, Muted, PageTitle, SectionHeading } from "@/components/ui/typography";
|
||||
@@ -79,7 +79,6 @@ export function WebhookManager({ workspaceId }: Props) {
|
||||
|
||||
<div className="mt-8 space-y-4">
|
||||
<ErrorAlert error={error instanceof Error ? error : null} />
|
||||
{isLoading && <PageLoader />}
|
||||
|
||||
{/* Add webhook form */}
|
||||
<motion.div
|
||||
@@ -133,73 +132,73 @@ export function WebhookManager({ workspaceId }: Props) {
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Webhook list */}
|
||||
{!isLoading && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="rounded-xl theme-card overflow-hidden"
|
||||
>
|
||||
{list.length === 0 ? (
|
||||
<div className="p-8 text-center">
|
||||
<Webhook
|
||||
className="w-8 h-8 mx-auto mb-2 opacity-20"
|
||||
style={{ color: "var(--text-3)" }}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<Muted>No webhook endpoints yet.</Muted>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y" style={{ borderColor: "var(--border)" }}>
|
||||
{list.map((wh, i) => (
|
||||
<motion.div
|
||||
key={(wh as { id: string }).id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: i * 0.04 }}
|
||||
className="flex items-center justify-between px-5 py-3 gap-4"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="text-xs font-mono truncate"
|
||||
style={{ color: "var(--accent-text)" }}
|
||||
>
|
||||
{mask((wh as { url: string }).url)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void open((wh as { url: string }).url)}
|
||||
className="flex-shrink-0"
|
||||
style={{
|
||||
color: "var(--text-4)",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs font-mono" style={{ color: "var(--text-4)" }}>
|
||||
{mask((wh as { id: string }).id)}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="rounded-xl theme-card overflow-hidden"
|
||||
>
|
||||
{isLoading ? (
|
||||
<WebhookListSkeleton />
|
||||
) : list.length === 0 ? (
|
||||
<div className="p-8 text-center">
|
||||
<Webhook
|
||||
className="w-8 h-8 mx-auto mb-2 opacity-20"
|
||||
style={{ color: "var(--text-3)" }}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<Muted>No webhook endpoints yet.</Muted>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y" style={{ borderColor: "var(--border)" }}>
|
||||
{list.map((wh, i) => (
|
||||
<motion.div
|
||||
key={(wh as { id: string }).id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: i * 0.04 }}
|
||||
className="flex items-center justify-between px-5 py-3 gap-4"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="text-xs font-mono truncate"
|
||||
style={{ color: "var(--accent-text)" }}
|
||||
>
|
||||
{mask((wh as { url: string }).url)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void open((wh as { url: string }).url)}
|
||||
className="flex-shrink-0"
|
||||
style={{
|
||||
color: "var(--text-4)",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setDeleteTarget((wh as { id: string }).id)}
|
||||
aria-label="Delete webhook"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
|
||||
</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
<span className="text-xs font-mono" style={{ color: "var(--text-4)" }}>
|
||||
{mask((wh as { id: string }).id)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setDeleteTarget((wh as { id: string }).id)}
|
||||
aria-label="Delete webhook"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
|
||||
</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
@@ -217,3 +216,19 @@ export function WebhookManager({ workspaceId }: Props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WebhookListSkeleton() {
|
||||
return (
|
||||
<div className="divide-y" style={{ borderColor: "var(--border)" }} aria-hidden="true">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div key={index} className="flex items-center justify-between px-5 py-3 gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Skeleton accent className="h-3 w-52 rounded" />
|
||||
<Skeleton className="mt-2 h-3 w-32 rounded" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useDeleteWorkspace, useQueueStatus, useScheduleDream, useWorkspace } fr
|
||||
import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
|
||||
import { ErrorAlert } from "@/components/shared/ErrorAlert";
|
||||
import { JsonViewer } from "@/components/shared/JsonViewer";
|
||||
import { PageLoader } from "@/components/shared/LoadingSpinner";
|
||||
import { Skeleton } from "@/components/shared/Skeleton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Body, Caption, PageTitle, SectionHeading } from "@/components/ui/typography";
|
||||
import { ScheduleDreamModal } from "@/components/workspaces/ScheduleDreamModal";
|
||||
@@ -107,7 +107,7 @@ export function WorkspaceDetail() {
|
||||
|
||||
<div className="mt-8">
|
||||
<ErrorAlert error={error instanceof Error ? error : null} />
|
||||
{isLoading && <PageLoader />}
|
||||
{isLoading && <WorkspaceDetailSkeleton />}
|
||||
|
||||
{!isLoading && workspace && (
|
||||
<div className="space-y-4">
|
||||
@@ -342,3 +342,42 @@ export function WorkspaceDetail() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceDetailSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4" aria-hidden="true">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div key={index} className="rounded-xl p-5 theme-card">
|
||||
<Skeleton accent className="h-5 w-5 rounded" />
|
||||
<Skeleton className="mt-4 h-4 w-24 rounded" />
|
||||
<Skeleton className="mt-3 h-3 w-32 rounded" />
|
||||
<Skeleton className="mt-2 h-3 w-24 rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl p-5 theme-card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Skeleton className="h-4 w-28 rounded" />
|
||||
<Skeleton className="h-4 w-20 rounded" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div key={index}>
|
||||
<Skeleton accent className="h-8 w-12 rounded" />
|
||||
<Skeleton className="mt-2 h-3 w-16 rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl p-5 theme-card">
|
||||
<Skeleton className="h-4 w-20 rounded" />
|
||||
<Skeleton className="mt-4 h-3 w-full rounded" />
|
||||
<Skeleton className="mt-2 h-3 w-[92%] rounded" />
|
||||
<Skeleton className="mt-2 h-3 w-[64%] rounded" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useWorkspaces } from "@/api/queries";
|
||||
import type { components } from "@/api/schema.d.ts";
|
||||
import { EmptyState } from "@/components/shared/EmptyState";
|
||||
import { ErrorAlert } from "@/components/shared/ErrorAlert";
|
||||
import { PageLoader } from "@/components/shared/LoadingSpinner";
|
||||
import { Pagination } from "@/components/shared/Pagination";
|
||||
import { Skeleton } from "@/components/shared/Skeleton";
|
||||
import { SortControl, type SortDir } from "@/components/shared/SortControl";
|
||||
import { MonoCaption, Muted, PageTitle } from "@/components/ui/typography";
|
||||
import { useDemo } from "@/hooks/useDemo";
|
||||
@@ -94,7 +94,7 @@ export function WorkspaceList() {
|
||||
</motion.div>
|
||||
|
||||
<ErrorAlert error={error instanceof Error ? error : null} />
|
||||
{isLoading && <PageLoader />}
|
||||
{isLoading && <WorkspaceListSkeleton />}
|
||||
|
||||
{!isLoading && workspaces.length === 0 && (
|
||||
<EmptyState
|
||||
@@ -156,3 +156,36 @@ export function WorkspaceList() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceListSkeleton() {
|
||||
return (
|
||||
<div aria-hidden="true">
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-xl px-5 py-4"
|
||||
style={{
|
||||
background: COLOR.cardBaseBg,
|
||||
border: `1px solid ${COLOR.cardBaseBorder}`,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton accent className="h-4 w-40 rounded" />
|
||||
<Skeleton className="h-4 w-4 rounded" />
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<Skeleton className="h-3 w-3 rounded-full" />
|
||||
<Skeleton className="h-3 w-32 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<Skeleton className="h-8 w-20 rounded-lg" />
|
||||
<Skeleton className="h-4 w-16 rounded" />
|
||||
<Skeleton className="h-8 w-20 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user