Files
supabase/apps/studio/state/explorer-query.ts
Joshen Lim fc3f6aaea8 Add source selector for explorer query tab (#49063)
## Context

This is just pulling out the relevant changes from
https://github.com/supabase/supabase/pull/49028 as I might have messed
up the stack while making changes down the PRs 🙏

Builds on the Query Tab in the Explorer UI, adds the source selector
component to run either a database query or a logs query - will
subsequently be looking into have the source selector component in the
QueryCell as well (within notebooks)

<img width="1095" height="594" alt="image"
src="https://github.com/user-attachments/assets/ab6e8d91-aed3-4845-9777-acbd7a3a7cb1"
/>
<img width="1086" height="667" alt="image"
src="https://github.com/user-attachments/assets/75d99063-755d-4ba6-a128-7ddcbbc9f161"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
  - Added query-source selection for Explorer and notebook queries.
- Supports log queries, database selection, and read-replica
connections.
- Added validation for custom log time ranges and retention limits, with
upgrade guidance when applicable.
  - Query source choices are saved and restored across sessions.
  - Changing sources clears previous results to prevent stale data.

- **Bug Fixes**
- Improved handling of unavailable log querying and missing database
connections.
- Legacy saved queries now fall back safely to the default database
source.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-14 01:07:20 +07:00

192 lines
5.4 KiB
TypeScript

import { untrustedSql, type UntrustedSqlFragment } from '@supabase/pg-meta'
import { LOCAL_STORAGE_KEYS, safeLocalStorage } from 'common'
import { proxy, ref, snapshot, useSnapshot } from 'valtio'
import { type QueryResult } from '@/components/interfaces/Explorer/types'
import {
cellSourceSchema,
createDefaultCellSource,
type CellSource,
} from '@/data/query-sources/query-source-registry'
export type ExplorerQueryDraft = {
id: string
projectRef: string
name: string
source: CellSource
uncheckedSql: UntrustedSqlFragment
updatedAt: number
}
export type ExplorerQueryResult = QueryResult & {
executedAt: number
}
type PersistedExplorerQueryDraft = {
name: string
source: CellSource
sql: string
updatedAt: number
}
type PersistedExplorerQueryDrafts = Record<string, PersistedExplorerQueryDraft>
type StorageLike = Pick<typeof safeLocalStorage, 'getItem' | 'setItem' | 'removeItem'>
const readPersistedDrafts = (storage: StorageLike, projectRef: string) => {
const raw = storage.getItem(LOCAL_STORAGE_KEYS.EXPLORER_QUERY_DRAFTS(projectRef))
if (!raw) return {} as PersistedExplorerQueryDrafts
try {
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
return Object.fromEntries(
Object.entries(parsed).flatMap(([id, value]) => {
if (
value === null ||
typeof value !== 'object' ||
!('name' in value) ||
typeof value.name !== 'string' ||
!('sql' in value) ||
typeof value.sql !== 'string' ||
!('updatedAt' in value) ||
typeof value.updatedAt !== 'number'
) {
return []
}
const parsedSource =
'source' in value ? cellSourceSchema.safeParse(value.source) : { success: false as const }
const source = parsedSource.success
? parsedSource.data
: createDefaultCellSource('database')
return [[id, { name: value.name, source, sql: value.sql, updatedAt: value.updatedAt }]]
})
)
} catch {
return {} as PersistedExplorerQueryDrafts
}
}
const writePersistedDrafts = (
storage: StorageLike,
projectRef: string,
drafts: PersistedExplorerQueryDrafts
) => {
const key = LOCAL_STORAGE_KEYS.EXPLORER_QUERY_DRAFTS(projectRef)
if (Object.keys(drafts).length === 0) storage.removeItem(key)
else storage.setItem(key, JSON.stringify(drafts))
}
export const createExplorerQueryState = (storage: StorageLike = safeLocalStorage) => {
const state = proxy({
drafts: {} as Record<string, ExplorerQueryDraft>,
results: {} as Record<string, ExplorerQueryResult>,
createDraft: ({
id,
projectRef,
name = 'Untitled query',
sql = '',
source = createDefaultCellSource('database'),
}: {
id: string
projectRef: string
name?: string
sql?: string
source?: CellSource
}) => {
const draft: ExplorerQueryDraft = {
id,
projectRef,
name,
source: cellSourceSchema.parse(source),
uncheckedSql: untrustedSql(sql),
updatedAt: Date.now(),
}
state.drafts[id] = draft
const persisted = readPersistedDrafts(storage, projectRef)
persisted[id] = { name, source: draft.source, sql, updatedAt: draft.updatedAt }
writePersistedDrafts(storage, projectRef, persisted)
return id
},
restoreDraft: ({ id, projectRef }: { id: string; projectRef: string }) => {
if (state.drafts[id]?.projectRef === projectRef) return true
const persisted = readPersistedDrafts(storage, projectRef)[id]
if (!persisted) return false
state.drafts[id] = {
id,
projectRef,
name: persisted.name,
source: persisted.source,
uncheckedSql: untrustedSql(persisted.sql),
updatedAt: persisted.updatedAt,
}
return true
},
updateDraft: ({
id,
name,
source,
sql,
}: {
id: string
name?: string
source?: CellSource
sql?: string
}) => {
const draft = state.drafts[id]
if (!draft) return
if (name !== undefined) draft.name = name
if (source !== undefined) {
draft.source = cellSourceSchema.parse(source)
delete state.results[id]
}
if (sql !== undefined) draft.uncheckedSql = untrustedSql(sql)
draft.updatedAt = Date.now()
const persisted = readPersistedDrafts(storage, draft.projectRef)
persisted[id] = {
name: draft.name,
source: draft.source,
sql: draft.uncheckedSql,
updatedAt: draft.updatedAt,
}
writePersistedDrafts(storage, draft.projectRef, persisted)
},
removeDraft: ({ id, projectRef }: { id: string; projectRef: string }) => {
if (state.drafts[id]?.projectRef === projectRef) {
delete state.drafts[id]
delete state.results[id]
}
const persisted = readPersistedDrafts(storage, projectRef)
delete persisted[id]
writePersistedDrafts(storage, projectRef, persisted)
},
setResult: ({ id, result }: { id: string; result: ExplorerQueryResult }) => {
state.results[id] = ref(result)
},
})
return state
}
export const explorerQueryState = createExplorerQueryState()
export const getExplorerQueryStateSnapshot = () => snapshot(explorerQueryState)
export const useExplorerQueryStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
useSnapshot(explorerQueryState, options)