Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,20 +123,24 @@ export const DestinationPanel = ({
pipelineId: existingDestination?.pipelineId,
})

const defaultValues = useMemo(
() => ({
const defaultValues = useMemo(() => {
const bigQueryConfig =
destinationData && 'big_query' in destinationData.config
? destinationData?.config.big_query
: null

return {
type: TypeEnum.enum.BigQuery,
name: destinationData?.name ?? '',
projectId: destinationData?.config?.big_query?.project_id ?? '',
datasetId: destinationData?.config?.big_query?.dataset_id ?? '',
projectId: bigQueryConfig?.project_id ?? '',
datasetId: bigQueryConfig?.dataset_id ?? '',
// For now, the password will always be set as empty for security reasons.
serviceAccountKey: destinationData?.config?.big_query?.service_account_key ?? '',
serviceAccountKey: bigQueryConfig?.service_account_key ?? '',
publicationName: pipelineData?.config.publication_name ?? '',
maxFillMs: pipelineData?.config?.batch?.max_fill_ms,
maxStalenessMins: destinationData?.config?.big_query?.max_staleness_mins,
}),
[destinationData, pipelineData]
)
maxStalenessMins: bigQueryConfig?.max_staleness_mins,
}
}, [destinationData, pipelineData])

const form = useForm<z.infer<typeof FormSchema>>({
mode: 'onBlur',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export const Destinations = () => {
sourceId={sourceId}
destinationId={destination.id}
destinationName={destination.name}
type={destination.config.big_query ? 'BigQuery' : 'Other'}
type={'big_query' in destination.config ? 'BigQuery' : 'Other'}
pipeline={pipeline}
error={pipelinesError}
isLoading={isPipelinesLoading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export function DiskManagementForm() {
const { data: org } = useSelectedOrganizationQuery()
const { setProjectStatus } = useSetProjectStatus()

const { data: resourceWarnings } = useResourceWarningsQuery()
const { data: resourceWarnings } = useResourceWarningsQuery({ ref: projectRef })
// [Joshen Cleanup] JFYI this client side filtering can be cleaned up once BE changes are live which will only return the warnings based on the provided ref
const projectResourceWarnings = (resourceWarnings ?? [])?.find(
(warning) => warning.project === project?.ref
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
isError: isErrorPermissions,
error: permissionsError,
} = usePermissionsQuery()
const { data: resourceWarnings } = useResourceWarningsQuery()
const { data: resourceWarnings } = useResourceWarningsQuery({ slug })

// Move all hooks to the top to comply with Rules of Hooks
const { data: integrations } = useOrgIntegrationsQuery({ orgSlug: organization?.slug })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export const DatabaseReadOnlyAlert = () => {
const { data: organization } = useSelectedOrganizationQuery()
const [showConfirmationModal, setShowConfirmationModal] = useState(false)

const { data: resourceWarnings } = useResourceWarningsQuery()

const { data: resourceWarnings } = useResourceWarningsQuery({ ref: projectRef })
// [Joshen Cleanup] JFYI this can be cleaned up once BE changes are live which will only return the warnings based on the provided ref
// No longer need to filter by ref on the client side
const isReadOnlyMode =
(resourceWarnings ?? [])?.find((warning) => warning.project === projectRef)
?.is_readonly_mode_enabled ?? false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export const InfrastructureActivity = () => {
})
const isFreePlan = organization?.plan?.id === 'free'

const { data: resourceWarnings } = useResourceWarningsQuery()
const { data: resourceWarnings } = useResourceWarningsQuery({ ref: projectRef })
// [Joshen Cleanup] JFYI this client side filtering can be cleaned up once BE changes are live which will only return the warnings based on the provided ref
const projectResourceWarnings = resourceWarnings?.find((x) => x.project === projectRef)

const { data: addons } = useProjectAddonsQuery({ projectRef })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { OrganizationProjectSelector } from 'components/ui/OrganizationProjectSe
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import {
Button,
cn,
CommandGroup_Shadcn_,
CommandItem_Shadcn_,
cn,
FormControl_Shadcn_,
FormField_Shadcn_,
} from 'ui'
Expand Down Expand Up @@ -76,6 +76,7 @@ function ProjectSelector({ form, orgSlug, projectRef }: ProjectSelectorProps) {
<OrganizationProjectSelector
key={orgSlug}
sameWidthAsTrigger
fetchOnMount
checkPosition="left"
slug={!orgSlug || orgSlug === NO_ORG_MARKER ? undefined : orgSlug}
selectedRef={field.value}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ describe('SupportFormPage', () => {
expect(submitSpy).toHaveBeenCalledTimes(1)
})
expect(submitSpy.mock.calls[0]?.[0]?.dashboardSentryIssueId).toBe(sentryIssueId)
})
}, 10_000)

test('includes initial error message from URL in submission payload', async () => {
const initialError = 'failed to fetch user data'
Expand Down Expand Up @@ -551,7 +551,7 @@ describe('SupportFormPage', () => {

const payload = submitSpy.mock.calls[0]?.[0]
expect(payload?.message).toMatch(initialError)
})
}, 10_000)

test('submits support request with problem category, library, and affected services', async () => {
const submitSpy = vi.fn()
Expand Down Expand Up @@ -640,7 +640,7 @@ describe('SupportFormPage', () => {
await waitFor(() => {
expect(screen.getByRole('heading', { name: /success/i })).toBeInTheDocument()
})
})
}, 10_000)

test('submits urgent login issues ticket for a different organization', async () => {
const submitSpy = vi.fn()
Expand Down Expand Up @@ -729,7 +729,7 @@ describe('SupportFormPage', () => {
await waitFor(() => {
expect(screen.getByRole('heading', { name: /success/i })).toBeInTheDocument()
})
})
}, 10_000)

test('submits database unresponsive ticket with initial error', async () => {
const submitSpy = vi.fn()
Expand Down Expand Up @@ -829,7 +829,7 @@ describe('SupportFormPage', () => {
await waitFor(() => {
expect(screen.getByRole('heading', { name: /success/i })).toBeInTheDocument()
})
})
}, 10_000)

test('when organization changes, project selector updates to match', async () => {
renderSupportFormPage()
Expand Down Expand Up @@ -968,7 +968,7 @@ describe('SupportFormPage', () => {
expect(screen.getByRole('heading', { name: /success/i })).toBeInTheDocument()
})
}
})
}, 10_000)

