mirror of
https://github.com/Rohithgilla12/data-peek
synced 2026-04-21 12:57:16 +00:00
feat(webapp): add Dexie-backed hooks for saved queries, history, tabs, and UI state
This commit is contained in:
parent
2d36590e6e
commit
e7c72de246
4 changed files with 318 additions and 0 deletions
91
apps/webapp/src/hooks/use-query-history.ts
Normal file
91
apps/webapp/src/hooks/use-query-history.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
'use client'
|
||||
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { useAuth } from '@clerk/nextjs'
|
||||
import { getDB } from '@/lib/dexie'
|
||||
|
||||
export function useQueryHistory(connectionId?: string, status?: 'success' | 'error') {
|
||||
const { userId } = useAuth()
|
||||
|
||||
const history = useLiveQuery(async () => {
|
||||
if (!userId) return []
|
||||
const db = getDB(userId)
|
||||
|
||||
let results = await db.queryHistory
|
||||
.where('_syncStatus')
|
||||
.notEqual('deleted')
|
||||
.reverse()
|
||||
.sortBy('executedAt')
|
||||
|
||||
if (connectionId) {
|
||||
results = results.filter((h) => h.connectionId === connectionId)
|
||||
}
|
||||
|
||||
if (status) {
|
||||
results = results.filter((h) => h.status === status)
|
||||
}
|
||||
|
||||
return results.slice(0, 200)
|
||||
}, [userId, connectionId, status])
|
||||
|
||||
const addEntry = async (entry: {
|
||||
connectionId: string
|
||||
query: string
|
||||
status: 'success' | 'error'
|
||||
durationMs?: number
|
||||
rowCount?: number
|
||||
errorMessage?: string
|
||||
}) => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
await db.queryHistory.add({
|
||||
id: crypto.randomUUID(),
|
||||
...entry,
|
||||
executedAt: new Date().toISOString(),
|
||||
_syncStatus: 'pending',
|
||||
})
|
||||
}
|
||||
|
||||
const remove = async (id: string) => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
const item = await db.queryHistory.get(id)
|
||||
if (!item) return
|
||||
|
||||
if (item._syncStatus === 'pending') {
|
||||
await db.queryHistory.delete(id)
|
||||
} else {
|
||||
await db.queryHistory.update(id, { _syncStatus: 'deleted' })
|
||||
}
|
||||
}
|
||||
|
||||
const clearAll = async (connectionId?: string) => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
|
||||
if (connectionId) {
|
||||
const entries = await db.queryHistory
|
||||
.where('connectionId')
|
||||
.equals(connectionId)
|
||||
.toArray()
|
||||
for (const entry of entries) {
|
||||
if (entry._syncStatus === 'pending') {
|
||||
await db.queryHistory.delete(entry.id)
|
||||
} else {
|
||||
await db.queryHistory.update(entry.id, { _syncStatus: 'deleted' })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const entries = await db.queryHistory.toArray()
|
||||
for (const entry of entries) {
|
||||
if (entry._syncStatus === 'pending') {
|
||||
await db.queryHistory.delete(entry.id)
|
||||
} else {
|
||||
await db.queryHistory.update(entry.id, { _syncStatus: 'deleted' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { history: history ?? [], addEntry, remove, clearAll }
|
||||
}
|
||||
101
apps/webapp/src/hooks/use-query-tabs.ts
Normal file
101
apps/webapp/src/hooks/use-query-tabs.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
'use client'
|
||||
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { useAuth } from '@clerk/nextjs'
|
||||
import { useCallback, useRef, useSyncExternalStore } from 'react'
|
||||
import { getDB } from '@/lib/dexie'
|
||||
|
||||
let activeTabId: string | null = null
|
||||
const activeTabListeners = new Set<() => void>()
|
||||
|
||||
function setActiveTabId(id: string) {
|
||||
activeTabId = id
|
||||
activeTabListeners.forEach((l) => l())
|
||||
}
|
||||
|
||||
function useActiveTabId() {
|
||||
return useSyncExternalStore(
|
||||
(cb) => {
|
||||
activeTabListeners.add(cb)
|
||||
return () => activeTabListeners.delete(cb)
|
||||
},
|
||||
() => activeTabId,
|
||||
() => activeTabId
|
||||
)
|
||||
}
|
||||
|
||||
let tabCounter = 1
|
||||
|
||||
export function useQueryTabs() {
|
||||
const { userId } = useAuth()
|
||||
const currentActiveTabId = useActiveTabId()
|
||||
const initializedRef = useRef(false)
|
||||
|
||||
const tabs = useLiveQuery(async () => {
|
||||
if (!userId) return []
|
||||
const db = getDB(userId)
|
||||
const all = await db.queryTabs.orderBy('updatedAt').toArray()
|
||||
|
||||
if (all.length === 0 && !initializedRef.current) {
|
||||
initializedRef.current = true
|
||||
const id = crypto.randomUUID()
|
||||
const now = new Date().toISOString()
|
||||
await db.queryTabs.add({ id, title: `Query ${tabCounter++}`, sql: '', updatedAt: now })
|
||||
setActiveTabId(id)
|
||||
return db.queryTabs.orderBy('updatedAt').toArray()
|
||||
}
|
||||
|
||||
if (!currentActiveTabId && all.length > 0) {
|
||||
setActiveTabId(all[all.length - 1].id)
|
||||
}
|
||||
|
||||
return all
|
||||
}, [userId, currentActiveTabId])
|
||||
|
||||
const addTab = useCallback(async () => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
const id = crypto.randomUUID()
|
||||
const now = new Date().toISOString()
|
||||
await db.queryTabs.add({ id, title: `Query ${tabCounter++}`, sql: '', updatedAt: now })
|
||||
setActiveTabId(id)
|
||||
}, [userId])
|
||||
|
||||
const removeTab = useCallback(
|
||||
async (id: string) => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
const all = await db.queryTabs.orderBy('updatedAt').toArray()
|
||||
if (all.length <= 1) return
|
||||
|
||||
await db.queryTabs.delete(id)
|
||||
if (currentActiveTabId === id) {
|
||||
const remaining = all.filter((t) => t.id !== id)
|
||||
setActiveTabId(remaining[remaining.length - 1].id)
|
||||
}
|
||||
},
|
||||
[userId, currentActiveTabId]
|
||||
)
|
||||
|
||||
const setActiveTab = useCallback((id: string) => {
|
||||
setActiveTabId(id)
|
||||
}, [])
|
||||
|
||||
const updateSql = useCallback(
|
||||
async (id: string, sql: string) => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
await db.queryTabs.update(id, { sql, updatedAt: new Date().toISOString() })
|
||||
},
|
||||
[userId]
|
||||
)
|
||||
|
||||
return {
|
||||
tabs: tabs ?? [],
|
||||
activeTabId: currentActiveTabId ?? '',
|
||||
addTab,
|
||||
removeTab,
|
||||
setActiveTab,
|
||||
updateSql,
|
||||
}
|
||||
}
|
||||
93
apps/webapp/src/hooks/use-saved-queries.ts
Normal file
93
apps/webapp/src/hooks/use-saved-queries.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
'use client'
|
||||
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { useAuth } from '@clerk/nextjs'
|
||||
import { getDB, type DexieSavedQuery } from '@/lib/dexie'
|
||||
|
||||
export function useSavedQueries(connectionId?: string, search?: string) {
|
||||
const { userId } = useAuth()
|
||||
|
||||
const queries = useLiveQuery(async () => {
|
||||
if (!userId) return []
|
||||
const db = getDB(userId)
|
||||
let collection = db.savedQueries
|
||||
.where('_syncStatus')
|
||||
.notEqual('deleted')
|
||||
|
||||
let results = await collection.reverse().sortBy('updatedAt')
|
||||
|
||||
if (connectionId) {
|
||||
results = results.filter((q) => q.connectionId === connectionId)
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const term = search.toLowerCase()
|
||||
results = results.filter(
|
||||
(q) => q.name.toLowerCase().includes(term) || q.query.toLowerCase().includes(term)
|
||||
)
|
||||
}
|
||||
|
||||
return results
|
||||
}, [userId, connectionId, search])
|
||||
|
||||
const create = async (input: {
|
||||
connectionId: string
|
||||
name: string
|
||||
query: string
|
||||
description?: string
|
||||
category?: string
|
||||
tags?: string[]
|
||||
}) => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
const now = new Date().toISOString()
|
||||
await db.savedQueries.add({
|
||||
id: crypto.randomUUID(),
|
||||
...input,
|
||||
usageCount: 0,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
_syncStatus: 'pending',
|
||||
})
|
||||
}
|
||||
|
||||
const update = async (
|
||||
id: string,
|
||||
updates: Partial<Pick<DexieSavedQuery, 'name' | 'query' | 'description' | 'category' | 'tags'>>
|
||||
) => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
await db.savedQueries.update(id, {
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString(),
|
||||
_syncStatus: 'pending',
|
||||
})
|
||||
}
|
||||
|
||||
const remove = async (id: string) => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
const item = await db.savedQueries.get(id)
|
||||
if (!item) return
|
||||
|
||||
if (item._syncStatus === 'pending') {
|
||||
await db.savedQueries.delete(id)
|
||||
} else {
|
||||
await db.savedQueries.update(id, { _syncStatus: 'deleted' })
|
||||
}
|
||||
}
|
||||
|
||||
const incrementUsage = async (id: string) => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
const item = await db.savedQueries.get(id)
|
||||
if (!item) return
|
||||
await db.savedQueries.update(id, {
|
||||
usageCount: item.usageCount + 1,
|
||||
updatedAt: new Date().toISOString(),
|
||||
_syncStatus: 'pending',
|
||||
})
|
||||
}
|
||||
|
||||
return { queries: queries ?? [], create, update, remove, incrementUsage }
|
||||
}
|
||||
33
apps/webapp/src/hooks/use-ui-state.ts
Normal file
33
apps/webapp/src/hooks/use-ui-state.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
'use client'
|
||||
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { useAuth } from '@clerk/nextjs'
|
||||
import { useCallback } from 'react'
|
||||
import { getDB } from '@/lib/dexie'
|
||||
|
||||
export function useUiState<T>(
|
||||
key: string,
|
||||
defaultValue: T
|
||||
): { value: T; set: (v: T) => Promise<void> } {
|
||||
const { userId } = useAuth()
|
||||
|
||||
const entry = useLiveQuery(async () => {
|
||||
if (!userId) return undefined
|
||||
const db = getDB(userId)
|
||||
return db.uiState.get(key)
|
||||
}, [userId, key])
|
||||
|
||||
const set = useCallback(
|
||||
async (value: T) => {
|
||||
if (!userId) return
|
||||
const db = getDB(userId)
|
||||
await db.uiState.put({ key, value })
|
||||
},
|
||||
[userId, key]
|
||||
)
|
||||
|
||||
return {
|
||||
value: (entry?.value as T) ?? defaultValue,
|
||||
set,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue