diff --git a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx
index 922bd07e57736..a4aa58474a60f 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx
@@ -2,7 +2,7 @@ import { Copy } from 'lucide-react'
import { useCallback, useState } from 'react'
import { toast } from 'sonner'
-import { ButtonTooltip } from 'components/ui/ButtonTooltip'
+import { Button } from 'ui'
import { getDecryptedValue } from 'data/vault/vault-secret-decrypted-value-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { copyToClipboard } from 'ui'
@@ -34,29 +34,14 @@ export const CopyEnvButton = ({
).then((values) => values.join('\n'))
copyToClipboard(envFile, () => {
- toast.success('Copied to clipboard')
+ toast.success('Copied to clipboard as .env file')
setIsLoading(false)
})
}, [serverOptions, values])
return (
- }
- onClick={onCopy}
- tooltip={{
- content: {
- text: (
-
- Copies an .env file with the configuration details
- to your clipboard.
-
- ),
- },
- }}
- >
- Copy env values
-
+ } onClick={onCopy}>
+ Copy all
+
)
}
diff --git a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx
index 7b9bae83c096e..ad4d675eaa4ef 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx
@@ -4,7 +4,9 @@ import { useState } from 'react'
import { useParams } from 'common'
import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
-import { Button, Input, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+import { Button, CardContent, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+import { Input } from 'ui-patterns/DataInputs/Input'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
export const DecryptedReadOnlyInput = ({
value,
@@ -41,52 +43,64 @@ export const DecryptedReadOnlyInput = ({
: value
return (
-
- {label}
-
-
-
-
-
-
- View parameter in Vault
-
-
- }
- value={renderedValue}
- type={secureEntry ? (isLoading ? 'text' : showHidden ? 'text' : 'password') : 'text'}
- descriptionText={descriptionText}
- layout="horizontal"
- actions={
- secureEntry ? (
- isLoading ? (
-
- } />
-
- ) : (
-
- : }
- onClick={() => setShowHidden(!showHidden)}
- />
-
- )
- ) : null
- }
- />
+
+
+ {label}
+
+
+
+
+
+
+ Open in Vault
+
+
+ }
+ description={descriptionText}
+ isReactForm={false}
+ >
+
+ }
+ />
+
+ ) : (
+
+ : }
+ onClick={() => setShowHidden(!showHidden)}
+ />
+
+ )
+ ) : null
+ }
+ />
+
+
)
}
diff --git a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/SimpleConfigurationDetails.tsx b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/SimpleConfigurationDetails.tsx
index 68085156f9f5e..eddf1305688c7 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/SimpleConfigurationDetails.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/SimpleConfigurationDetails.tsx
@@ -1,8 +1,14 @@
-import Link from '@ui/components/Typography/Link'
-import { ScaffoldSectionDescription, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
+import {
+ ScaffoldHeader,
+ ScaffoldSection,
+ ScaffoldSectionDescription,
+ ScaffoldSectionTitle,
+} from 'components/layouts/Scaffold'
+import { InlineLink } from 'components/ui/InlineLink'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import { DOCS_URL } from 'lib/constants'
import { Card } from 'ui'
import { getCatalogURI, getConnectionURL } from '../StorageSettings/StorageSettings.utils'
import { DESCRIPTIONS } from './constants'
@@ -11,9 +17,9 @@ import { DecryptedReadOnlyInput } from './DecryptedReadOnlyInput'
const wrapperMeta = {
options: [
- { name: 'vault_token', label: 'Vault Token', secureEntry: false },
- { name: 'warehouse', label: 'Warehouse', secureEntry: false },
- { name: 's3.endpoint', label: 'S3 Endpoint', secureEntry: false },
+ { name: 'vault_token', label: 'Vault token', secureEntry: false },
+ { name: 'warehouse', label: 'Warehouse name', secureEntry: false },
+ { name: 's3.endpoint', label: 'S3 endpoint', secureEntry: false },
{ name: 'catalog_uri', label: 'Catalog URI', secureEntry: false },
],
}
@@ -37,24 +43,23 @@ export const SimpleConfigurationDetails = ({ bucketName }: { bucketName: string
}
return (
-
-
+
+
- Configuration Details
-
- You can use the following configuration details to connect to the bucket from your code.
+ Connection details
+
+ Connect to this bucket from an Iceberg client.{' '}
+
+ Learn more
+
-
-
-
- To get AWS credentials, you can create them using the{' '}
-
- S3 Access Keys
- {' '}
- feature.
-
+
+
+
{wrapperMeta.options.map((option) => {
return (
-
+
)
}
diff --git a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/constants.ts b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/constants.ts
index c9851d3bef345..d7a2711792ddf 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/constants.ts
+++ b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/constants.ts
@@ -8,17 +8,17 @@ export const OPTION_ORDER = [
]
export const LABELS: Record = {
- vault_aws_access_key_id: 'S3 Access Key ID',
- vault_aws_secret_access_key: 'S3 Secret Access Key',
- vault_token: 'Catalog Token',
- warehouse: 'Warehouse Name',
- 's3.endpoint': 'S3 Endpoint',
+ vault_aws_access_key_id: 'S3 access key ID',
+ vault_aws_secret_access_key: 'S3 secret access key',
+ vault_token: 'Catalog token',
+ warehouse: 'Warehouse name',
+ 's3.endpoint': 'S3 endpoint',
catalog_uri: 'Catalog URI',
}
export const DESCRIPTIONS: Record = {
- vault_aws_access_key_id: 'Matches the AWS access key ID from a S3 Access Key.',
- vault_aws_secret_access_key: 'Matches the AWS secret access from a S3 Access Key.',
+ vault_aws_access_key_id: 'Matches the AWS access key ID from an S3 access key.',
+ vault_aws_secret_access_key: 'Matches the AWS secret access from an S3 access key.',
vault_token: 'Corresponds to the service role key.',
warehouse: 'Matches the name of the bucket.',
's3.endpoint': '',
diff --git a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/index.tsx b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/index.tsx
index 218a83eb2d8bf..13e365d7f2de7 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/index.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/index.tsx
@@ -1,7 +1,4 @@
-import { snakeCase, uniq } from 'lodash'
-import Link from 'next/link'
-import { useMemo } from 'react'
-
+import { useIsNewStorageUIEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants'
import { WRAPPER_HANDLERS } from 'components/interfaces/Integrations/Wrappers/Wrappers.constants'
import { WrapperMeta } from 'components/interfaces/Integrations/Wrappers/Wrappers.types'
@@ -10,14 +7,17 @@ import {
formatWrapperTables,
wrapperMetaComparator,
} from 'components/interfaces/Integrations/Wrappers/Wrappers.utils'
+import { BUCKET_TYPES } from 'components/interfaces/Storage/Storage.constants'
+import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import {
ScaffoldContainer,
ScaffoldHeader,
+ ScaffoldSection,
ScaffoldSectionDescription,
ScaffoldSectionTitle,
- ScaffoldTitle,
} from 'components/layouts/Scaffold'
import { DocsButton } from 'components/ui/DocsButton'
+import { InlineLink } from 'components/ui/InlineLink'
import {
DatabaseExtension,
useDatabaseExtensionsQuery,
@@ -29,20 +29,24 @@ import { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-cr
import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
+import { snakeCase, uniq } from 'lodash'
+import { Plus } from 'lucide-react'
+import Link from 'next/link'
+import { useMemo, useState } from 'react'
import {
- Alert_Shadcn_,
- AlertDescription_Shadcn_,
- AlertTitle_Shadcn_,
Button,
Card,
+ CardContent,
Table,
TableBody,
+ TableCell,
TableHead,
TableHeader,
TableRow,
- WarningIcon,
} from 'ui'
+import { Admonition } from 'ui-patterns/admonition'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+import { DeleteBucketModal } from '../DeleteBucketModal'
import { DESCRIPTIONS, LABELS, OPTION_ORDER } from './constants'
import { CopyEnvButton } from './CopyEnvButton'
import { DecryptedReadOnlyInput } from './DecryptedReadOnlyInput'
@@ -51,6 +55,8 @@ import { SimpleConfigurationDetails } from './SimpleConfigurationDetails'
import { useIcebergWrapperExtension } from './useIcebergWrapper'
export const AnalyticBucketDetails = ({ bucket }: { bucket: Bucket }) => {
+ const [modal, setModal] = useState<'delete' | null>(null)
+ const isStorageV2 = useIsNewStorageUIEnabled()
const { data: project } = useSelectedProjectQuery()
const { data: extensionsData } = useDatabaseExtensionsQuery({
@@ -131,6 +137,8 @@ export const AnalyticBucketDetails = ({ bucket }: { bucket: Bucket }) => {
const wrappersExtension = extensionsData?.find((ext) => ext.name === 'wrappers')
+ const config = BUCKET_TYPES['analytics']
+
const state = isFDWsLoading
? 'loading'
: extensionState === 'installed'
@@ -140,131 +148,225 @@ export const AnalyticBucketDetails = ({ bucket }: { bucket: Bucket }) => {
: extensionState
return (
-
-
-
-
- Analytics Bucket {bucket.name}
-
-
- Namespaces and tables connected to this bucket.
-
-
-
-
-
- {state === 'loading' && }
- {state === 'not-installed' && (
-
- )}
- {state === 'needs-upgrade' && (
-
- )}
- {state === 'added' && wrapperInstance && (
- <>
-
- {isLoadingNamespaces || isFDWsLoading ? (
-
- ) : namespaces.length === 0 ? (
-
- No namespaces in this bucket
-
- Create a namespace and add some data{' '}
-
- {' '}
- to get started
-
-
-
+ <>
+
] : []}
+ >
+
+ {state === 'loading' && (
+
+
+
+ )}
+ {state === 'not-installed' && (
+
+ )}
+ {state === 'needs-upgrade' && (
+
+ )}
+
+ {state === 'added' && wrapperInstance && (
+ <>
+ {isStorageV2 ? (
+
+
+
+ Tables
+
+ Analytics tables connected to this bucket.
+
+
+ } onClick={() => {}}>
+ New table
+
+
+
+
+
+
+
+ Name
+ Schema
+ Created at
+
+
+
+
+
+ No tables yet
+
+ Create an analytics table to get started
+
+
+
+
+
+
+
) : (
-
-
-
-
- Namespace
- Schema
- Tables
-
-
-
-
- {namespaces.map(({ namespace, schema, tables }) => (
-
- ))}
-
-
-
+
+
+ Namespaces
+
+ Connected namespaces and tables.
+
+
+
+ {isLoadingNamespaces || isFDWsLoading ? (
+
+ ) : namespaces.length === 0 ? (
+
+
+
+
+ Namespace
+ Schema
+ Tables
+
+
+
+
+
+
+
+ No namespaces in this bucket
+
+
+ Create a namespace and add some data
+
+
+
+
+
+
+ ) : (
+
+
+
+
+ Namespace
+ Schema
+ Tables
+
+
+
+
+ {namespaces.map(({ namespace, schema, tables }) => (
+
+ ))}
+
+
+
+ )}
+
)}
-
-
-
-
-
- Connection Details
-
- You can use the following parameters to connect to the bucket from an Iceberg
- client.
-
-
-
+
+
+
+
+ Configuration
+
+ Connect to this bucket from an Iceberg client.{' '}
+
+ Learn more
+
+
+
!option.hidden && wrapperValues[option.name]
)}
values={wrapperValues}
/>
-
+
+
+
+ {wrapperMeta.server.options
+ .filter((option) => !option.hidden && wrapperValues[option.name])
+ .sort((a, b) => OPTION_ORDER.indexOf(a.name) - OPTION_ORDER.indexOf(b.name))
+ .map((option) => {
+ return (
+
+ )
+ })}
+
+
+ >
+ )}
+ {state === 'missing' &&
}
+
+
+
+
+
+
+
Delete bucket
+
+ This will also delete any data in your bucket. Make sure you have a backup if
+ you want to keep your data.
+
-
-
- {wrapperMeta.server.options
- .filter((option) => !option.hidden && wrapperValues[option.name])
- .sort((a, b) => OPTION_ORDER.indexOf(a.name) - OPTION_ORDER.indexOf(b.name))
- .map((option) => {
- return (
-
- )
- })}
-
-
- >
- )}
- {state === 'missing' &&
}
-
-
+ {
+ setModal('delete')
+ }}
+ >
+ Delete bucket
+
+
+
+
+
+
+
+
setModal(null)}
+ />
+ >
)
}
@@ -284,24 +386,22 @@ const ExtensionNotInstalled = ({
return (
<>
-
-
-
- You need to install the wrappers extension to connect this Analytics bucket to the
- database.
-
-
+
+
- The {wrapperMeta.label} wrapper requires the Wrappers extension to be installed. You can
- install version {wrappersExtension?.installed_version}
+ The Wrappers extension is required in order to query analytics tables.{' '}
{databaseNeedsUpgrading &&
- ' which is below the minimum version that supports Iceberg wrapper'}
- . Please {databaseNeedsUpgrading && 'upgrade your database then '}install the{' '}
- wrappers extension to create this wrapper.
+ 'Please first upgrade your database and then install the extension.'}{' '}
+
+ Learn more
+
-
-
-
+ {}}>
- {databaseNeedsUpgrading ? 'Upgrade database' : 'Install wrappers extension'}
+ {databaseNeedsUpgrading ? 'Upgrade database' : 'Install extension'}
-
-
+
+
>
)
@@ -337,26 +437,19 @@ const ExtensionNeedsUpgrade = ({
return (
<>
-
-
-
- Your extension version is outdated for this wrapper.
-
-
+
+
The {wrapperMeta.label} wrapper requires a minimum extension version of{' '}
{wrapperMeta.minimumExtensionVersion}. You have version{' '}
{wrappersExtension?.installed_version} installed. Please{' '}
- {databaseNeedsUpgrading && 'upgrade your database then '}update the extension by
- disabling and enabling the wrappers extension to create
- this wrapper.
+ {databaseNeedsUpgrading && 'first upgrade your database, and then '}update the extension
+ by disabling and enabling the Wrappers extension.
-
- Warning: Before reinstalling the wrapper extension, you must first remove all existing
- wrappers. Afterward, you can recreate the wrappers.
+
+ Before reinstalling the wrapper extension, you must first remove all existing wrappers.
+ Afterward, you can recreate the wrappers.
-
-
- {databaseNeedsUpgrading ? 'Upgrade database' : 'View wrappers extension'}
+ {databaseNeedsUpgrading ? 'Upgrade database' : 'Extensions'}
-
-
+
+
>
)
@@ -385,20 +478,14 @@ const WrapperMissing = ({ bucketName }: { bucketName: string }) => {
return (
<>
-
-
-
- This Analytics bucket does not have a foreign data wrapper setup.
-
-
- You need to setup a wrapper to connect this bucket to the database.
-
-
+
+
+ The Iceberg Wrapper integration is required in order to query analytics tables.
- Setup a wrapper
+ Install wrapper
-
-
+
+
>
)
diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx
index 994707ac6c727..6f5171d26beb7 100644
--- a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx
+++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx
@@ -550,59 +550,61 @@ export const CreateBucketModal = ({
) : (
<>
{icebergWrapperExtensionState === 'installed' ? (
-
-
- Supabase will setup a
-
- foreign data wrapper
- {bucketName && {`${bucketName}_fdw`} }
-
-
- {' '}
- for easier access to the data. This action will also create{' '}
-
- S3 Access Keys
- {bucketName && (
- <>
- {' '}
- named {`${bucketName}_keys`}
- >
- )}
-
- and
+
+
+
+ Supabase will setup a
- four Vault Secrets
- {bucketName && (
- <>
- {' '}
- prefixed with{' '}
- {`${bucketName}_vault_`}
- >
- )}
+ foreign data wrapper
+ {bucketName && {`${bucketName}_fdw`} }
- .
-
-
-
- As a final step, you'll need to create an{' '}
- Iceberg namespace before you
- connect the Iceberg data to your database.
-
-
+
+ {' '}
+ for easier access to the data. This action will also create{' '}
+
+ S3 Access Keys
+ {bucketName && (
+ <>
+ {' '}
+ named {`${bucketName}_keys`}
+ >
+ )}
+
+ and
+
+ four Vault Secrets
+ {bucketName && (
+ <>
+ {' '}
+ prefixed with{' '}
+ {`${bucketName}_vault_`}
+ >
+ )}
+
+ .
+
+
+
+ As a final step, you'll need to create an{' '}
+ Iceberg namespace before you
+ connect the Iceberg data to your database.
+
+
+
) : (
-
+
You need to install the Iceberg wrapper extension to connect your Analytic
diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts b/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts
index 2023b7c044665..ad340ad84d234 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts
@@ -1,11 +1,19 @@
import { useParams } from 'common'
import { useBucketsQuery } from 'data/storage/buckets-query'
+import { useStorageV2Page } from '../Storage.utils'
export const useSelectedBucket = () => {
+ const page = useStorageV2Page()
const { ref, bucketId } = useParams()
const { data: buckets = [], isSuccess, isError, error } = useBucketsQuery({ projectRef: ref })
- const bucket = buckets.find((b) => b.id === bucketId)
+ const bucketsByType =
+ page === 'files'
+ ? buckets.filter((b) => b.type === 'STANDARD')
+ : page === 'analytics'
+ ? buckets.filter((b) => b.type === 'ANALYTICS')
+ : buckets
+ const bucket = bucketsByType.find((b) => b.id === bucketId)
return { bucket, isSuccess, isError, error }
}
diff --git a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx
index aebbc62f7ec5d..17c85bcc69a25 100644
--- a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx
+++ b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx
@@ -26,6 +26,7 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => {
const suffix = !!featurePreviewModal ? `?featurePreviewModal=${featurePreviewModal}` : ''
if (isStorageV2) {
+ // From old UI to new UI
if (pathname.endsWith('/storage/settings')) {
router.push(`/project/${ref}/storage/files/settings${suffix}`)
} else if (pathname.endsWith('/storage/policies')) {
@@ -38,6 +39,7 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => {
}
}
} else {
+ // From new UI to old UI
if (pathname.endsWith('/files/settings')) {
router.push(`/project/${ref}/storage/settings${suffix}`)
} else if (pathname.endsWith('/files/policies')) {
@@ -47,6 +49,12 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => {
pathname.endsWith('/analytics/buckets/[bucketId]')
) {
router.push(`/project/${ref}/storage/buckets/${bucketId}${suffix}`)
+ } else if (
+ pathname.endsWith('/storage/files') ||
+ pathname.endsWith('/storage/analytics') ||
+ pathname.endsWith('/storage/vectors')
+ ) {
+ router.push(`/project/${ref}/storage/buckets`)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
index ea619b5d3caf5..9e5083fd63326 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
@@ -22,6 +22,7 @@ import { useHotKey } from 'hooks/ui/useHotKey'
import { prepareMessagesForAPI } from 'lib/ai/message-utils'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import uuidv4 from 'lib/uuid'
+import type { AssistantModel } from 'state/ai-assistant-state'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { Button, cn, KeyboardShortcut } from 'ui'
@@ -59,6 +60,19 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
const { snippets } = useSqlEditorV2StateSnapshot()
const snap = useAiAssistantStateSnapshot()
+ const isPaidPlan = selectedOrganization?.plan?.id !== 'free'
+
+ const selectedModel = useMemo(() => {
+ const defaultModel: AssistantModel = isPaidPlan ? 'gpt-5' : 'gpt-5-mini'
+ const model = snap.model ?? defaultModel
+
+ if (!isPaidPlan && model === 'gpt-5') {
+ return 'gpt-5-mini'
+ }
+
+ return model
+ }, [isPaidPlan, snap.model])
+
const [updatedOptInSinceMCP] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.AI_ASSISTANT_MCP_OPT_IN,
false
@@ -201,6 +215,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
table: currentTable?.name,
chatName: currentChat,
orgSlug: selectedOrganizationRef.current?.slug,
+ model: selectedModel,
},
headers: { Authorization: authorizationHeader ?? '' },
}
@@ -640,6 +655,8 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
snap.setSqlSnippets(newSnippets)
}}
includeSnippetsInMessage={aiOptInLevel !== 'disabled'}
+ selectedModel={selectedModel}
+ onSelectModel={(model) => snap.setModel(model)}
/>
diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx
index edf949772a2b4..2839ee44f4fb5 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx
@@ -6,6 +6,7 @@ import { ExpandingTextArea } from 'ui'
import { cn } from 'ui/src/lib/utils'
import { ButtonTooltip } from '../ButtonTooltip'
import { type SqlSnippet } from './AIAssistant.types'
+import { ModelSelector } from './ModelSelector'
import { getSnippetContent, SnippetRow } from './SnippetRow'
export interface FormProps {
@@ -43,6 +44,10 @@ export interface FormProps {
className?: string
/* If currently editing an existing message */
isEditing?: boolean
+ /* The currently selected AI model */
+ selectedModel: 'gpt-5' | 'gpt-5-mini'
+ /* Callback when a model is chosen */
+ onSelectModel: (model: 'gpt-5' | 'gpt-5-mini') => void
}
const AssistantChatFormComponent = forwardRef(
@@ -62,6 +67,8 @@ const AssistantChatFormComponent = forwardRef(
includeSnippetsInMessage = false,
className,
isEditing = false,
+ selectedModel,
+ onSelectModel,
...props
},
ref
@@ -114,7 +121,7 @@ const AssistantChatFormComponent = forwardRef(
ref={textAreaRef}
disabled={disabled}
className={cn(
- 'text-sm pr-10 max-h-64',
+ 'text-sm pr-10 pb-9 max-h-64',
sqlSnippets && sqlSnippets.length > 0 && 'pt-10'
)}
placeholder={placeholder}
@@ -124,33 +131,39 @@ const AssistantChatFormComponent = forwardRef(
onChange={(event) => onValueChange(event)}
onKeyDown={handleKeyDown}
/>
-
- {loading ? (
- onStop ? (
+
+
+
+
+
+
+ {loading ? (
+ onStop ? (
+
}
+ onClick={onStop}
+ className="w-7 h-7 rounded-full p-0 text-center flex items-center justify-center"
+ tooltip={{ content: { side: 'top', text: 'Stop response' } }}
+ />
+ ) : (
+
+ )
+ ) : (
}
- onClick={onStop}
- className="w-7 h-7 rounded-full p-0 text-center flex items-center justify-center"
- tooltip={{ content: { side: 'top', text: 'Stop response' } }}
+ htmlType="submit"
+ aria-label="Send message"
+ icon={
}
+ disabled={!canSubmit}
+ className={cn(
+ 'w-7 h-7 rounded-full p-0 text-center flex items-center justify-center',
+ !canSubmit ? 'opacity-50' : 'opacity-100'
+ )}
+ tooltip={{ content: { side: 'top', text: 'Send message' } }}
/>
- ) : (
-
- )
- ) : (
-
}
- disabled={!canSubmit}
- className={cn(
- 'w-7 h-7 rounded-full p-0 text-center flex items-center justify-center',
- !canSubmit ? 'opacity-50' : 'opacity-100'
- )}
- tooltip={{ content: { side: 'top', text: 'Send message' } }}
- />
- )}
+ )}
+
diff --git a/apps/studio/components/ui/AIAssistantPanel/ModelSelector.tsx b/apps/studio/components/ui/AIAssistantPanel/ModelSelector.tsx
new file mode 100644
index 0000000000000..1a2a73a3608d5
--- /dev/null
+++ b/apps/studio/components/ui/AIAssistantPanel/ModelSelector.tsx
@@ -0,0 +1,103 @@
+import { Check, ChevronsUpDown } from 'lucide-react'
+import { useState } from 'react'
+
+import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
+import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
+import { useRouter } from 'next/router'
+import {
+ Badge,
+ Button,
+ CommandGroup_Shadcn_,
+ CommandItem_Shadcn_,
+ CommandList_Shadcn_,
+ Command_Shadcn_,
+ PopoverContent_Shadcn_,
+ PopoverTrigger_Shadcn_,
+ Popover_Shadcn_,
+ TooltipContent,
+ TooltipTrigger,
+ Tooltip,
+} from 'ui'
+
+interface ModelSelectorProps {
+ selectedModel: 'gpt-5' | 'gpt-5-mini'
+ onSelectModel: (model: 'gpt-5' | 'gpt-5-mini') => void
+}
+
+export const ModelSelector = ({ selectedModel, onSelectModel }: ModelSelectorProps) => {
+ const router = useRouter()
+ const { data: organization } = useSelectedOrganizationQuery()
+
+ const [open, setOpen] = useState(false)
+
+ const canAccessProModels = organization?.plan?.id !== 'free'
+ const slug = organization?.slug ?? '_'
+
+ const upgradeHref = `/org/${slug ?? '_'}/billing?panel=subscriptionPlan&source=ai-assistant-model`
+
+ const handleSelectModel = (model: 'gpt-5' | 'gpt-5-mini') => {
+ if (model === 'gpt-5' && !canAccessProModels) {
+ setOpen(false)
+ void router.push(upgradeHref)
+ return
+ }
+
+ onSelectModel(model)
+ setOpen(false)
+ }
+
+ return (
+
+
+ }
+ >
+ {selectedModel}
+
+
+
+
+
+
+ handleSelectModel('gpt-5-mini')}
+ className="flex justify-between"
+ >
+ gpt-5-mini
+ {selectedModel === 'gpt-5-mini' && }
+
+ handleSelectModel('gpt-5')}
+ className="flex justify-between"
+ >
+ gpt-5
+ {canAccessProModels ? (
+ selectedModel === 'gpt-5' ? (
+
+ ) : null
+ ) : (
+
+
+
+
+ Upgrade
+
+
+
+
+ gpt-5 is available on Pro plans and above
+
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/ui/InlineLink.tsx b/apps/studio/components/ui/InlineLink.tsx
index 1d69d11e68c49..d82ba4eefa0b2 100644
--- a/apps/studio/components/ui/InlineLink.tsx
+++ b/apps/studio/components/ui/InlineLink.tsx
@@ -11,7 +11,7 @@ interface InlineLinkProps {
}
export const InlineLinkClassName =
- 'underline transition underline-offset-2 decoration-foreground-lighter hover:decoration-foreground text-foreground'
+ 'underline transition underline-offset-2 decoration-foreground-lighter hover:decoration-foreground text-inherit hover:text-foreground'
export const InlineLink = ({
href,
diff --git a/apps/studio/lib/github.ts b/apps/studio/lib/github.ts
index d2493e2ddcfd1..454fa222ee710 100644
--- a/apps/studio/lib/github.ts
+++ b/apps/studio/lib/github.ts
@@ -2,18 +2,22 @@ import { LOCAL_STORAGE_KEYS } from 'common'
import { makeRandomString } from './helpers'
const GITHUB_INTEGRATION_APP_NAME =
- process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
- ? `supabase`
- : process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
- ? `supabase-staging`
- : `supabase-local-testing`
+ process.env.NIMBUS_PROD_PROJECTS_URL !== undefined
+ ? 'supabase-snap'
+ : process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
+ ? `supabase`
+ : process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
+ ? `supabase-staging`
+ : `supabase-local-testing`
const GITHUB_INTEGRATION_CLIENT_ID =
- process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
- ? `Iv1.b91a6d8eaa272168`
- : process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
- ? `Iv1.2681ab9a0360d8ad`
- : `Iv1.5022a3b44d150fbf`
+ process.env.NIMBUS_PROD_PROJECTS_URL !== undefined
+ ? 'Iv23li2pAiqDGgaSrP8q'
+ : process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
+ ? `Iv1.b91a6d8eaa272168`
+ : process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
+ ? `Iv1.2681ab9a0360d8ad`
+ : `Iv1.5022a3b44d150fbf`
const GITHUB_INTEGRATION_AUTHORIZATION_URL = `https://github.com/login/oauth/authorize?client_id=${GITHUB_INTEGRATION_CLIENT_ID}`
export const GITHUB_INTEGRATION_INSTALLATION_URL = `https://github.com/apps/${GITHUB_INTEGRATION_APP_NAME}/installations/new`
diff --git a/apps/studio/pages/api/ai/sql/generate-v4.ts b/apps/studio/pages/api/ai/sql/generate-v4.ts
index f57fc2d6c75c9..ecd441f503085 100644
--- a/apps/studio/pages/api/ai/sql/generate-v4.ts
+++ b/apps/studio/pages/api/ai/sql/generate-v4.ts
@@ -62,6 +62,7 @@ const requestBodySchema = z.object({
table: z.string().optional(),
chatName: z.string().optional(),
orgSlug: z.string().optional(),
+ model: z.enum(['gpt-5', 'gpt-5-mini']).optional(),
})
async function handlePost(req: NextApiRequest, res: NextApiResponse) {
@@ -79,7 +80,14 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
return res.status(400).json({ error: 'Invalid request body', issues: parseError.issues })
}
- const { messages: rawMessages, projectRef, connectionString, orgSlug, chatName } = data
+ const {
+ messages: rawMessages,
+ projectRef,
+ connectionString,
+ orgSlug,
+ chatName,
+ model: requestedModel,
+ } = data
let aiOptInLevel: AiOptInLevel = 'disabled'
let isLimited = false
@@ -139,7 +147,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
providerOptions,
} = await getModel({
provider: 'openai',
- model: 'gpt-5',
+ model: requestedModel ?? 'gpt-5',
routingKey: projectRef,
isLimited,
})
diff --git a/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx
new file mode 100644
index 0000000000000..19e8e6768178c
--- /dev/null
+++ b/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx
@@ -0,0 +1,48 @@
+import { useParams } from 'common'
+
+import { AnalyticBucketDetails } from 'components/interfaces/Storage/AnalyticBucketDetails'
+import StorageBucketsError from 'components/interfaces/Storage/StorageBucketsError'
+import { useSelectedBucket } from 'components/interfaces/Storage/StorageExplorer/useSelectedBucket'
+import DefaultLayout from 'components/layouts/DefaultLayout'
+import StorageLayout from 'components/layouts/StorageLayout/StorageLayout'
+import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
+import type { NextPageWithLayout } from 'types'
+
+const AnalyticsBucketPage: NextPageWithLayout = () => {
+ const { bucketId } = useParams()
+ const { data: project } = useSelectedProjectQuery()
+ const { projectRef } = useStorageExplorerStateSnapshot()
+ const { bucket, error, isSuccess, isError } = useSelectedBucket()
+
+ // [Joshen] Checking against projectRef from storage explorer to check if the store has initialized
+ if (!project || !projectRef) return null
+
+ return (
+
+ {isError &&
}
+
+ {isSuccess ? (
+ !bucket ? (
+
+
Bucket {bucketId} cannot be found
+
+ ) : bucket.type === 'ANALYTICS' ? (
+
+ ) : (
+
+
This bucket is not an analytics bucket
+
+ )
+ ) : null}
+
+ )
+}
+
+AnalyticsBucketPage.getLayout = (page) => (
+
+ {page}
+
+)
+
+export default AnalyticsBucketPage
diff --git a/apps/studio/state/ai-assistant-state.tsx b/apps/studio/state/ai-assistant-state.tsx
index cb138eebb8272..70b855b3a1d1d 100644
--- a/apps/studio/state/ai-assistant-state.tsx
+++ b/apps/studio/state/ai-assistant-state.tsx
@@ -17,6 +17,8 @@ export type AssistantMessageType = MessageType & { results?: { [id: string]: any
export type SqlSnippet = string | { label: string; content: string }
+export type AssistantModel = 'gpt-5' | 'gpt-5-mini'
+
type ChatSession = {
id: string
name: string
@@ -33,6 +35,7 @@ type AiAssistantData = {
tables: { schema: string; name: string }[]
chats: Record
activeChatId?: string
+ model: AssistantModel
}
// Data structure stored in IndexedDB
@@ -41,6 +44,7 @@ type StoredAiAssistantState = {
open: boolean
activeChatId?: string
chats: Record
+ model?: AssistantModel
}
const INITIAL_AI_ASSISTANT: AiAssistantData = {
@@ -51,6 +55,7 @@ const INITIAL_AI_ASSISTANT: AiAssistantData = {
tables: [],
chats: {},
activeChatId: undefined,
+ model: 'gpt-5',
}
const DB_NAME = 'ai-assistant-db'
@@ -165,6 +170,7 @@ async function tryMigrateFromLocalStorage(
open: parsedFromLocalStorage.open ?? false,
activeChatId: parsedFromLocalStorage.activeChatId,
chats: parsedFromLocalStorage.chats,
+ model: parsedFromLocalStorage.model ?? INITIAL_AI_ASSISTANT.model,
}
} else {
console.warn('Data in localStorage is not in the expected format, ignoring.')
@@ -251,6 +257,10 @@ export const createAiAssistantState = (): AiAssistantState => {
state.open = !state.open
},
+ setModel: (model: AssistantModel) => {
+ state.model = model
+ },
+
// Chat management
get activeChat(): ChatSession | undefined {
return state.activeChatId ? state.chats[state.activeChatId] : undefined
@@ -392,6 +402,7 @@ export const createAiAssistantState = (): AiAssistantState => {
state.open = persistedState.open
state.chats = persistedState.chats
state.activeChatId = persistedState.activeChatId
+ state.model = persistedState.model ?? INITIAL_AI_ASSISTANT.model
// Check URL param again to override loaded 'open' state if present
if (typeof window !== 'undefined') {
@@ -433,6 +444,7 @@ export type AiAssistantState = AiAssistantData & {
closeAssistant: () => void
toggleAssistant: () => void
activeChat: ChatSession | undefined
+ setModel: (model: AssistantModel) => void
newChat: (
options?: { name?: string } & Partial<
Pick
@@ -512,6 +524,7 @@ export const AiAssistantStateContextProvider = ({ children }: PropsWithChildren)
projectRef: project?.ref,
open: snap.open,
activeChatId: snap.activeChatId,
+ model: snap.model,
chats: snap.chats
? Object.entries(snap.chats).reduce((acc, [chatId, chat]) => {
// Limit messages before saving