mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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 -->
This commit is contained in:
1 parent
93d9d80535
commit
fc3f6aaea8
5 files changed
+323
-17
No files matched your search
@@ -0,0 +1,153 @@
|
||||
import { useFlag, useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { Check, ChevronDown } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
|
||||
import { DatabaseParametersSubMenu } from '@/components/interfaces/QuerySources/DatabaseParametersSubMenu'
|
||||
import { LogsCustomRangeDialog } from '@/components/interfaces/QuerySources/LogsCustomRangeDialog'
|
||||
import { LogsTimeRangeSubMenu } from '@/components/interfaces/QuerySources/LogsTimeRangeSubMenu'
|
||||
import { QuerySourceIcon } from '@/components/interfaces/QuerySources/QuerySourceIcon'
|
||||
import { maybeShowUpgradePromptIfNotEntitled } from '@/components/interfaces/Settings/Logs/Logs.utils'
|
||||
import UpgradePrompt from '@/components/interfaces/Settings/Logs/UpgradePrompt'
|
||||
import {
|
||||
createDefaultCellSource,
|
||||
QUERY_SOURCE_LABELS,
|
||||
QUERY_SOURCES,
|
||||
type CellSource,
|
||||
} from '@/data/query-sources/query-source-registry'
|
||||
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
||||
|
||||
export type ExplorerQuerySourceMenuProps = {
|
||||
source: CellSource
|
||||
onSourceChange: (source: CellSource) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Source binding and parameter controls shared by standalone Explorer queries
|
||||
* and notebook query-cell toolbars. The consumer owns the binding; this menu
|
||||
* only emits complete, validated-by-construction `CellSource` values.
|
||||
*/
|
||||
export const ExplorerQuerySourceMenu = ({
|
||||
source,
|
||||
onSourceChange,
|
||||
}: ExplorerQuerySourceMenuProps) => {
|
||||
const { ref } = useParams()
|
||||
const isLogsSourceEnabled = useFlag('sqlEditorLogsSource')
|
||||
const isOtelLogsEnabled = useFlag('otelLegacyLogs')
|
||||
const [isCustomRangeOpen, setIsCustomRangeOpen] = useState(false)
|
||||
const [showUpgradePrompt, setShowUpgradePrompt] = useState(false)
|
||||
const { getEntitlementNumericValue } = useCheckEntitlements('log.retention_days')
|
||||
const entitledToLogDays = getEntitlementNumericValue()
|
||||
|
||||
const availableSources = QUERY_SOURCES.filter(
|
||||
(candidate) =>
|
||||
candidate.type !== 'logs' ||
|
||||
(isLogsSourceEnabled && isOtelLogsEnabled) ||
|
||||
source.type === 'logs'
|
||||
)
|
||||
|
||||
const applyCustomRange = ({ from, to }: { from: Date; to: Date }) => {
|
||||
const fromIso = dayjs(from).startOf('day').toISOString()
|
||||
if (maybeShowUpgradePromptIfNotEntitled(fromIso, entitledToLogDays)) {
|
||||
setShowUpgradePrompt(true)
|
||||
return
|
||||
}
|
||||
|
||||
onSourceChange({
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: {
|
||||
time_range: {
|
||||
type: 'absolute',
|
||||
from: fromIso,
|
||||
to: dayjs(to).endOf('day').toISOString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="text"
|
||||
size="tiny"
|
||||
aria-label={`Query source: ${QUERY_SOURCE_LABELS[source.id]}`}
|
||||
icon={<QuerySourceIcon source={source.id} className="text-foreground-light" />}
|
||||
iconRight={<ChevronDown className="text-foreground-light" />}
|
||||
>
|
||||
{QUERY_SOURCE_LABELS[source.id]}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-60">
|
||||
{availableSources.map((candidate) => (
|
||||
<DropdownMenuItem
|
||||
key={candidate.id}
|
||||
className="justify-between"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault()
|
||||
if (candidate.id !== source.id) {
|
||||
onSourceChange(createDefaultCellSource(candidate.id))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-x-2">
|
||||
<QuerySourceIcon source={candidate.id} className="text-foreground-light" />
|
||||
{QUERY_SOURCE_LABELS[candidate.id]}
|
||||
</span>
|
||||
{source.id === candidate.id && <Check size={14} />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{source.type === 'database' ? (
|
||||
<DatabaseParametersSubMenu
|
||||
identifier={source.parameters.identifier ?? ref}
|
||||
onIdentifierChange={(identifier) =>
|
||||
onSourceChange({
|
||||
id: 'database',
|
||||
type: 'database',
|
||||
parameters: { identifier },
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<LogsTimeRangeSubMenu
|
||||
range={source.parameters.time_range}
|
||||
onRangeChange={(timeRange) =>
|
||||
onSourceChange({
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: timeRange },
|
||||
})
|
||||
}
|
||||
onOpenCustomRange={() => setIsCustomRangeOpen(true)}
|
||||
onShowUpgrade={() => setShowUpgradePrompt(true)}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{source.type === 'logs' && (
|
||||
<>
|
||||
<LogsCustomRangeDialog
|
||||
open={isCustomRangeOpen}
|
||||
onOpenChange={setIsCustomRangeOpen}
|
||||
onApply={applyCustomRange}
|
||||
/>
|
||||
<UpgradePrompt show={showUpgradePrompt} setShowUpgradePrompt={setShowUpgradePrompt} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { acceptUntrustedSql, untrustedSql } from '@supabase/pg-meta'
|
||||
import { useFlag } from 'common'
|
||||
import { CodeSquare, Eye, EyeOff, Play } from 'lucide-react'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { resolveLogTimeRange } from '../QuerySources/LogTimeRange.utils'
|
||||
import {
|
||||
ExplorerQuery,
|
||||
ExplorerQueryEditor,
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
ExplorerQueryResults,
|
||||
ExplorerQueryViewport,
|
||||
} from './ExplorerQuery'
|
||||
import { ExplorerQuerySourceMenu } from './ExplorerQuerySourceMenu'
|
||||
import {
|
||||
ExplorerToolbar,
|
||||
ExplorerToolbarAction,
|
||||
@@ -23,6 +26,15 @@ import { QueryResultTable } from './QueryResultTable'
|
||||
import { type QueryDisplay, type QueryResult } from './types'
|
||||
import { applyAutoLimit } from '@/components/interfaces/SQLEditor/SQLEditor.utils'
|
||||
import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
|
||||
import { isValidConnString } from '@/data/fetchers'
|
||||
import { useExecuteLogsSqlMutation } from '@/data/logs/execute-logs-sql-mutation'
|
||||
import { acceptUntrustedLogsSql, untrustedLogSql } from '@/data/logs/safe-analytics-sql'
|
||||
import {
|
||||
createDefaultCellSource,
|
||||
QUERY_SOURCE_REGISTRY,
|
||||
type CellSource,
|
||||
} from '@/data/query-sources/query-source-registry'
|
||||
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
|
||||
import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
|
||||
import { useLatest } from '@/hooks/misc/useLatest'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
@@ -32,6 +44,7 @@ export type QueryEditorProps = {
|
||||
variant: 'embedded' | 'viewport'
|
||||
title: string
|
||||
sql: string
|
||||
source?: CellSource
|
||||
result?: QueryResult
|
||||
rowLimit: number
|
||||
display?: QueryDisplay
|
||||
@@ -39,6 +52,7 @@ export type QueryEditorProps = {
|
||||
onTitleChange: (title: string) => void
|
||||
onSqlChange: (sql: string) => void
|
||||
onSqlCommit?: (sql: string) => void
|
||||
onSourceChange?: (source: CellSource) => void
|
||||
onResultChange: (result: QueryResult) => void
|
||||
onDisplayChange?: (display: QueryDisplay) => void
|
||||
}
|
||||
@@ -53,6 +67,7 @@ export const QueryEditor = ({
|
||||
variant,
|
||||
title,
|
||||
sql,
|
||||
source,
|
||||
result,
|
||||
rowLimit,
|
||||
display,
|
||||
@@ -60,35 +75,88 @@ export const QueryEditor = ({
|
||||
onTitleChange,
|
||||
onSqlChange,
|
||||
onSqlCommit,
|
||||
onSourceChange,
|
||||
onResultChange,
|
||||
onDisplayChange,
|
||||
}: QueryEditorProps) => {
|
||||
const sqlRef = useLatest(sql)
|
||||
const onSqlCommitRef = useLatest(onSqlCommit)
|
||||
|
||||
const isOtelLogsEnabled = useFlag('otelLegacyLogs')
|
||||
const { data: project, isPending: isLoadingProject } = useSelectedProjectQuery()
|
||||
|
||||
const view = display?.view ?? 'table'
|
||||
const columns = Object.keys(result?.rows?.[0] ?? {})
|
||||
const sourceBinding = source ?? createDefaultCellSource('database')
|
||||
|
||||
const [showQuery, setShowQuery] = useState(true)
|
||||
|
||||
const { mutate: executeSql, isPending: isExecuting } = useExecuteSqlMutation({
|
||||
const databaseIdentifier =
|
||||
sourceBinding.type === 'database' ? sourceBinding.parameters.identifier : undefined
|
||||
|
||||
const { data: databases, isPending: isLoadingDatabases } = useReadReplicasQuery(
|
||||
{ projectRef: project?.ref },
|
||||
{
|
||||
enabled:
|
||||
databaseIdentifier !== undefined &&
|
||||
project?.ref !== undefined &&
|
||||
databaseIdentifier !== project.ref,
|
||||
}
|
||||
)
|
||||
|
||||
const { mutate: executeSql, isPending: isExecutingSql } = useExecuteSqlMutation({
|
||||
onSuccess: (data) => onResultChange({ rows: data.result }),
|
||||
onError: (error) => onResultChange({ error }),
|
||||
})
|
||||
|
||||
const { mutate: executeLogsSql, isPending: isExecutingLogs } = useExecuteLogsSqlMutation({
|
||||
onSuccess: (data) => onResultChange({ rows: data.rows as readonly Record<string, unknown>[] }),
|
||||
onError: (error) => onResultChange({ error }),
|
||||
})
|
||||
|
||||
const isResolvingDatabase =
|
||||
databaseIdentifier !== undefined && databaseIdentifier !== project?.ref && isLoadingDatabases
|
||||
const isExecuting = isExecutingSql || isExecutingLogs
|
||||
const isBusy = isLoadingProject || isResolvingDatabase || isExecuting
|
||||
|
||||
const handleRunQuery = (sqlToRun: string = sql) => {
|
||||
if (!project || isLoadingProject || isExecuting || sqlToRun.trim().length === 0) return
|
||||
if (!project || isBusy || sqlToRun.trim().length === 0) return
|
||||
|
||||
onSqlCommit?.(sql)
|
||||
|
||||
if (sourceBinding.type === 'logs') {
|
||||
if (!isOtelLogsEnabled) {
|
||||
onResultChange({
|
||||
error: { message: "Querying logs isn't available for this project yet." },
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
executeLogsSql({
|
||||
projectRef: project.ref,
|
||||
sql: acceptUntrustedLogsSql(untrustedLogSql(sqlToRun)),
|
||||
range: resolveLogTimeRange(sourceBinding.parameters.time_range),
|
||||
endpoint: QUERY_SOURCE_REGISTRY.logs.endpoint,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const safeSql = acceptUntrustedSql(untrustedSql(sqlToRun))
|
||||
const limitedSql = applyAutoLimit(safeSql, rowLimit)
|
||||
const connectionString =
|
||||
databaseIdentifier === undefined || databaseIdentifier === project.ref
|
||||
? project.connectionString
|
||||
: databases?.find((database) => database.identifier === databaseIdentifier)
|
||||
?.connectionString
|
||||
|
||||
if (!isValidConnString(connectionString)) {
|
||||
onResultChange({ error: { message: 'Unable to run query: Connection string is missing' } })
|
||||
return
|
||||
}
|
||||
|
||||
executeSql({
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
connectionString,
|
||||
sql: limitedSql.sql,
|
||||
autoLimit: limitedSql.appendAutoLimit ? rowLimit : undefined,
|
||||
contextualInvalidation: true,
|
||||
@@ -107,6 +175,9 @@ export const QueryEditor = ({
|
||||
<ExplorerToolbarTitle onSaveTitle={onTitleChange}>{title}</ExplorerToolbarTitle>
|
||||
<ExplorerToolbarActions>
|
||||
{toolbarActions}
|
||||
{source && onSourceChange && (
|
||||
<ExplorerQuerySourceMenu source={source} onSourceChange={onSourceChange} />
|
||||
)}
|
||||
{display && onDisplayChange && (
|
||||
<DisplaySettingsButton
|
||||
result={result}
|
||||
|
||||
@@ -75,6 +75,7 @@ export const QueryTab = () => {
|
||||
variant="viewport"
|
||||
title={draft.name}
|
||||
sql={draft.uncheckedSql}
|
||||
source={draft.source}
|
||||
result={result}
|
||||
rowLimit={QUERY_ROW_LIMIT}
|
||||
onTitleChange={(value) => {
|
||||
@@ -83,6 +84,7 @@ export const QueryTab = () => {
|
||||
tabs.updateTab(createTabId('query', { id }), { label: name })
|
||||
}}
|
||||
onSqlChange={(sql) => explorerQueryState.updateDraft({ id, sql })}
|
||||
onSourceChange={(source) => explorerQueryState.updateDraft({ id, source })}
|
||||
onResultChange={handleResultChange}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -26,12 +26,57 @@ describe('explorer query drafts', () => {
|
||||
expect(secondState.restoreDraft({ id: 'query-1', projectRef: 'project-a' })).toBe(true)
|
||||
expect(secondState.drafts['query-1']).toMatchObject({
|
||||
name: 'Active users',
|
||||
source: { id: 'database', type: 'database', parameters: {} },
|
||||
uncheckedSql: 'select * from users',
|
||||
projectRef: 'project-a',
|
||||
})
|
||||
expect(secondState.restoreDraft({ id: 'query-1', projectRef: 'project-b' })).toBe(false)
|
||||
})
|
||||
|
||||
it('persists source parameters and clears stale results when they change', () => {
|
||||
const storage = createMemoryStorage()
|
||||
const state = createExplorerQueryState(storage)
|
||||
|
||||
state.createDraft({ id: 'query-1', projectRef: 'project-a', sql: 'select 1' })
|
||||
state.setResult({ id: 'query-1', result: { rows: [{ value: 1 }], executedAt: 1 } })
|
||||
state.updateDraft({
|
||||
id: 'query-1',
|
||||
source: {
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 3, unit: 'hour' } },
|
||||
},
|
||||
})
|
||||
|
||||
expect(state.results['query-1']).toBeUndefined()
|
||||
|
||||
const restored = createExplorerQueryState(storage)
|
||||
expect(restored.restoreDraft({ id: 'query-1', projectRef: 'project-a' })).toBe(true)
|
||||
expect(restored.drafts['query-1'].source).toEqual({
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 3, unit: 'hour' } },
|
||||
})
|
||||
})
|
||||
|
||||
it('restores pre-source drafts as database queries', () => {
|
||||
const storage = createMemoryStorage()
|
||||
storage.setItem(
|
||||
LOCAL_STORAGE_KEYS.EXPLORER_QUERY_DRAFTS('project-a'),
|
||||
JSON.stringify({
|
||||
'query-1': { name: 'Legacy query', sql: 'select 1', updatedAt: 1 },
|
||||
})
|
||||
)
|
||||
|
||||
const state = createExplorerQueryState(storage)
|
||||
expect(state.restoreDraft({ id: 'query-1', projectRef: 'project-a' })).toBe(true)
|
||||
expect(state.drafts['query-1'].source).toEqual({
|
||||
id: 'database',
|
||||
type: 'database',
|
||||
parameters: {},
|
||||
})
|
||||
})
|
||||
|
||||
it('removes the persisted draft and its session result when its tab closes', () => {
|
||||
const storage = createMemoryStorage()
|
||||
const state = createExplorerQueryState(storage)
|
||||
|
||||
@@ -3,11 +3,17 @@ 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
|
||||
}
|
||||
@@ -18,6 +24,7 @@ export type ExplorerQueryResult = QueryResult & {
|
||||
|
||||
type PersistedExplorerQueryDraft = {
|
||||
name: string
|
||||
source: CellSource
|
||||
sql: string
|
||||
updatedAt: number
|
||||
}
|
||||
@@ -35,18 +42,27 @@ const readPersistedDrafts = (storage: StorageLike, projectRef: string) => {
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(parsed).filter((entry): entry is [string, PersistedExplorerQueryDraft] => {
|
||||
const draft = entry[1]
|
||||
return (
|
||||
draft !== null &&
|
||||
typeof draft === 'object' &&
|
||||
'name' in draft &&
|
||||
typeof draft.name === 'string' &&
|
||||
'sql' in draft &&
|
||||
typeof draft.sql === 'string' &&
|
||||
'updatedAt' in draft &&
|
||||
typeof draft.updatedAt === 'number'
|
||||
)
|
||||
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 {
|
||||
@@ -74,23 +90,26 @@ export const createExplorerQueryState = (storage: StorageLike = safeLocalStorage
|
||||
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, sql, updatedAt: draft.updatedAt }
|
||||
persisted[id] = { name, source: draft.source, sql, updatedAt: draft.updatedAt }
|
||||
writePersistedDrafts(storage, projectRef, persisted)
|
||||
|
||||
return id
|
||||
@@ -106,23 +125,39 @@ export const createExplorerQueryState = (storage: StorageLike = safeLocalStorage
|
||||
id,
|
||||
projectRef,
|
||||
name: persisted.name,
|
||||
source: persisted.source,
|
||||
uncheckedSql: untrustedSql(persisted.sql),
|
||||
updatedAt: persisted.updatedAt,
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
updateDraft: ({ id, name, sql }: { id: string; name?: string; sql?: string }) => {
|
||||
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,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user