learn and share notes on atproto (wip) 馃 malfestio.stormlightlabs.org/
readability solid axum atproto srs
at main 212 lines 6.6 kB view raw
1import { api } from "$lib/api"; 2import type { LocalCard, LocalDeck, LocalNote, SyncQueueItem } from "$lib/db"; 3import { syncStore } from "$lib/sync-store"; 4import type { Column } from "$ui/DataTable"; 5import { DataTable } from "$ui/DataTable"; 6import { createResource, createSignal, Show } from "solid-js"; 7 8type SyncRecord = { 9 id: string; 10 type: "deck" | "note" | "card"; 11 title: string; 12 status: string; 13 version: number; 14 updatedAt: string; 15}; 16 17type QueueRecord = { 18 id: string; 19 entityType: string; 20 entityId: string; 21 operation: string; 22 retryCount: number; 23 createdAt: string; 24 lastError?: string; 25}; 26 27export function SyncDataTable() { 28 const [activeTab, setActiveTab] = createSignal<"records" | "queue">("records"); 29 const [refreshKey, setRefreshKey] = createSignal(0); 30 31 const [data, { refetch }] = createResource(refreshKey, async () => { 32 const result = await syncStore.getAllLocalData(); 33 return result; 34 }); 35 36 const syncRecords = (): SyncRecord[] => { 37 const d = data(); 38 if (!d) return []; 39 40 const decks: SyncRecord[] = d.decks.map((deck: LocalDeck) => ({ 41 id: deck.id, 42 type: "deck" as const, 43 title: deck.title, 44 status: deck.syncStatus, 45 version: deck.localVersion, 46 updatedAt: deck.updatedAt, 47 })); 48 49 const notes: SyncRecord[] = d.notes.map((note: LocalNote) => ({ 50 id: note.id, 51 type: "note" as const, 52 title: note.title, 53 status: note.syncStatus, 54 version: note.localVersion, 55 updatedAt: note.updatedAt, 56 })); 57 58 const cards: SyncRecord[] = d.cards.map((card: LocalCard) => ({ 59 id: card.id, 60 type: "card" as const, 61 title: card.front.slice(0, 50) + (card.front.length > 50 ? "..." : ""), 62 status: card.syncStatus, 63 version: card.localVersion, 64 updatedAt: "", 65 })); 66 67 return [...decks, ...notes, ...cards]; 68 }; 69 70 const queueRecords = (): QueueRecord[] => { 71 const d = data(); 72 if (!d) return []; 73 74 return d.queue.map((item: SyncQueueItem) => ({ 75 id: String(item.id || ""), 76 entityType: item.entityType, 77 entityId: item.entityId, 78 operation: item.operation, 79 retryCount: item.retryCount, 80 createdAt: item.createdAt, 81 lastError: item.lastError, 82 })); 83 }; 84 85 const handleSync = async (type: string, id: string) => { 86 await syncStore.queueForSync(type as "deck" | "card" | "note", id, "push"); 87 await syncStore.processQueue(); 88 setRefreshKey((k) => k + 1); 89 }; 90 91 const handleResolve = async ( 92 type: string, 93 id: string, 94 strategy: "last_write_wins" | "keep_local" | "keep_remote", 95 ) => { 96 await api.resolveConflict(type, id, strategy); 97 setRefreshKey((k) => k + 1); 98 }; 99 100 const handleClear = async () => { 101 if (confirm("Clear all local sync data? This cannot be undone.")) { 102 await syncStore.clearAll(); 103 setRefreshKey((k) => k + 1); 104 } 105 }; 106 107 const recordColumns: Column<SyncRecord>[] = [ 108 { key: "type", header: "Type", sortable: true, width: "80px" }, 109 { key: "title", header: "Title", sortable: true }, 110 { 111 key: "status", 112 header: "Status", 113 sortable: true, 114 width: "120px", 115 render: (row) => ( 116 <span 117 class={`px-2 py-1 rounded text-xs ${ 118 row.status === "synced" 119 ? "bg-green-900 text-green-300" 120 : row.status === "conflict" 121 ? "bg-red-900 text-red-300" 122 : row.status === "pending_push" 123 ? "bg-blue-900 text-blue-300" 124 : "bg-gray-700 text-gray-300" 125 }`}> 126 {row.status} 127 </span> 128 ), 129 }, 130 { key: "version", header: "Ver", sortable: true, width: "60px" }, 131 { 132 key: "actions", 133 header: "Actions", 134 width: "150px", 135 render: (row) => ( 136 <div class="flex gap-2"> 137 <Show when={row.status === "conflict"}> 138 <button 139 onClick={() => handleResolve(row.type, row.id, "keep_local")} 140 class="text-xs text-blue-400 hover:underline"> 141 Keep Local 142 </button> 143 </Show> 144 <Show when={row.status !== "synced"}> 145 <button onClick={() => handleSync(row.type, row.id)} class="text-xs text-green-400 hover:underline"> 146 Sync 147 </button> 148 </Show> 149 </div> 150 ), 151 }, 152 ]; 153 154 const queueColumns: Column<QueueRecord>[] = [ 155 { key: "entityType", header: "Type", sortable: true, width: "80px" }, 156 { key: "entityId", header: "Entity ID", sortable: true }, 157 { key: "operation", header: "Op", width: "60px" }, 158 { key: "retryCount", header: "Retries", sortable: true, width: "80px" }, 159 { key: "lastError", header: "Error" }, 160 ]; 161 162 return ( 163 <div class="space-y-4"> 164 <div class="flex items-center justify-between"> 165 <div class="flex gap-2"> 166 <button 167 onClick={() => setActiveTab("records")} 168 class={`px-3 py-1.5 text-sm rounded ${ 169 activeTab() === "records" ? "bg-blue-600 text-white" : "bg-gray-700 text-gray-300" 170 }`}> 171 Records ({syncRecords().length}) 172 </button> 173 <button 174 onClick={() => setActiveTab("queue")} 175 class={`px-3 py-1.5 text-sm rounded ${ 176 activeTab() === "queue" ? "bg-blue-600 text-white" : "bg-gray-700 text-gray-300" 177 }`}> 178 Queue ({queueRecords().length}) 179 </button> 180 </div> 181 <div class="flex gap-2"> 182 <button 183 onClick={() => refetch()} 184 class="px-3 py-1.5 text-sm bg-gray-700 text-gray-300 rounded hover:bg-gray-600"> 185 Refresh 186 </button> 187 <button onClick={handleClear} class="px-3 py-1.5 text-sm bg-red-900 text-red-300 rounded hover:bg-red-800"> 188 Clear All 189 </button> 190 </div> 191 </div> 192 193 <Show when={data.loading}> 194 <div class="text-gray-400 text-sm">Loading...</div> 195 </Show> 196 197 <Show when={!data.loading && activeTab() === "records"}> 198 <Show when={syncRecords().length > 0} fallback={<div class="text-gray-500 text-sm">No local records</div>}> 199 <DataTable columns={recordColumns} data={syncRecords()} getRowId={(r) => r.id} /> 200 </Show> 201 </Show> 202 203 <Show when={!data.loading && activeTab() === "queue"}> 204 <Show 205 when={queueRecords().length > 0} 206 fallback={<div class="text-gray-500 text-sm">No pending queue items</div>}> 207 <DataTable columns={queueColumns} data={queueRecords()} getRowId={(r) => r.id} /> 208 </Show> 209 </Show> 210 </div> 211 ); 212}