test('shows toast on submission error and allows form re-editing and resubmission', async () => {
const submitSpy = vi.fn()
Expand Down Expand Up @@ -1046,7 +1046,7 @@ describe('SupportFormPage', () => {
await waitFor(() => {
expect(screen.getByRole('heading', { name: /success/i })).toBeInTheDocument()
})
})
}, 10_000)

test('submits support request with attachments and includes attachment URLs in message', async () => {
const submitSpy = vi.fn()
Expand Down Expand Up @@ -1199,7 +1199,7 @@ describe('SupportFormPage', () => {
url.revokeObjectURL = originalRevokeObjectURL
vi.mocked(createSupportStorageClient).mockReset()
}
})
}, 10_000)

test('can submit form with no organizations and no projects', async () => {
const submitSpy = vi.fn()
Expand Down Expand Up @@ -1266,5 +1266,5 @@ describe('SupportFormPage', () => {
await waitFor(() => {
expect(screen.getByRole('heading', { name: /success/i })).toBeInTheDocument()
})
})
}, 10_000)
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PostgresTable } from '@supabase/postgres-meta'
import dayjs from 'dayjs'
import { isEmpty, isUndefined, noop } from 'lodash'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'

import { DocsButton } from 'components/ui/DocsButton'
Expand All @@ -24,6 +25,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useUrlState } from 'hooks/ui/useUrlState'
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import { DOCS_URL } from 'lib/constants'
import { usePHFlag } from 'hooks/ui/useFlag'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Badge, Checkbox, Input, SidePanel } from 'ui'
import { Admonition } from 'ui-patterns'
Expand All @@ -45,6 +47,12 @@ import {
generateTableFieldFromPostgresTable,
validateFields,
} from './TableEditor.utils'
import { TableTemplateSelector } from './TableQuickstart/TableTemplateSelector'
import { QuickstartVariant } from './TableQuickstart/types'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'

const NEW_PROJECT_THRESHOLD_DAYS = 7

