feat(webapp): add SyncProvider and migrate saved queries + history panels to Dexie

This commit is contained in:
Rohith Gilla 2026-04-06 17:14:14 +05:30
parent e7c72de246
commit be681f1601
No known key found for this signature in database
5 changed files with 62 additions and 55 deletions

View file

@ -3,10 +3,12 @@ import { AppSidebar } from '@/components/sidebar/app-sidebar'
import { UsageBanner } from '@/components/upgrade/usage-banner'
import { UrlSync } from '@/components/url-sync'
import { CommandPalette } from '@/components/command-palette'
import { SyncProvider } from '@/components/sync-provider'
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<Suspense>
<SyncProvider>
<div className="flex h-screen overflow-hidden">
<UrlSync />
<CommandPalette />
@ -16,6 +18,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
<main className="flex-1 overflow-auto">{children}</main>
</div>
</div>
</SyncProvider>
</Suspense>
)
}

View file

@ -2,9 +2,9 @@
import { useState } from 'react'
import { Play, Trash2, CheckCircle, XCircle } from 'lucide-react'
import { trpc } from '@/lib/trpc-client'
import { useQueryHistory } from '@/hooks/use-query-history'
import { useConnectionStore } from '@/stores/connection-store'
import { useQueryStore } from '@/stores/query-store'
import { useQueryTabs } from '@/hooks/use-query-tabs'
function formatRelativeTime(date: Date | string): string {
const now = Date.now()
@ -23,17 +23,8 @@ function formatRelativeTime(date: Date | string): string {
export function QueryHistoryPanel() {
const [statusFilter, setStatusFilter] = useState<'success' | 'error' | undefined>()
const { activeConnectionId } = useConnectionStore()
const { activeTabId, updateSql } = useQueryStore()
const utils = trpc.useUtils()
const { data: entries, isLoading } = trpc.history.list.useQuery(
{ connectionId: activeConnectionId ?? undefined, status: statusFilter, limit: 100 },
{ enabled: !!activeConnectionId }
)
const deleteMutation = trpc.history.delete.useMutation({
onSuccess: () => utils.history.list.invalidate(),
})
const { activeTabId, updateSql } = useQueryTabs()
const { history, remove } = useQueryHistory(activeConnectionId ?? undefined, statusFilter)
if (!activeConnectionId) {
return (
@ -64,11 +55,10 @@ export function QueryHistoryPanel() {
</button>
</div>
<div className="flex-1 overflow-y-auto">
{isLoading && <div className="px-3 py-4 text-xs text-muted-foreground">Loading...</div>}
{entries?.length === 0 && (
{history.length === 0 && (
<div className="px-3 py-4 text-xs text-muted-foreground text-center">No history yet</div>
)}
{entries?.map((entry) => (
{history.map((entry) => (
<div
key={entry.id}
className={`group px-3 py-2 border-b border-border/30 hover:bg-muted/30 ${
@ -90,7 +80,7 @@ export function QueryHistoryPanel() {
· {entry.durationMs}ms
</span>
)}
{entry.rowCount !== null && entry.status === 'success' && (
{entry.rowCount !== null && entry.rowCount !== undefined && entry.status === 'success' && (
<span className="text-[10px] text-muted-foreground">
· {entry.rowCount} rows
</span>
@ -105,7 +95,7 @@ export function QueryHistoryPanel() {
<Play className="h-3 w-3" />
</button>
<button
onClick={() => deleteMutation.mutate({ id: entry.id })}
onClick={() => remove(entry.id)}
className="p-1 rounded text-muted-foreground hover:text-destructive"
title="Delete"
>

View file

@ -4,28 +4,21 @@ import { useState } from 'react'
import { Bookmark, X } from 'lucide-react'
import { trpc } from '@/lib/trpc-client'
import { useConnectionStore } from '@/stores/connection-store'
import { useQueryStore } from '@/stores/query-store'
import { useQueryTabs } from '@/hooks/use-query-tabs'
import { useSavedQueries } from '@/hooks/use-saved-queries'
import { ProBadge } from '@/components/upgrade/pro-badge'
export function SaveQueryDialog() {
const [open, setOpen] = useState(false)
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [isSaving, setIsSaving] = useState(false)
const { activeConnectionId } = useConnectionStore()
const { tabs, activeTabId } = useQueryStore()
const { tabs, activeTabId } = useQueryTabs()
const activeTab = tabs.find((t) => t.id === activeTabId)
const utils = trpc.useUtils()
const { create } = useSavedQueries()
const { data: usage } = trpc.usage.current.useQuery()
const saveMutation = trpc.savedQueries.create.useMutation({
onSuccess: () => {
utils.savedQueries.list.invalidate()
setOpen(false)
setName('')
setDescription('')
},
})
if (!open) {
if (
usage?.plan === 'free' &&
@ -63,19 +56,24 @@ export function SaveQueryDialog() {
autoFocus
/>
<button
onClick={() => {
onClick={async () => {
if (!activeConnectionId || !activeTab?.sql || !name.trim()) return
saveMutation.mutate({
setIsSaving(true)
await create({
connectionId: activeConnectionId,
name: name.trim(),
query: activeTab.sql,
description: description || undefined,
})
setIsSaving(false)
setOpen(false)
setName('')
setDescription('')
}}
disabled={!name.trim() || saveMutation.isPending}
disabled={!name.trim() || isSaving}
className="rounded-md bg-accent px-2 py-1 text-xs text-accent-foreground hover:bg-accent/90 disabled:opacity-50"
>
{saveMutation.isPending ? '...' : 'Save'}
{isSaving ? '...' : 'Save'}
</button>
<button
onClick={() => setOpen(false)}

View file

@ -2,27 +2,19 @@
import { useState } from 'react'
import { Play, Trash2, Search } from 'lucide-react'
import { trpc } from '@/lib/trpc-client'
import { useSavedQueries } from '@/hooks/use-saved-queries'
import { useConnectionStore } from '@/stores/connection-store'
import { useQueryStore } from '@/stores/query-store'
import { useQueryTabs } from '@/hooks/use-query-tabs'
export function SavedQueriesPanel() {
const [search, setSearch] = useState('')
const { activeConnectionId } = useConnectionStore()
const { activeTabId, updateSql } = useQueryStore()
const utils = trpc.useUtils()
const { data: queries, isLoading } = trpc.savedQueries.list.useQuery(
{ connectionId: activeConnectionId ?? undefined, search: search || undefined },
{ enabled: !!activeConnectionId }
const { activeTabId, updateSql } = useQueryTabs()
const { queries, remove, incrementUsage } = useSavedQueries(
activeConnectionId ?? undefined,
search || undefined
)
const deleteMutation = trpc.savedQueries.delete.useMutation({
onSuccess: () => utils.savedQueries.list.invalidate(),
})
const incrementMutation = trpc.savedQueries.incrementUsage.useMutation()
if (!activeConnectionId) {
return (
<div className="px-3 py-4 text-xs text-muted-foreground">Select a connection first</div>
@ -44,15 +36,12 @@ export function SavedQueriesPanel() {
</div>
</div>
<div className="flex-1 overflow-y-auto">
{isLoading && (
<div className="px-3 py-4 text-xs text-muted-foreground">Loading...</div>
)}
{queries?.length === 0 && (
{queries.length === 0 && (
<div className="px-3 py-4 text-xs text-muted-foreground text-center">
No saved queries
</div>
)}
{queries?.map((q) => (
{queries.map((q) => (
<div key={q.id} className="group px-3 py-2 border-b border-border/30 hover:bg-muted/30">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-foreground truncate">{q.name}</span>
@ -60,7 +49,7 @@ export function SavedQueriesPanel() {
<button
onClick={() => {
updateSql(activeTabId, q.query)
incrementMutation.mutate({ id: q.id })
incrementUsage(q.id)
}}
className="p-1 rounded text-muted-foreground hover:text-accent"
title="Load into editor"
@ -68,7 +57,7 @@ export function SavedQueriesPanel() {
<Play className="h-3 w-3" />
</button>
<button
onClick={() => deleteMutation.mutate({ id: q.id })}
onClick={() => remove(q.id)}
className="p-1 rounded text-muted-foreground hover:text-destructive"
title="Delete"
>

View file

@ -0,0 +1,27 @@
'use client'
import { useEffect, useRef } from 'react'
import { useAuth } from '@clerk/nextjs'
import { trpc, type TRPCClient } from '@/lib/trpc-client'
import { SyncManager } from '@/lib/sync-manager'
export function SyncProvider({ children }: { children: React.ReactNode }) {
const { userId } = useAuth()
const syncRef = useRef<SyncManager | null>(null)
const trpcClient = trpc.useUtils().client as TRPCClient
useEffect(() => {
if (!userId || !trpcClient) return
const manager = new SyncManager(userId, trpcClient)
syncRef.current = manager
manager.start()
return () => {
manager.stop()
syncRef.current = null
}
}, [userId, trpcClient])
return <>{children}</>
}