export interface TableEditorProps {
table?: PostgresTable
Expand Down Expand Up @@ -90,6 +98,31 @@ export const TableEditor = ({
const { realtimeAll: realtimeEnabled } = useIsFeatureEnabled(['realtime:all'])
const { mutate: sendEvent } = useSendEventMutation()

/**
* Returns:
* - `QuickstartVariant`: user variation (if bucketed)
* - `false`: user not yet bucketed or targeted
* - `undefined`: posthog still loading
*/
const tableQuickstartVariant = usePHFlag<QuickstartVariant | false | undefined>('tableQuickstart')

const [quickstartDismissed, setQuickstartDismissed] = useLocalStorage(
LOCAL_STORAGE_KEYS.TABLE_QUICKSTART_DISMISSED,
false
)

const isRecentProject = useMemo(() => {
if (!project?.inserted_at) return false
return dayjs().diff(dayjs(project.inserted_at), 'day') < NEW_PROJECT_THRESHOLD_DAYS
}, [project?.inserted_at])

const shouldShowTemplateQuickstart =
isNewRecord &&
!isDuplicating &&
tableQuickstartVariant === QuickstartVariant.TEMPLATES &&
!quickstartDismissed &&
isRecentProject

const { docsRowLevelSecurityGuidePath } = useCustomContent(['docs:row_level_security_guide_path'])

const [params, setParams] = useUrlState()
Expand Down Expand Up @@ -280,6 +313,20 @@ export const TableEditor = ({
}
>
<SidePanel.Content className="space-y-10 py-6">
{shouldShowTemplateQuickstart && (
<TableTemplateSelector
variant={tableQuickstartVariant}
onSelectTemplate={(template) => {
const updates: Partial<TableField> = {}
if (template.name) updates.name = template.name
if (template.comment) updates.comment = template.comment
if (template.columns) updates.columns = template.columns
onUpdateField(updates)
}}
onDismiss={() => setQuickstartDismissed(true)}
disabled={false}
/>
)}
<Input
data-testid="table-name-input"
label="Name"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useState, useCallback, useEffect, useMemo } from 'react'
import { toast } from 'sonner'
import { Button, cn } from 'ui'
import { tableTemplates } from './templates'
import { QuickstartVariant } from './types'
import { convertTableSuggestionToTableField } from './utils'
import type { TableSuggestion } from './types'
import type { TableField } from '../TableEditor.types'

interface TableTemplateSelectorProps {
variant: Exclude<QuickstartVariant, QuickstartVariant.CONTROL> // [Sean] this will be used in PR #38934
onSelectTemplate: (tableField: Partial<TableField>) => void
onDismiss?: () => void
disabled?: boolean
}

const SUCCESS_MESSAGE_DURATION_MS = 3000

export const TableTemplateSelector = ({
variant: _variant,
onSelectTemplate,
onDismiss,
disabled,
}: TableTemplateSelectorProps) => {
const [activeCategory, setActiveCategory] = useState<string | null>(null) // null => All
const [selectedTemplate, setSelectedTemplate] = useState<TableSuggestion | null>(null)

const handleSelectTemplate = useCallback(
(template: TableSuggestion) => {
const tableField = convertTableSuggestionToTableField(template)
onSelectTemplate(tableField)
setSelectedTemplate(template)
toast.success(
`${template.tableName} template applied. You can add or modify the fields below.`,
{
duration: SUCCESS_MESSAGE_DURATION_MS,
}
)
},
[onSelectTemplate]
)

const categories = useMemo(() => Object.keys(tableTemplates), [])

useEffect(() => {
if (activeCategory === null && categories.length > 0) {
setActiveCategory(categories[0])
}
}, [categories, activeCategory])

const displayed = useMemo(
() => (activeCategory ? tableTemplates[activeCategory] || [] : []),
[activeCategory]
)

return (
<div className="rounded-lg border border-default bg-surface-75 p-4">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="text-sm font-medium">Start faster with a table template</h3>
<p className="text-xs text-foreground-lighter mt-1">
Save time by starting from a ready-made table schema.
</p>
</div>
{onDismiss && (
<Button type="text" size="tiny" onClick={onDismiss}>
Dismiss
</Button>
)}
</div>

<div className="flex flex-wrap gap-2 mb-3">
{categories.map((category) => (
<button
key={category}
onClick={() => setActiveCategory(category)}
disabled={disabled}
className={cn(
'px-2 py-1 rounded-md text-xs capitalize border',
activeCategory === category
? 'border-foreground bg-surface-200'
: 'border-default hover:border-foreground-muted hover:bg-surface-100',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
{category}
</button>
))}
</div>

<div className="grid gap-2">
{displayed.map((t) => (
<button
key={`${activeCategory}:${t.tableName}`}
onClick={() => handleSelectTemplate(t)}
disabled={disabled}
className={cn(
'text-left p-3 rounded-md border transition-all w-full',
selectedTemplate?.tableName === t.tableName
? 'border-foreground bg-surface-200'
: 'border-default hover:border-foreground-muted hover:bg-surface-100',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="text-sm font-medium font-mono">{t.tableName}</div>
{t.rationale && (
<div className="text-sm text-foreground-light mt-1">{t.rationale}</div>
)}
</div>
<div className="flex items-center gap-1 text-sm text-foreground-muted ml-3">
<span>{t.fields.length} columns</span>
</div>
</div>
</button>
))}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const LIMITS = {
MAX_PROMPT_LENGTH: 500,
MAX_TABLES_TO_GENERATE: 3,
MIN_TABLES_TO_GENERATE: 2,
} as const

export const AI_QUICK_IDEAS = [
'Recipe sharing app',
'Event ticketing system',
'Fitness tracker',
'Learning management platform',
] as const
Loading
Loading