diff --git a/apps/docs/content/guides/platform/compute-and-disk.mdx b/apps/docs/content/guides/platform/compute-and-disk.mdx index 0b4e26f21c0ca..74aeec43fefeb 100644 --- a/apps/docs/content/guides/platform/compute-and-disk.mdx +++ b/apps/docs/content/guides/platform/compute-and-disk.mdx @@ -108,13 +108,14 @@ Be aware that increasing IOPS or throughput incurs additional charges. When selecting your disk, it's essential to focus on the performance needs of your workload. Here's a comparison of our available disk types: -| | General Purpose SSD (gp3) | High Performance SSD (io2) | -| ----------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| **Use Case** | General workloads, development environments, small to medium databases | High-performance needs, large-scale databases, mission-critical applications | -| **Max Disk Size** | 16 TB | 60 TB | -| **Max IOPS** | 16,000 IOPS (at 32 GB disk size) | 80,000 IOPS (at 80 GB disk size) | -| **Throughput** | 125 MiB/s (default) to 1,000 MiB/s (maximum) | Automatically scales with IOPS | -| **Best For** | Great value for most use cases | Low latency and very high IOPS requirements | +| | General Purpose SSD (gp3) | High Performance SSD (io2) | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| **Use Case** | General workloads, development environments, small to medium databases | High-performance needs, large-scale databases, mission-critical applications | +| **Max Disk Size** | 16 TB | 60 TB | +| **Max IOPS** | 16,000 IOPS (at 32 GB disk size) | 80,000 IOPS (at 80 GB disk size) | +| **Throughput** | 125 MiB/s (default) to 1,000 MiB/s (maximum) | Automatically scales with IOPS | +| **Best For** | Great value for most use cases | Low latency and very high IOPS requirements | +| **Pricing** | Disk: 8 GB included, then $0.125 per GB
IOPS: 3,000 included, then $0.024 per IOPS
Throughput: 125 Mbps included, then $0.95 per Mbps | Disk: $0.195 per GB
IOPS: $0.119 per IOPS
Throughput: Scales with IOPS at no additional cost | For general, day-to-day operations, gp3 should be more than enough. If you need high throughput and IOPS for critical systems, io2 will provide the performance required. diff --git a/apps/docs/content/guides/platform/database-size.mdx b/apps/docs/content/guides/platform/database-size.mdx index bb9174b5f9f92..a4774aa07a084 100644 --- a/apps/docs/content/guides/platform/database-size.mdx +++ b/apps/docs/content/guides/platform/database-size.mdx @@ -139,8 +139,22 @@ Once you have reclaimed space, you can run the following to disable [read-only]( set default_transaction_read_only = 'off'; ``` +### Disk Size Distribution + +You can check the distribution of your disk size on your [project's compute and disk page](/dashboard/_/settings/compute-and-disk). + +![Disk Size Distribution](/docs/img/guides/platform/database-size/disk-size-distribution.png) + +Your disk size usage falls in three categories: + +- **Database** - Disk usage by the database. This includes the actual data, indexes, materialized views, ... +- **WAL** - Disk usage by the write-ahead log. The usage depends on your WAL settings and the amount of data being written to the database. +- **System** - Disk usage reserved by the system to ensure the database can operate smoothly. Users cannot modify this and it should only take very little space. + ### Reducing disk size Disks don't automatically downsize during normal operation. Once you have [reduced your database size](/docs/guides/platform/database-size#database-size), they _will_ automatically "right-size" during a [project upgrade](/docs/guides/platform/upgrading). The final disk size after the upgrade is 1.2x the size of the database with a minimum of 8 GB. For example, if your database size is 100GB, and you have a 200GB disk, the size after a project upgrade will be 120 GB. +In case you have a large WAL directory, you may [modify WAL settings](/docs/guides/database/custom-postgres-config) such as `max_wal_size`. Use at your own risk as changing these settings can have side effects. To query your current WAL size, use `SELECT SUM(size) FROM pg_ls_waldir()`. + In the event that your project is already on the latest version of Postgres and cannot be upgraded, a new version of Postgres will be released approximately every week which you can then upgrade to once it becomes available. diff --git a/apps/docs/public/img/guides/platform/database-size/disk-size-distribution.png b/apps/docs/public/img/guides/platform/database-size/disk-size-distribution.png new file mode 100644 index 0000000000000..32d0648b3d1a2 Binary files /dev/null and b/apps/docs/public/img/guides/platform/database-size/disk-size-distribution.png differ diff --git a/apps/studio/components/interfaces/Home/Connect/Connect.constants.ts b/apps/studio/components/interfaces/Connect/Connect.constants.ts similarity index 78% rename from apps/studio/components/interfaces/Home/Connect/Connect.constants.ts rename to apps/studio/components/interfaces/Connect/Connect.constants.ts index 80933e20cef18..4d94d82a20a5b 100644 --- a/apps/studio/components/interfaces/Home/Connect/Connect.constants.ts +++ b/apps/studio/components/interfaces/Connect/Connect.constants.ts @@ -1,3 +1,63 @@ +import { CodeBlockLang } from 'ui' + +export type DatabaseConnectionType = + | 'uri' + | 'psql' + | 'golang' + | 'jdbc' + | 'dotnet' + | 'nodejs' + | 'php' + | 'python' + | 'sqlalchemy' + +export const DATABASE_CONNECTION_TYPES: { + id: DatabaseConnectionType + label: string + contentType: 'input' | 'code' + lang: CodeBlockLang + fileTitle: string | undefined +}[] = [ + { id: 'uri', label: 'URI', contentType: 'input', lang: 'bash', fileTitle: undefined }, + { id: 'psql', label: 'PSQL', contentType: 'code', lang: 'bash', fileTitle: undefined }, + { id: 'golang', label: 'Golang', contentType: 'code', lang: 'go', fileTitle: '.env' }, + { id: 'jdbc', label: 'JDBC', contentType: 'input', lang: 'bash', fileTitle: undefined }, + { + id: 'dotnet', + label: '.NET', + contentType: 'code', + lang: 'csharp', + fileTitle: 'appsettings.json', + }, + { id: 'nodejs', label: 'Node.js', contentType: 'code', lang: 'js', fileTitle: '.env' }, + { id: 'php', label: 'PHP', contentType: 'code', lang: 'php', fileTitle: '.env' }, + { id: 'python', label: 'Python', contentType: 'code', lang: 'python', fileTitle: '.env' }, + { id: 'sqlalchemy', label: 'SQLAlchemy', contentType: 'code', lang: 'python', fileTitle: '.env' }, +] + +export const CONNECTION_PARAMETERS = { + host: { + key: 'host', + description: 'The hostname of your database', + }, + port: { + key: 'port', + description: 'Port number for the connection', + }, + database: { + key: 'database', + description: 'Default database name', + }, + user: { + key: 'user', + description: 'Database user', + }, + pool_mode: { + key: 'pool_mode', + description: 'Connection pooling behavior', + }, +} as const + export type ConnectionType = { key: string icon: string @@ -283,6 +343,7 @@ export const ORMS: ConnectionType[] = [ ] export const CONNECTION_TYPES = [ + { key: 'direct', label: 'Connection String', obj: [] }, { key: 'frameworks', label: 'App Frameworks', obj: FRAMEWORKS }, { key: 'mobiles', label: 'Mobile Frameworks', obj: MOBILES }, { key: 'orms', label: 'ORMs', obj: ORMS }, diff --git a/apps/studio/components/interfaces/Home/Connect/Connect.tsx b/apps/studio/components/interfaces/Connect/Connect.tsx similarity index 85% rename from apps/studio/components/interfaces/Home/Connect/Connect.tsx rename to apps/studio/components/interfaces/Connect/Connect.tsx index bcab28afdc2d2..9a4c306e578a9 100644 --- a/apps/studio/components/interfaces/Home/Connect/Connect.tsx +++ b/apps/studio/components/interfaces/Connect/Connect.tsx @@ -3,15 +3,17 @@ import { useParams } from 'common' import { ExternalLink, Plug } from 'lucide-react' import { useState } from 'react' -import { DatabaseConnectionString } from 'components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString' -import { PoolingModesModal } from 'components/interfaces/Settings/Database/PoolingModesModal' +import { DatabaseConnectionString } from 'components/interfaces/Connect/DatabaseConnectionString' +import { DatabaseConnectionString as OldDatabaseConnectionString } from 'components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString' + import Panel from 'components/ui/Panel' import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useFlag } from 'hooks/ui/useFlag' +import { useAppStateSnapshot } from 'state/app-state' import { Button, DIALOG_PADDING_X, - DIALOG_PADDING_X_SMALL, DIALOG_PADDING_Y, Dialog, DialogContent, @@ -31,7 +33,9 @@ import ConnectDropdown from './ConnectDropdown' import ConnectTabContent from './ConnectTabContent' const Connect = () => { + const state = useAppStateSnapshot() const { ref: projectRef } = useParams() + const connectDialogUpdate = useFlag('connectDialogUpdate') const [connectionObject, setConnectionObject] = useState(FRAMEWORKS) const [selectedParent, setSelectedParent] = useState(connectionObject[0].key) // aka nextjs @@ -145,14 +149,18 @@ const Connect = () => { return ( <> - + - - + Connect to your project Get the connection strings and environment variables for your app @@ -163,10 +171,7 @@ const Connect = () => { defaultValue="direct" onValueChange={(value) => handleConnectionType(value)} > - - - Connection String - + {CONNECTION_TYPES.map((type) => ( {type.label} @@ -183,6 +188,26 @@ const Connect = () => { .find((parent) => parent.key === selectedParent) ?.children.find((child) => child.key === selectedChild)?.children.length || 0) > 0 + if (type.key === 'direct') { + return ( + +
+ {connectDialogUpdate ? ( + + ) : ( +
+ +
+ )} +
+
+ ) + } + return ( { className={cn(DIALOG_PADDING_X, DIALOG_PADDING_Y, '!mt-0')} >
-
+
{ ) })} - - -
- ) } diff --git a/apps/studio/components/interfaces/Home/Connect/Connect.types.ts b/apps/studio/components/interfaces/Connect/Connect.types.ts similarity index 100% rename from apps/studio/components/interfaces/Home/Connect/Connect.types.ts rename to apps/studio/components/interfaces/Connect/Connect.types.ts diff --git a/apps/studio/components/interfaces/Home/Connect/Connect.utils.ts b/apps/studio/components/interfaces/Connect/Connect.utils.ts similarity index 100% rename from apps/studio/components/interfaces/Home/Connect/Connect.utils.ts rename to apps/studio/components/interfaces/Connect/Connect.utils.ts diff --git a/apps/studio/components/interfaces/Connect/ConnectDropdown.tsx b/apps/studio/components/interfaces/Connect/ConnectDropdown.tsx new file mode 100644 index 0000000000000..3a17f63e65956 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/ConnectDropdown.tsx @@ -0,0 +1,98 @@ +import { Box, Check, ChevronDown } from 'lucide-react' +import { useState } from 'react' + +import { + Button, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandInput_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + Command_Shadcn_, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, + Popover_Shadcn_, + cn, +} from 'ui' +import { ConnectionIcon } from './ConnectionIcon' + +interface ConnectDropdownProps { + state: string + updateState: (state: string) => void + label: string + items: any[] +} + +const ConnectDropdown = ({ + state, + updateState, + label, + + items, +}: ConnectDropdownProps) => { + const [open, setOpen] = useState(false) + + function onSelectLib(key: string) { + updateState(key) + setOpen(false) + } + + const selectedItem = items.find((item) => item.key === state) + + return ( + +
+ + {label} + + + + +
+ + + + + No results found. + + {items.map((item) => ( + { + onSelectLib(item.key) + setOpen(false) + }} + className="flex gap-2 items-center" + > + {item.icon ? : } + {item.label} + + + ))} + + + + +
+ ) +} + +export default ConnectDropdown diff --git a/apps/studio/components/interfaces/Home/Connect/ConnectTabContent.tsx b/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx similarity index 92% rename from apps/studio/components/interfaces/Home/Connect/ConnectTabContent.tsx rename to apps/studio/components/interfaces/Connect/ConnectTabContent.tsx index 005b806419eaa..9237c6ecf6cf1 100644 --- a/apps/studio/components/interfaces/Home/Connect/ConnectTabContent.tsx +++ b/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx @@ -1,5 +1,5 @@ import dynamic from 'next/dynamic' -import { forwardRef, HTMLAttributes } from 'react' +import { forwardRef, HTMLAttributes, useMemo } from 'react' import { useParams } from 'common' import { getConnectionStrings } from 'components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.utils' @@ -49,16 +49,15 @@ const ConnectTabContentNew = forwardRef( const connectionStringPoolerTransaction = connectionStringsPooler.uri const connectionStringPoolerSession = connectionStringsPooler.uri.replace('6543', '5432') - const ContentFile = dynamic( - () => import(`./content/${filePath}/content`), - { + const ContentFile = useMemo(() => { + return dynamic(() => import(`./content/${filePath}/content`), { loading: () => (
), - } - ) + }) + }, [filePath]) return (
diff --git a/apps/studio/components/interfaces/Home/Connect/ConnectTabs.tsx b/apps/studio/components/interfaces/Connect/ConnectTabs.tsx similarity index 81% rename from apps/studio/components/interfaces/Home/Connect/ConnectTabs.tsx rename to apps/studio/components/interfaces/Connect/ConnectTabs.tsx index f5871de3f9147..ec7b1b5a61fe8 100644 --- a/apps/studio/components/interfaces/Home/Connect/ConnectTabs.tsx +++ b/apps/studio/components/interfaces/Connect/ConnectTabs.tsx @@ -1,8 +1,7 @@ -import { Tabs_Shadcn_ } from 'ui' import { FileJson2 } from 'lucide-react' -import { TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' -import { TabsContent_Shadcn_ } from 'ui' -import React, { ReactNode } from 'react' +import { isValidElement, ReactNode } from 'react' + +import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' interface ConnectTabTriggerProps { value: string @@ -22,7 +21,7 @@ interface ConnectTabContentProps { const ConnectTabs = ({ children }: ConnectFileTabProps) => { const firstChild = children[0] - const defaultValue = React.isValidElement(firstChild) + const defaultValue = isValidElement(firstChild) ? (firstChild.props as any)?.children[0]?.props?.value || '' : null @@ -57,4 +56,4 @@ export const ConnectTabContent = ({ value, children }: ConnectTabContentProps) = ) } -export { ConnectTabTrigger, ConnectTabTriggers, ConnectTabs } +export { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers } diff --git a/apps/studio/components/interfaces/Home/Connect/ConnectionIcon.tsx b/apps/studio/components/interfaces/Connect/ConnectionIcon.tsx similarity index 89% rename from apps/studio/components/interfaces/Home/Connect/ConnectionIcon.tsx rename to apps/studio/components/interfaces/Connect/ConnectionIcon.tsx index a79acebc468bf..fd2452d29619d 100644 --- a/apps/studio/components/interfaces/Home/Connect/ConnectionIcon.tsx +++ b/apps/studio/components/interfaces/Connect/ConnectionIcon.tsx @@ -1,12 +1,13 @@ -import { BASE_PATH } from 'lib/constants' - import { useTheme } from 'next-themes' import Image from 'next/image' + +import { BASE_PATH } from 'lib/constants' + interface ConnectionIconProps { connection: any } -const ConnectionIcon = ({ connection }: ConnectionIconProps) => { +export const ConnectionIcon = ({ connection }: ConnectionIconProps) => { const { resolvedTheme } = useTheme() const imageFolder = ['ionic-angular'].includes(connection) ? 'icons/frameworks' : 'libraries' @@ -28,5 +29,3 @@ const ConnectionIcon = ({ connection }: ConnectionIconProps) => { /> ) } - -export default ConnectionIcon diff --git a/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx b/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx new file mode 100644 index 0000000000000..bce5602468da8 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx @@ -0,0 +1,265 @@ +import { ChevronRight, FileCode, X } from 'lucide-react' +import Link from 'next/link' + +import { + Button, + cn, + CodeBlock, + CodeBlockLang, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + WarningIcon, +} from 'ui' +import { ConnectionParameters } from './ConnectionParameters' +import { DirectConnectionIcon, TransactionIcon } from './PoolerIcons' + +interface ConnectionPanelProps { + type?: 'direct' | 'transaction' | 'session' + title: string + description: string + connectionString: string + ipv4Status: { + type: 'error' | 'success' + title: string + description?: string + link?: { text: string; url: string } + } + notice?: string[] + parameters?: Array<{ + key: string + value: string + description?: string + }> + contentType?: 'input' | 'code' + lang?: CodeBlockLang + fileTitle?: string + onCopyCallback: () => void +} + +const IPv4StatusIcon = ({ className, active }: { className?: string; active: boolean }) => { + return ( +
+ + + + + {!active ? ( +
+ +
+ ) : ( +
+ + + +
+ )} +
+ ) +} + +export const CodeBlockFileHeader = ({ title }: { title: string }) => { + return ( +
+
+ + {title} +
+
+ ) +} + +export const ConnectionPanel = ({ + type = 'direct', + title, + description, + connectionString, + ipv4Status, + notice, + parameters = [], + lang = 'bash', + fileTitle, + onCopyCallback, +}: ConnectionPanelProps) => { + return ( +
+
+

{title}

+

{description}

+
+ {fileTitle && } + + {notice && ( +
+ {notice?.map((text: string) => ( +

+ {text} +

+ ))} +
+ )} + {parameters.length > 0 && } +
+
+
+
+ {type !== 'session' && ( + <> +
+
+ {type === 'transaction' ? : } +
+
+ + {type === 'transaction' + ? 'Suitable for a large number of connected clients' + : 'Suitable for long-lived, persistent connections'} + +
+
+
+
+ + {type === 'transaction' + ? 'Pre-warmed connection pool to Postgres' + : 'Each client has a dedicated connection to Postgres'} + +
+
+ + )} + +
+
+ +
+
+ {ipv4Status.title} + {ipv4Status.description && ( + {ipv4Status.description} + )} + {ipv4Status.type === 'error' && ( + + Use Session Pooler if on a IPv4 network or purchase IPv4 addon + + )} + {ipv4Status.link && ( +
+ +
+ )} +
+
+ + {type === 'session' && ( +
+
+ +
+
+ Only use on a IPv4 network + + Use Direct Connection if connecting via an IPv6 network + +
+
+ )} + + {ipv4Status.type === 'error' && ( + + + + + +
+

+ A few major platforms are IPv4-only and may not work with a Direct Connection: +

+
+
Vercel
+
GitHub Actions
+
Render
+
Retool
+
+

+ If you wish to use a Direct Connection with these, please purchase{' '} + + IPv4 support + + . +

+

+ You may also use the{' '} + Session Pooler or{' '} + Transaction Pooler if you are on + a IPv4 network. +

+
+
+
+ )} +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx b/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx new file mode 100644 index 0000000000000..6a5e4ba168717 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx @@ -0,0 +1,91 @@ +import { Check, ChevronRight, Copy } from 'lucide-react' +import { useState } from 'react' + +import { copyToClipboard } from 'lib/helpers' +import { + Button, + cn, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + Separator, +} from 'ui' + +interface Parameter { + key: string + value: string + description?: string +} + +interface ConnectionParametersProps { + parameters: Parameter[] +} + +export const ConnectionParameters = ({ parameters }: ConnectionParametersProps) => { + const [isOpen, setIsOpen] = useState(false) + const [copiedMap, setCopiedMap] = useState>({}) + + return ( + + + + + +
+ {parameters.map((param) => ( +
+
+ {param.key}: + {param.value} + +
+
+ ))} +
+ +
+ For security reasons, your database password is never shown. +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx new file mode 100644 index 0000000000000..cd489d0dc0be4 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx @@ -0,0 +1,477 @@ +import { ChevronDown } from 'lucide-react' +import { HTMLAttributes, ReactNode, useState } from 'react' + +import { useParams } from 'common' +import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils' +import AlertError from 'components/ui/AlertError' +import DatabaseSelector from 'components/ui/DatabaseSelector' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query' +import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' +import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { pluckObjectFields } from 'lib/helpers' +import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' +import { + CodeBlock, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + Collapsible_Shadcn_, + DIALOG_PADDING_X, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Select_Shadcn_, + Separator, + cn, +} from 'ui' +import { Admonition } from 'ui-patterns' +import { + CONNECTION_PARAMETERS, + DATABASE_CONNECTION_TYPES, + DatabaseConnectionType, +} from './Connect.constants' +import { CodeBlockFileHeader, ConnectionPanel } from './ConnectionPanel' +import { getConnectionStrings, getPoolerTld } from './DatabaseSettings.utils' +import examples, { Example } from './DirectConnectionExamples' + +const StepLabel = ({ + number, + children, + ...props +}: { number: number; children: ReactNode } & HTMLAttributes) => ( +
+
+ {number} +
+ {children} +
+) + +export const DatabaseConnectionString = () => { + const { ref: projectRef } = useParams() + const state = useDatabaseSelectorStateSnapshot() + + const [selectedTab, setSelectedTab] = useState('uri') + + const { + data: poolingInfo, + error: poolingInfoError, + isLoading: isLoadingPoolingInfo, + isError: isErrorPoolingInfo, + isSuccess: isSuccessPoolingInfo, + } = usePoolingConfigurationQuery({ + projectRef, + }) + const poolingConfiguration = poolingInfo?.find((x) => x.identifier === state.selectedDatabaseId) + + const { + data: databases, + error: readReplicasError, + isLoading: isLoadingReadReplicas, + isError: isErrorReadReplicas, + isSuccess: isSuccessReadReplicas, + } = useReadReplicasQuery({ projectRef }) + + const error = poolingInfoError || readReplicasError + const isLoading = isLoadingPoolingInfo || isLoadingReadReplicas + const isError = isErrorPoolingInfo || isErrorReadReplicas + const isSuccess = isSuccessPoolingInfo && isSuccessReadReplicas + + const selectedDatabase = (databases ?? []).find( + (db) => db.identifier === state.selectedDatabaseId + ) + + const { data: addons } = useProjectAddonsQuery({ projectRef }) + const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? []) + + const { mutate: sendEvent } = useSendEventMutation() + + const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at'] + const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' } + const connectionInfo = pluckObjectFields(selectedDatabase || emptyState, DB_FIELDS) + + const handleCopy = (id: string) => { + const labelValue = DATABASE_CONNECTION_TYPES.find((type) => type.id === id)?.label + sendEvent({ + category: 'settings', + action: 'copy_connection_string', + label: labelValue ?? '', + }) + } + + const connectionStrings = + isSuccessPoolingInfo && poolingConfiguration !== undefined + ? getConnectionStrings(connectionInfo, poolingConfiguration, { + projectRef, + }) + : { + direct: { + uri: '', + psql: '', + golang: '', + jdbc: '', + dotnet: '', + nodejs: '', + php: '', + python: '', + sqlalchemy: '', + }, + pooler: { + uri: '', + psql: '', + golang: '', + jdbc: '', + dotnet: '', + nodejs: '', + php: '', + python: '', + sqlalchemy: '', + }, + } + + const poolerTld = + isSuccessPoolingInfo && poolingConfiguration !== undefined + ? getPoolerTld(poolingConfiguration?.connectionString) + : 'com' + + // @mildtomato - Possible reintroduce later + // + // const poolerConnStringSyntax = + // isSuccessPoolingInfo && poolingConfiguration !== undefined + // ? constructConnStringSyntax(poolingConfiguration?.connectionString, { + // selectedTab, + // usePoolerConnection: snap.usePoolerConnection, + // ref: projectRef as string, + // cloudProvider: isProjectLoading ? '' : project?.cloud_provider || '', + // region: isProjectLoading ? '' : project?.region || '', + // tld: snap.usePoolerConnection ? poolerTld : connectionTld, + // portNumber: `[5432 or 6543]`, + // }) + // : [] + // useEffect(() => { + // // if (poolingConfiguration?.pool_mode === 'session') { + // // setPoolingMode(poolingConfiguration.pool_mode) + // // } + // }, [poolingConfiguration?.pool_mode]) + + const lang = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.lang ?? 'bash' + const contentType = + DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.contentType ?? 'input' + + const example: Example | undefined = examples[selectedTab as keyof typeof examples] + + const exampleFiles = example?.files + const exampleInstallCommands = example?.installCommands + const examplePostInstallCommands = example?.postInstallCommands + const hasCodeExamples = exampleFiles || exampleInstallCommands + const fileTitle = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.fileTitle + + // [Refactor] See if we can do this in an immutable way, technically not a good practice to do this + let stepNumber = 0 + + return ( +
+
+
+ + Type + + + setSelectedTab(connectionType) + } + > + + + + + {DATABASE_CONNECTION_TYPES.map((type) => ( + + {type.label} + + ))} + + +
+ +
+ + {isLoading && ( +
+ +
+ )} + + {isError && ( +
+ +
+ )} + + {isSuccess && ( +
+ {/* // handle non terminal examples */} + {hasCodeExamples && ( +
+
+ + Install the following + + {exampleInstallCommands?.map((cmd, i) => ( + + {cmd} + + ))} +
+ {exampleFiles && exampleFiles?.length > 0 && ( +
+ + Add file to project + + {exampleFiles?.map((file, i) => ( +
+ + +
+ ))} +
+ )} +
+ )} + +
+ {hasCodeExamples && ( +
+ Choose type of connection +
+ )} +
+ handleCopy(selectedTab)} + /> + handleCopy(selectedTab)} + /> + {ipv4Addon && ( + +

+ If you are using Session Pooler, we recommend switching to Direct Connection. +

+
+ )} + + handleCopy(selectedTab)} + /> +
+
+ {examplePostInstallCommands && ( +
+
+ + Add the configuration package to read the settings + + {examplePostInstallCommands?.map((cmd, i) => ( + + {cmd} + + ))} +
+
+ )} +
+ )} + + {/* Possibly reintroduce later - @mildtomato */} + {/* + + +
+

+ How to connect to a different database or switch to another user +

+ +
+
+ +
+

+ You can use the following URI format to switch to a different database or user + {snap.usePoolerConnection ? ' when using connection pooling' : ''}. +

+

+ {poolerConnStringSyntax.map((x, idx) => { + if (x.tooltip) { + return ( + + + {x.value} + + {x.tooltip} + + ) + } else { + return ( + + {x.value} + + ) + } + })} +

+
+
+
*/} + + {selectedTab === 'python' && ( + <> + + + +
+

+ Connecting to SQL Alchemy +

+ +
+
+ +
+

+ Please use postgresql:// instead of postgres:// as your + dialect when connecting via SQLAlchemy. +

+

+ Example: + create_engine("postgresql+psycopg2://...") +

+

+
+
+
+ + )} +
+ ) +} diff --git a/apps/studio/components/interfaces/Connect/DatabaseSettings.utils.ts b/apps/studio/components/interfaces/Connect/DatabaseSettings.utils.ts new file mode 100644 index 0000000000000..398789c05d7b3 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/DatabaseSettings.utils.ts @@ -0,0 +1,388 @@ +import type { PoolingConfiguration } from 'data/database/pooling-configuration-query' + +type ConnectionStrings = { + psql: string + uri: string + golang: string + jdbc: string + dotnet: string + nodejs: string + php: string + python: string + sqlalchemy: string +} + +export const getConnectionStrings = ( + connectionInfo: { + db_user: string + db_port: number + db_host: string + db_name: string + }, + poolingInfo: PoolingConfiguration, + metadata: { + projectRef?: string + pgVersion?: string + } +): { + direct: ConnectionStrings + pooler: ConnectionStrings +} => { + const isMd5 = poolingInfo.connectionString.includes('options=reference') + const { projectRef } = metadata + const password = '[YOUR-PASSWORD]' + + // Direct connection variables + const directUser = connectionInfo.db_user + const directPort = connectionInfo.db_port + const directHost = connectionInfo.db_host + const directName = connectionInfo.db_name + + // Pooler connection variables + const poolerUser = poolingInfo.db_user + const poolerPort = poolingInfo.db_port + const poolerHost = poolingInfo.db_host + const poolerName = poolingInfo.db_name + + // Direct connection strings + const directPsqlString = isMd5 + ? `psql "postgresql://${directUser}:${password}@${directHost}:${directPort}/${directName}"` + : `psql -h ${directHost} -p ${directPort} -d ${directName} -U ${directUser}` + + const directUriString = `postgresql://${directUser}:${password}@${directHost}:${directPort}/${directName}` + + const directGolangString = `DATABASE_URL=${poolingInfo.connectionString}` + + const directJdbcString = `jdbc:postgresql://${directHost}:${directPort}/${directName}?user=${directUser}&password=${password}` + + // User Id=${directUser};Password=${password};Server=${directHost};Port=${directPort};Database=${directName}` + const directDotNetString = `{ + "ConnectionStrings": { + "DefaultConnection": "Host=${directHost};Database=${directName};Username=${directUser};Password=${password};SSL Mode=Require;Trust Server Certificate=true" + } +}` + + // `User Id=${poolerUser};Password=${password};Server=${poolerHost};Port=${poolerPort};Database=${poolerName}${isMd5 ? `;Options='reference=${projectRef}'` : ''}` + const poolerDotNetString = `{ + "ConnectionStrings": { + "DefaultConnection": "User Id=${poolerUser};Password=${password};Server=${poolerHost};Port=${poolerPort};Database=${poolerName}${isMd5 ? `;Options='reference=${projectRef}'` : ''}" + } +}` + + // Pooler connection strings + const poolerPsqlString = isMd5 + ? `psql "postgresql://${poolerUser}:${password}@${poolerHost}:${poolerPort}/${poolerName}?options=reference%3D${projectRef}"` + : `psql -h ${poolerHost} -p ${poolerPort} -d ${poolerName} -U ${poolerUser}.${projectRef}` + + const poolerUriString = poolingInfo.connectionString + + const nodejsPoolerUriString = `DATABASE_URL=${poolingInfo.connectionString}` + + const poolerGolangString = `user=${poolerUser} +password=${password} +host=${poolerHost} +port=${poolerPort} +dbname=${poolerName}${isMd5 ? `options=reference=${projectRef}` : ''}` + + const poolerJdbcString = `jdbc:postgresql://${poolerHost}:${poolerPort}/${poolerName}?user=${poolerUser}${isMd5 ? `&options=reference%3D${projectRef}` : ''}&password=${password}` + + const sqlalchemyString = `user=${directUser} +password=${password} +host=${directHost} +port=${directPort} +dbname=${directName}` + + const poolerSqlalchemyString = `user=${poolerUser} +password=${password} +host=${poolerHost} +port=${poolerPort} +dbname=${poolerName}` + + return { + direct: { + psql: directPsqlString, + uri: directUriString, + golang: directGolangString, + jdbc: directJdbcString, + dotnet: directDotNetString, + nodejs: nodejsPoolerUriString, + php: directGolangString, + python: directGolangString, + sqlalchemy: sqlalchemyString, + }, + pooler: { + psql: poolerPsqlString, + uri: poolerUriString, + golang: poolerGolangString, + jdbc: poolerJdbcString, + dotnet: poolerDotNetString, + nodejs: nodejsPoolerUriString, + php: poolerGolangString, + python: poolerGolangString, + sqlalchemy: poolerSqlalchemyString, + }, + } +} + +const DB_USER_DESC = 'Database user (e.g postgres)' +const DB_PASS_DESC = 'Database password' +const DB_NAME_DESC = 'Database name (e.g postgres)' +const PROJECT_REF_DESC = "Project's reference ID" +const PORT_NUMBER_DESC = 'Port number (Use 5432 if using prepared statements)' + +// [Joshen] This is to the best of interpreting the syntax from the API response +// // There's different format for PG13 (depending on authentication method being md5) and PG14 +export const constructConnStringSyntax = ( + connString: string, + { + selectedTab, + usePoolerConnection, + ref, + cloudProvider, + region, + tld, + portNumber, + }: { + selectedTab: 'uri' | 'psql' | 'golang' | 'jdbc' | 'dotnet' | 'nodejs' | 'php' | 'python' + usePoolerConnection: boolean + ref: string + cloudProvider: string + region: string + tld: string + portNumber: string + } +) => { + const isMd5 = connString.includes('options=reference') + const poolerHostDetails = [ + { value: cloudProvider.toLocaleLowerCase(), tooltip: 'Cloud provider' }, + { value: '-0-', tooltip: undefined }, + { value: region, tooltip: "Project's region" }, + { value: `.pooler.supabase.${tld}`, tooltip: undefined }, + ] + const dbHostDetails = [ + { value: 'db.', tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + { value: `.supabase.${tld}`, tooltip: undefined }, + ] + + if (selectedTab === 'uri' || selectedTab === 'nodejs') { + if (isMd5) { + return [ + { value: 'postgresql://', tooltip: undefined }, + { value: '[user]', tooltip: DB_USER_DESC }, + { value: ':', tooltip: undefined }, + { value: '[password]', tooltip: DB_PASS_DESC }, + { value: '@', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ':', tooltip: undefined }, + { value: portNumber, tooltip: PORT_NUMBER_DESC }, + { value: '/', tooltip: undefined }, + { value: '[db-name]', tooltip: DB_NAME_DESC }, + ...(usePoolerConnection + ? [ + { value: `?options=reference%3D`, tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + ] + : []), + ] + } else { + return [ + { value: 'postgresql://', tooltip: undefined }, + { value: '[user]', tooltip: DB_USER_DESC }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + ] + : []), + { value: ':', tooltip: undefined }, + { value: '[password]', tooltip: DB_PASS_DESC }, + { value: '@', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ':', tooltip: undefined }, + { value: portNumber, tooltip: PORT_NUMBER_DESC }, + { value: '/', tooltip: undefined }, + { value: '[db-name]', tooltip: DB_NAME_DESC }, + ] + } + } + + if (selectedTab === 'psql') { + if (isMd5) { + return [ + { value: 'psql "postgresql://', tooltip: undefined }, + { value: '[user]', tooltip: DB_USER_DESC }, + { value: ':', tooltip: undefined }, + { value: '[password]', tooltip: DB_PASS_DESC }, + { value: '@', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ':', tooltip: undefined }, + { value: portNumber, tooltip: PORT_NUMBER_DESC }, + { value: '/', tooltip: undefined }, + { value: '[db-name]', tooltip: DB_NAME_DESC }, + ...(usePoolerConnection + ? [ + { value: '?options=reference%3D', tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + ] + : []), + ] + } else { + return [ + { value: 'psql -h ', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ' -p ', tooltip: undefined }, + { value: portNumber, tooltip: PORT_NUMBER_DESC }, + { value: ' -d ', tooltip: undefined }, + { value: '[db-name]', tooltip: DB_NAME_DESC }, + { value: ' -U ', tooltip: undefined }, + { value: '[user]', tooltip: DB_USER_DESC }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + ] + : []), + ] + } + } + + if (selectedTab === 'golang' || selectedTab === 'php' || selectedTab === 'python') { + if (isMd5) { + return [ + { value: 'user=', tooltip: undefined }, + { value: '[user]', tooltip: DB_USER_DESC }, + { value: ' password=', tooltip: undefined }, + { value: '[password]', tooltip: DB_PASS_DESC }, + { value: ' host=', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ' port=', tooltip: undefined }, + { value: portNumber, tooltip: PORT_NUMBER_DESC }, + { value: ' dbname=', tooltip: undefined }, + { value: '[db-name]', tooltip: DB_NAME_DESC }, + ...(usePoolerConnection + ? [ + { value: ' options=reference=', tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + ] + : []), + ] + } else { + return [ + { value: 'user=', tooltip: undefined }, + { value: '[user]', tooltip: DB_USER_DESC }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + ] + : []), + { value: ' password=', tooltip: undefined }, + { value: '[password]', tooltip: DB_PASS_DESC }, + { value: ' host=', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ' port=', tooltip: undefined }, + { value: portNumber, tooltip: PORT_NUMBER_DESC }, + { value: ' dbname=', tooltip: undefined }, + { value: '[db-name]', tooltip: DB_NAME_DESC }, + ] + } + } + + if (selectedTab === 'jdbc') { + if (isMd5) { + return [ + { value: 'jdbc:postgresql://', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ':', tooltip: undefined }, + { value: portNumber, tooltip: PORT_NUMBER_DESC }, + { value: '/', tooltip: undefined }, + { value: '[db-name]', tooltip: DB_NAME_DESC }, + { value: '?user=', tooltip: undefined }, + { value: '[user]', tooltip: DB_USER_DESC }, + { value: '&password=', tooltip: undefined }, + { value: '[password]', tooltip: DB_PASS_DESC }, + ...(usePoolerConnection + ? [ + { value: '&options=reference%3D', tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + ] + : []), + ] + } else { + return [ + { value: 'jdbc:postgresql://', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: `:`, tooltip: undefined }, + { value: portNumber, tooltip: PORT_NUMBER_DESC }, + { value: '/', tooltip: undefined }, + { value: '[db-name]', tooltip: DB_NAME_DESC }, + { value: '?user=', tooltip: undefined }, + { value: '[user]', tooltip: DB_USER_DESC }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + ] + : []), + { value: '&password=', tooltip: undefined }, + { value: '[password]', tooltip: DB_PASS_DESC }, + ] + } + } + + if (selectedTab === 'dotnet') { + if (isMd5) { + return [ + { value: 'User Id=', tooltip: undefined }, + { value: '[user]', tooltip: DB_USER_DESC }, + { value: ';Password=', tooltip: undefined }, + { value: '[password]', tooltip: DB_PASS_DESC }, + { value: ';Server=', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ';Port=', tooltip: undefined }, + { value: portNumber, tooltip: PORT_NUMBER_DESC }, + { value: ';Database=', tooltip: undefined }, + { value: '[db-name]', tooltip: DB_NAME_DESC }, + ...(usePoolerConnection + ? [ + { value: ";Options='reference=", tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + { value: "'", tooltip: undefined }, + ] + : []), + ] + } else { + return [ + { value: 'User Id=', tooltip: undefined }, + { value: '[user]', tooltip: DB_USER_DESC }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: PROJECT_REF_DESC }, + ] + : []), + { value: ';Password=', tooltip: undefined }, + { value: '[password]', tooltip: DB_PASS_DESC }, + { value: ';Server=', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ';Port=', tooltip: undefined }, + { value: portNumber, tooltip: PORT_NUMBER_DESC }, + { value: ';Database=', tooltip: undefined }, + { value: '[db-name]', tooltip: DB_NAME_DESC }, + ] + } + } + + return [] +} + +export const getPoolerTld = (connString: string) => { + try { + const segment = connString.split('pooler.supabase.')[1] + const tld = segment.split(':6543')[0] + return tld + } catch { + return 'com' + } +} diff --git a/apps/studio/components/interfaces/Connect/DirectConnectionExamples.tsx b/apps/studio/components/interfaces/Connect/DirectConnectionExamples.tsx new file mode 100644 index 0000000000000..80084d1337d57 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/DirectConnectionExamples.tsx @@ -0,0 +1,153 @@ +export type Example = { + installCommands?: string[] + postInstallCommands?: string[] + files?: { + name: string + content: string + }[] +} + +const examples = { + nodejs: { + installCommands: ['npm install postgres'], + files: [ + { + name: 'db.js', + content: `import postgres from 'postgres' + +const connectionString = process.env.DATABASE_URL +const sql = postgres(connectionString) + +export default sql`, + }, + ], + }, + golang: { + installCommands: ['go get github.com/jackc/pgx/v5'], + files: [ + { + name: 'main.go', + content: `package main + +import ( + "context" + "log" + "os" + "github.com/jackc/pgx/v5" +) + +func main() { + conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatalf("Failed to connect to the database: %v", err) + } + defer conn.Close(context.Background()) + + // Example query to test connection + var version string + if err := conn.QueryRow(context.Background(), "SELECT version()").Scan(&version); err != nil { + log.Fatalf("Query failed: %v", err) + } + + log.Println("Connected to:", version) +}`, + }, + ], + }, + dotnet: { + installCommands: [ + 'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION', + ], + postInstallCommands: [ + 'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION', + ], + }, + python: { + installCommands: ['pip install python-dotenv psycopg2'], + files: [ + { + name: 'main.py', + content: `import psycopg2 +from dotenv import load_dotenv +import os + +# Load environment variables from .env +load_dotenv() + +# Fetch variables +USER = os.getenv("user") +PASSWORD = os.getenv("password") +HOST = os.getenv("host") +PORT = os.getenv("port") +DBNAME = os.getenv("dbname") + +# Connect to the database +try: + connection = psycopg2.connect( + user=USER, + password=PASSWORD, + host=HOST, + port=PORT, + dbname=DBNAME + ) + print("Connection successful!") + + # Create a cursor to execute SQL queries + cursor = connection.cursor() + + # Example query + cursor.execute("SELECT NOW();") + result = cursor.fetchone() + print("Current Time:", result) + + # Close the cursor and connection + cursor.close() + connection.close() + print("Connection closed.") + +except Exception as e: + print(f"Failed to connect: {e}")`, + }, + ], + }, + sqlalchemy: { + installCommands: ['pip install python-dotenv sqlalchemy psycopg2'], + files: [ + { + name: 'main.py', + content: `from sqlalchemy import create_engine +# from sqlalchemy.pool import NullPool +from dotenv import load_dotenv +import os + +# Load environment variables from .env +load_dotenv() + +# Fetch variables +USER = os.getenv("user") +PASSWORD = os.getenv("password") +HOST = os.getenv("host") +PORT = os.getenv("port") +DBNAME = os.getenv("dbname") + +# Construct the SQLAlchemy connection string +DATABASE_URL = f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?sslmode=require" + +# Create the SQLAlchemy engine +engine = create_engine(DATABASE_URL) +# If using Transaction Pooler or Session Pooler, we want to ensure we disable SQLAlchemy client side pooling - +# https://docs.sqlalchemy.org/en/20/core/pooling.html#switching-pool-implementations +# engine = create_engine(DATABASE_URL, poolclass=NullPool) + +# Test the connection +try: + with engine.connect() as connection: + print("Connection successful!") +except Exception as e: + print(f"Failed to connect: {e}")`, + }, + ], + }, +} + +export default examples diff --git a/apps/studio/components/interfaces/Connect/PoolerIcons.tsx b/apps/studio/components/interfaces/Connect/PoolerIcons.tsx new file mode 100644 index 0000000000000..945052bbeb987 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/PoolerIcons.tsx @@ -0,0 +1,485 @@ +import { AnimatePresence, motion } from 'framer-motion' +import { Fragment, useEffect, useState } from 'react' + +import { Database } from 'icons' +import { cn } from 'ui' + +// Add overall icon dimension controls +const ICON_WIDTH = 48 +const ICON_HEIGHT = 96 + +// Add these to your existing constants section +const LINE_WIDTH = 2 // SVG container width +const LINE_STROKE_WIDTH = 1 // Width of the actual line +const LINE_OFFSET = 1 // For centering the line in container + +const FlowingLine = ({ + x, + y1, + y2, + isActive, +}: { + x: number + y1: number + y2: number + isActive: boolean +}) => { + return ( + + {isActive && ( + + + + + + + + + + + + )} + + ) +} + +const TopRect = ({ isActive }: { isActive: boolean }) => ( + +) + +const BottomRect = ({ isActive }: { isActive: boolean }) => ( + + + + +) + +// Update existing constants to use these dimensions +const TOP_LINE_START = ICON_HEIGHT * 0.18 // 20% from top +const TOP_LINE_END = ICON_HEIGHT * 0.28 // 48% from top +const BOTTOM_LINE_START = ICON_HEIGHT * 0.4 // 65% from top +const BOTTOM_LINE_END = ICON_HEIGHT * 0.59 // 80% from top + +// Update rect positions and dimensions +const TOP_RECT_Y = ICON_HEIGHT * 0.32 // 55% from top +const BOTTOM_RECT_Y = ICON_HEIGHT * 0.64 // 85% from top +const RECT_X = ICON_WIDTH * 0.17 // ~17% from left +const RECT_WIDTH = ICON_WIDTH * 0.67 // ~67% of total width + +// Update circle positions +const CIRCLE_Y = ICON_HEIGHT * 0.13 // 13% from top +const CIRCLE_SPACING = ICON_WIDTH * 0.25 // 25% of width +const CIRCLE_START_X = ICON_WIDTH * 0.25 // 25% from left +const CIRCLE_RADIUS = ICON_WIDTH * 0.055 // ~3.8% of width + +// Static circle for SessionIcon +const ConnectionDot = ({ index, isActive }: { index: number; isActive: boolean }) => ( + +) + +export const TransactionIcon = () => { + const [dots, setDots] = useState([false, false, false]) + const [lines, setLines] = useState([false, false, false]) + const [bottomLineActive, setBottomLineActive] = useState(false) + + useEffect(() => { + // Watch lines state and update bottomLineActive accordingly + setBottomLineActive(lines.some(Boolean)) + }, [lines]) + + useEffect(() => { + const animateDot = (index: number) => { + // Clear previous state + setDots((prev) => { + const newState = [...prev] + newState[index] = false + return newState + }) + setLines((prev) => { + const newState = [...prev] + newState[index] = false + return newState + }) + + // Step 1: Animate dot in + setTimeout(() => { + setDots((prev) => { + const newState = [...prev] + newState[index] = true + return newState + }) + }, 0) + + // Step 2: After dot is in, wait, then show line + setTimeout(() => { + setLines((prev) => { + const newState = [...prev] + newState[index] = true + return newState + }) + }, 400) // Wait 400ms after dot appears before showing line + + // Step 3: Clear everything + setTimeout(() => { + setDots((prev) => { + const newState = [...prev] + newState[index] = false + return newState + }) + setLines((prev) => { + const newState = [...prev] + newState[index] = false + return newState + }) + }, 1000) // Total animation duration + } + + // Initial staggered animation + setTimeout(() => animateDot(0), 0) + setTimeout(() => animateDot(1), 200) + setTimeout(() => animateDot(2), 400) + + // Set up intervals for continuous animation + const intervals = [0, 1, 2].map((index) => + setInterval(() => animateDot(index), 3000 + index * 200) + ) + + return () => intervals.forEach(clearInterval) + }, []) + + return ( +
+ + {[0, 1, 2].map((index) => ( + + + {dots[index] && ( + + )} + + + ))} + + + + {[0, 1, 2].map((index) => ( + + ))} + + +
+ ) +} + +export const SessionIcon = () => { + const [topLineStates, setTopLineStates] = useState([false, false, false]) + const [bottomLineStates, setBottomLineStates] = useState([false, false, false]) + + useEffect(() => { + // Function to animate a single dot + const animateDot = (index: number) => { + setTopLineStates((prev) => { + const newState = [...prev] + newState[index] = true + return newState + }) + + setTimeout(() => { + setBottomLineStates((prev) => { + const newState = [...prev] + newState[index] = true + return newState + }) + }, 300) + + setTimeout(() => { + setTopLineStates((prev) => { + const newState = [...prev] + newState[index] = false + return newState + }) + setBottomLineStates((prev) => { + const newState = [...prev] + newState[index] = false + return newState + }) + }, 5000) + } + + // Start initial animations immediately with slight delays + setTimeout(() => animateDot(0), 100) + setTimeout(() => animateDot(1), 1500) + setTimeout(() => animateDot(2), 3000) + + // Set up intervals for subsequent animations + const intervals = [0, 1, 2].map((index) => + setInterval(() => animateDot(index), Math.random() * 3000 + 8000) + ) + + return () => intervals.forEach(clearInterval) + }, []) + + return ( +
+ + {[0, 1, 2].map((index) => ( + + + + ))} + state)} /> + state)} /> + + {[0, 1, 2].map((index) => ( + + + + + ))} +
+ ) +} + +export const DirectConnectionIcon = () => { + const [dots, setDots] = useState([false, false, false]) + const [lines, setLines] = useState([false, false, false]) + + useEffect(() => { + // Function to animate a single dot + const animateDot = (index: number) => { + setDots((prev) => { + const newState = [...prev] + newState[index] = true + return newState + }) + setLines((prev) => { + const newState = [...prev] + newState[index] = true + return newState + }) + + // Clear after 2.5s + setTimeout(() => { + setDots((prev) => { + const newState = [...prev] + newState[index] = false + return newState + }) + setLines((prev) => { + const newState = [...prev] + newState[index] = false + return newState + }) + }, 2500) + } + + // Initial staggered animation + // Start initial animations immediately with slight delays + setTimeout(() => animateDot(0), 100) + setTimeout(() => animateDot(1), 1500) + setTimeout(() => animateDot(2), 3000) + + // Set up intervals for continuous animation + const intervals = [0, 1, 2].map((index) => + setInterval(() => animateDot(index), Math.random() * 3000 + 8000) + ) + + return () => intervals.forEach(clearInterval) + }, []) + + return ( +
+ + {[0, 1, 2].map((index) => ( + + + {dots[index] && ( + + )} + + + ))} + state)} /> + + {[0, 1, 2].map((index) => ( + + ))} +
+ ) +} diff --git a/apps/studio/components/interfaces/Home/Connect/content/androidkotlin/supabasekt/content.tsx b/apps/studio/components/interfaces/Connect/content/androidkotlin/supabasekt/content.tsx similarity index 93% rename from apps/studio/components/interfaces/Home/Connect/content/androidkotlin/supabasekt/content.tsx rename to apps/studio/components/interfaces/Connect/content/androidkotlin/supabasekt/content.tsx index bcb90e3c9a5e8..9e1be901e598c 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/androidkotlin/supabasekt/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/androidkotlin/supabasekt/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTriggers, ConnectTabTrigger, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/astro/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/astro/supabasejs/content.tsx similarity index 91% rename from apps/studio/components/interfaces/Home/Connect/content/astro/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/astro/supabasejs/content.tsx index a88f865dde21f..1ce13c63f32b0 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/astro/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/astro/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTriggers, ConnectTabTrigger, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/drizzle/content.tsx b/apps/studio/components/interfaces/Connect/content/drizzle/content.tsx similarity index 92% rename from apps/studio/components/interfaces/Home/Connect/content/drizzle/content.tsx rename to apps/studio/components/interfaces/Connect/content/drizzle/content.tsx index de06febc66033..5f33d70a75935 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/drizzle/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/drizzle/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ connectionStringPooler }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/exporeactnative/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/exporeactnative/supabasejs/content.tsx similarity index 94% rename from apps/studio/components/interfaces/Home/Connect/content/exporeactnative/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/exporeactnative/supabasejs/content.tsx index 00fc587397770..fdae1dcdba17a 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/exporeactnative/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/exporeactnative/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/flutter/supabaseflutter/content.tsx b/apps/studio/components/interfaces/Connect/content/flutter/supabaseflutter/content.tsx similarity index 93% rename from apps/studio/components/interfaces/Home/Connect/content/flutter/supabaseflutter/content.tsx rename to apps/studio/components/interfaces/Connect/content/flutter/supabaseflutter/content.tsx index 8ae2c76a4d066..2eb7d6651eea0 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/flutter/supabaseflutter/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/flutter/supabaseflutter/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTriggers, ConnectTabTrigger, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/ionicangular/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx similarity index 96% rename from apps/studio/components/interfaces/Home/Connect/content/ionicangular/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx index 4a51e5f27a1b3..3beda75cbd043 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/ionicangular/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTriggers, ConnectTabTrigger, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/ionicreact/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/ionicreact/supabasejs/content.tsx similarity index 95% rename from apps/studio/components/interfaces/Home/Connect/content/ionicreact/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/ionicreact/supabasejs/content.tsx index 193a6e6719d98..0971e6979bcef 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/ionicreact/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/ionicreact/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTriggers, ConnectTabTrigger, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/nextjs/app/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/nextjs/app/supabasejs/content.tsx similarity index 96% rename from apps/studio/components/interfaces/Home/Connect/content/nextjs/app/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/nextjs/app/supabasejs/content.tsx index 2ff11fda1fcd8..e6d1be4fc26c8 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/nextjs/app/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/nextjs/app/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/nextjs/pages/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/nextjs/pages/supabasejs/content.tsx similarity index 93% rename from apps/studio/components/interfaces/Home/Connect/content/nextjs/pages/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/nextjs/pages/supabasejs/content.tsx index f22c1474db0b5..5ad87160931cb 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/nextjs/pages/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/nextjs/pages/supabasejs/content.tsx @@ -1,12 +1,12 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' +import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' import { + ConnectTabContent, ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, - ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' -import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' +} from 'components/interfaces/Connect/ConnectTabs' const ContentFile = ({ projectKeys }: ContentFileProps) => { return ( diff --git a/apps/studio/components/interfaces/Home/Connect/content/nuxt/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/nuxt/supabasejs/content.tsx similarity index 92% rename from apps/studio/components/interfaces/Home/Connect/content/nuxt/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/nuxt/supabasejs/content.tsx index 6b0428be949f6..b07237bad6ead 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/nuxt/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/nuxt/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/prisma/content.tsx b/apps/studio/components/interfaces/Connect/content/prisma/content.tsx similarity index 89% rename from apps/studio/components/interfaces/Home/Connect/content/prisma/content.tsx rename to apps/studio/components/interfaces/Connect/content/prisma/content.tsx index 1094ad05e2465..56c5371b9dbd8 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/prisma/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/prisma/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ connectionStringPooler }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/react/create-react-app/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/react/create-react-app/supabasejs/content.tsx similarity index 93% rename from apps/studio/components/interfaces/Home/Connect/content/react/create-react-app/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/react/create-react-app/supabasejs/content.tsx index de20afa32849d..c5b84f69027dc 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/react/create-react-app/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/react/create-react-app/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/react/vite/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/react/vite/supabasejs/content.tsx similarity index 93% rename from apps/studio/components/interfaces/Home/Connect/content/react/vite/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/react/vite/supabasejs/content.tsx index f7b0c82ff92f7..465d8bf097858 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/react/vite/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/react/vite/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/refine/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/refine/supabasejs/content.tsx similarity index 95% rename from apps/studio/components/interfaces/Home/Connect/content/refine/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/refine/supabasejs/content.tsx index 6c5cd2ea118dd..c87fdff6bdaa7 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/refine/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/refine/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/remix/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/remix/supabasejs/content.tsx similarity index 94% rename from apps/studio/components/interfaces/Home/Connect/content/remix/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/remix/supabasejs/content.tsx index 4487cf53afdf4..324eac69426eb 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/remix/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/remix/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTriggers, ConnectTabTrigger, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/solidjs/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/solidjs/supabasejs/content.tsx similarity index 92% rename from apps/studio/components/interfaces/Home/Connect/content/solidjs/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/solidjs/supabasejs/content.tsx index 9ab97360e6f8e..9cd17ddd2576e 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/solidjs/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/solidjs/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/sveltekit/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/sveltekit/supabasejs/content.tsx similarity index 93% rename from apps/studio/components/interfaces/Home/Connect/content/sveltekit/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/sveltekit/supabasejs/content.tsx index 7b058645c0767..5f326bfb8e18a 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/sveltekit/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/sveltekit/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTriggers, ConnectTabTrigger, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/swift/supabaseswift/content.tsx b/apps/studio/components/interfaces/Connect/content/swift/supabaseswift/content.tsx similarity index 92% rename from apps/studio/components/interfaces/Home/Connect/content/swift/supabaseswift/content.tsx rename to apps/studio/components/interfaces/Connect/content/swift/supabaseswift/content.tsx index 43ced097a497a..83b5c4f0a39ae 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/swift/supabaseswift/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/swift/supabaseswift/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTriggers, ConnectTabTrigger, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/Home/Connect/content/vuejs/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/vuejs/supabasejs/content.tsx similarity index 92% rename from apps/studio/components/interfaces/Home/Connect/content/vuejs/supabasejs/content.tsx rename to apps/studio/components/interfaces/Connect/content/vuejs/supabasejs/content.tsx index 5ab61da9c8ae7..9f10023c13557 100644 --- a/apps/studio/components/interfaces/Home/Connect/content/vuejs/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/vuejs/supabasejs/content.tsx @@ -1,11 +1,11 @@ -import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types' +import type { ContentFileProps } from 'components/interfaces/Connect/Connect.types' import { ConnectTabs, ConnectTabTrigger, ConnectTabTriggers, ConnectTabContent, -} from 'components/interfaces/Home/Connect/ConnectTabs' +} from 'components/interfaces/Connect/ConnectTabs' import { SimpleCodeBlock } from '@ui/components/SimpleCodeBlock' const ContentFile = ({ projectKeys }: ContentFileProps) => { diff --git a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx index 24c0638a1ebb5..fdf94851a4cc3 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx @@ -23,6 +23,7 @@ import { BillingChangeBadge } from '../ui/BillingChangeBadge' import FormMessage from '../ui/FormMessage' import { NoticeBar } from '../ui/NoticeBar' import { InstanceSpecs } from 'lib/constants' +import { DocsButton } from 'components/ui/DocsButton' /** * to do: this could be a type from api-types @@ -137,6 +138,14 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) {

Hardware resources allocated to your Postgres database

+ +
+ +
+ @@ -148,6 +151,13 @@ export function DiskSizeField({ {includedDiskGB > 0 && subscription?.plan.id && `Your plan includes ${includedDiskGB} GB of disk size for ${watchedStorageType}.`} + +
+ +
@@ -30,6 +30,7 @@ export default function DiskSpaceBar({ form }: DiskSpaceBarProps) { const { resolvedTheme } = useTheme() const { formState, watch } = form const isDarkMode = resolvedTheme?.includes('dark') + const project = useSelectedProject() const { data: diskUtil, @@ -38,30 +39,54 @@ export default function DiskSpaceBar({ form }: DiskSpaceBarProps) { projectRef: ref, }) - const usedSize = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100 - const totalSize = formState.defaultValues?.totalSize || 0 - const show = formState.dirtyFields.totalSize !== undefined && usedSize + const { data: diskBreakdown } = useDiskBreakdownQuery({ + projectRef: ref, + connectionString: project?.connectionString, + }) + + const diskBreakdownBytes = useMemo(() => { + return { + availableBytes: diskUtil?.metrics.fs_avail_bytes ?? 0, + totalUsedBytes: diskUtil?.metrics.fs_used_bytes ?? 0, + totalDiskSizeBytes: diskUtil?.metrics.fs_size_bytes, + dbSizeBytes: Math.max(0, diskBreakdown?.db_size_bytes ?? 0), + walSizeBytes: Math.max(0, diskBreakdown?.wal_size_bytes ?? 0), + systemBytes: Math.max( + 0, + (diskUtil?.metrics.fs_used_bytes ?? 0) - + (diskBreakdown?.db_size_bytes ?? 0) - + (diskBreakdown?.wal_size_bytes ?? 0) + ), + } + }, [diskUtil, diskBreakdown]) + + const showNewSize = formState.dirtyFields.totalSize !== undefined && diskBreakdown const newTotalSize = watch('totalSize') - const usedPercentage = (usedSize / totalSize) * 100 - const resizePercentage = AUTOSCALING_THRESHOLD * 100 + const totalSize = formState.defaultValues?.totalSize || 0 + const usedSizeTotal = Math.round(((diskBreakdownBytes?.totalUsedBytes ?? 0) / GB) * 100) / 100 + const usedTotalPercentage = Math.min((usedSizeTotal / totalSize) * 100, 100) - const newUsedPercentage = (usedSize / newTotalSize) * 100 - const newResizePercentage = AUTOSCALING_THRESHOLD * 100 + const usedSizeDatabase = Math.round(((diskBreakdownBytes?.dbSizeBytes ?? 0) / GB) * 100) / 100 + const usedPercentageDatabase = Math.min((usedSizeDatabase / totalSize) * 100, 100) + const newUsedPercentageDatabase = Math.min((usedSizeDatabase / newTotalSize) * 100, 100) - const { project } = useProjectContext() - const { data } = useDatabaseSizeQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - const { remainingDuration } = useRemainingDurationForDiskAttributeUpdate({ projectRef: ref }) + const usedSizeWAL = Math.round(((diskBreakdownBytes?.walSizeBytes ?? 0) / GB) * 100) / 100 + const usedPercentageWAL = Math.min((usedSizeWAL / totalSize) * 100, 100) + const newUsedPercentageWAL = Math.min((usedSizeWAL / newTotalSize) * 100, 100) + + const usedSizeSystem = Math.round(((diskBreakdownBytes?.systemBytes ?? 0) / GB) * 100) / 100 + const usedPercentageSystem = Math.min((usedSizeSystem / totalSize) * 100, 100) + const newUsedPercentageSystem = Math.min((usedSizeSystem / newTotalSize) * 100, 100) + + const resizePercentage = AUTOSCALING_THRESHOLD * 100 + const newResizePercentage = AUTOSCALING_THRESHOLD * 100 - const databaseSizeBytes = data ?? 0 return (
- {usedSize.toFixed(2)} + {usedSizeTotal.toFixed(2)} GB used of @@ -73,85 +98,67 @@ export default function DiskSpaceBar({ form }: DiskSpaceBarProps) {
- {!show ? ( - -
+ +
+
= 90 && remainingDuration > 0 - ? 'bg-destructive' - : 'bg-foreground', - 'relative overflow-hidden transition-all duration-500 ease-in-out' - )} - style={{ width: `${usedPercentage >= 100 ? 100 : usedPercentage}%` }} - > -
-
-
- - ) : ( - -
+ +
+ +
+ + {!showNewSize && (
= 100 ? 100 : newUsedPercentage}%` }} - > -
-
-
- - )} + className="bg-transparent-800 border-r transition-all duration-500 ease-in-out" + style={{ + width: `${resizePercentage - usedTotalPercentage <= 0 ? 0 : resizePercentage - usedTotalPercentage}%`, + }} + /> + )} +
+ - {show && ( + {showNewSize && (
- {!show && ( + {!showNewSize && (
@@ -202,24 +209,75 @@ export default function DiskSpaceBar({ form }: DiskSpaceBarProps) { )}
- {!show && ( -
-
-
- Used Space -
-
-
- Available space -
+ {!showNewSize && ( +
+ + + + + +
)}

Note: Disk Size refers to the total space your project occupies on disk, including the database itself (currently{' '} - {formatBytes(databaseSizeBytes, 2, 'GB')}), additional files like the - write-ahead log (WAL), and other internal resources. + {formatBytes(diskBreakdownBytes?.dbSizeBytes, 2, 'GB')}), additional files like + the write-ahead log (currently{' '} + {formatBytes(diskBreakdownBytes?.walSizeBytes, 2, 'GB')}), and other system + resources (currently {formatBytes(diskBreakdownBytes?.systemBytes, 2, 'GB')}). + Data can take 5 minutes to refresh.

) } + +const LegendItem = ({ + name, + description, + color, + size, +}: { + name: string + description: string + color: string + size: number +}) => ( + + +
+
+ {name} +
+ + +
+
+ + {name} - {formatBytes(size, 2, 'GB')} + +
+

{description}

+ + +) diff --git a/apps/studio/components/interfaces/Home/Connect/ConnectDropdown.tsx b/apps/studio/components/interfaces/Home/Connect/ConnectDropdown.tsx deleted file mode 100644 index 7f4217fb52a9e..0000000000000 --- a/apps/studio/components/interfaces/Home/Connect/ConnectDropdown.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Box, Check, ChevronDown } from 'lucide-react' -import { useState } from 'react' -import { - Button, - CommandEmpty_Shadcn_, - CommandGroup_Shadcn_, - CommandInput_Shadcn_, - CommandItem_Shadcn_, - CommandList_Shadcn_, - Command_Shadcn_, - PopoverContent_Shadcn_, - PopoverTrigger_Shadcn_, - Popover_Shadcn_, - cn, -} from 'ui' -import ConnectionIcon from './ConnectionIcon' - -interface ConnectDropdownProps { - state: string - updateState: (state: string) => void - label: string - items: any[] -} - -const ConnectDropdown = ({ - state, - updateState, - label, - - items, -}: ConnectDropdownProps) => { - const [open, setOpen] = useState(false) - - function onSelectLib(key: string) { - updateState(key) - setOpen(false) - } - - const selectedItem = items.find((item) => item.key === state) - - return ( - <> - -
- - {label} - - - - -
- - - - - No results found. - - {items.map((item) => ( - { - onSelectLib(item.key) - setOpen(false) - }} - className="flex gap-2 items-center" - > - {item.icon ? : } - {item.label} - - - ))} - - - - -
- - ) -} - -export default ConnectDropdown diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx index 5383630a19980..df7486172fe31 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx @@ -178,7 +178,7 @@ const DiskUsage = ({ {project.name} diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionStringMoved.tsx b/apps/studio/components/interfaces/Settings/Database/ConnectionStringMoved.tsx new file mode 100644 index 0000000000000..3852592b86259 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Database/ConnectionStringMoved.tsx @@ -0,0 +1,53 @@ +import { Button } from 'ui' +import { Plug, GitBranch, ChevronsUpDown, Pointer } from 'lucide-react' + +export const ConnectionStringMoved = () => { + return ( +
+
+

Connection string has moved

+

+ You can find Project connect details by clicking 'Connect' in the top bar +

+
+
+
+
+
+
+
+ + + +
+ +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx index ebfa5449d4365..14793602a1952 100644 --- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx @@ -56,6 +56,9 @@ interface DatabaseConnectionStringProps { appearance: 'default' | 'minimal' } +/** + * @deprecated Will be removed once `connectDialogUpdate` flag is persisted + */ export const DatabaseConnectionString = ({ appearance }: DatabaseConnectionStringProps) => { const project = useSelectedProject() const { ref: projectRef, connectionString } = useParams() diff --git a/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx b/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx index e870b0801225d..039c9ff5d81e8 100644 --- a/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx +++ b/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx @@ -50,14 +50,12 @@ const OrganizationDropdown = ({ isNewNav = false }: OrganizationDropdownProps) =
-
- -
+
diff --git a/apps/studio/components/layouts/AppLayout/ProjectDropdown.tsx b/apps/studio/components/layouts/AppLayout/ProjectDropdown.tsx index 5e477b21b4a58..e2d465bcaa6d2 100644 --- a/apps/studio/components/layouts/AppLayout/ProjectDropdown.tsx +++ b/apps/studio/components/layouts/AppLayout/ProjectDropdown.tsx @@ -117,58 +117,56 @@ const ProjectDropdown = ({ isNewNav = false }: ProjectDropdownProps) => { } return IS_PLATFORM ? ( -
- - - - - - - - - No projects found - - 7 ? 'h-[210px]' : ''}> - {projects?.map((project) => ( - - ))} - - - {projectCreationEnabled && ( - <> - - - { + + + + + + + + + No projects found + + 7 ? 'h-[210px]' : ''}> + {projects?.map((project) => ( + + ))} + + + {projectCreationEnabled && ( + <> + + + { + setOpen(false) + router.push(`/new/${selectedOrganization?.slug}`) + }} + onClick={() => setOpen(false)} + > + { setOpen(false) - router.push(`/new/${selectedOrganization?.slug}`) }} - onClick={() => setOpen(false)} + className="w-full flex items-center gap-2" > - { - setOpen(false) - }} - className="w-full flex items-center gap-2" - > - -

New project

- -
-
- - )} -
-
-
-
-
+ +

New project

+ + + + + )} + +
+
+
) : (
)} diff --git a/apps/studio/data/config/disk-breakdown-query.ts b/apps/studio/data/config/disk-breakdown-query.ts new file mode 100644 index 0000000000000..5218d40d51f90 --- /dev/null +++ b/apps/studio/data/config/disk-breakdown-query.ts @@ -0,0 +1,59 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import type { ResponseError } from 'types' +import { configKeys } from './keys' +import { executeSql } from 'data/sql/execute-sql-query' + +export type DiskBreakdownVariables = { + projectRef?: string + connectionString?: string +} + +type DiskBreakdownResult = { + db_size_bytes: number + wal_size_bytes: number +} + +export async function getDiskBreakdown( + { projectRef, connectionString }: DiskBreakdownVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('Project ref is required') + if (!connectionString) throw new Error('Connection string is required') + + const { result } = await executeSql( + { + projectRef, + connectionString, + sql: ` + SELECT + ( + SELECT + SUM(pg_database_size(pg_database.datname)) AS db_size_bytes + FROM + pg_database + ), + ( + SELECT SUM(size) + FROM + pg_ls_waldir() + ) AS wal_size_bytes`, + }, + signal + ) + + return result[0] as DiskBreakdownResult +} + +export type DiskBreakdownData = Awaited> +export type DiskBreakdownError = ResponseError + +export const useDiskBreakdownQuery = ( + { projectRef, connectionString }: DiskBreakdownVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + configKeys.diskBreakdown(projectRef), + ({ signal }) => getDiskBreakdown({ projectRef, connectionString }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } + ) diff --git a/apps/studio/data/config/keys.ts b/apps/studio/data/config/keys.ts index a2aab7748f18a..e154ed6548159 100644 --- a/apps/studio/data/config/keys.ts +++ b/apps/studio/data/config/keys.ts @@ -14,6 +14,8 @@ export const configKeys = { ['projects', projectRef, 'upgrade-status'] as const, diskAttributes: (projectRef: string | undefined) => ['projects', projectRef, 'disk-attributes'] as const, + diskBreakdown: (projectRef: string | undefined) => + ['projects', projectRef, 'disk-breakdown'] as const, diskUtilization: (projectRef: string | undefined) => ['projects', projectRef, 'disk-utilization'] as const, projectCreationPostgresVersions: ( diff --git a/apps/studio/lib/constants/telemetry.ts b/apps/studio/lib/constants/telemetry.ts index 87f03cf95b915..2030c342b8d24 100644 --- a/apps/studio/lib/constants/telemetry.ts +++ b/apps/studio/lib/constants/telemetry.ts @@ -4,6 +4,7 @@ export enum TELEMETRY_EVENTS { FEATURE_PREVIEWS = 'Dashboard UI Feature Previews', AI_ASSISTANT_V2 = 'AI Assistant V2', + CONNECT_UI = 'Connect UI', CRON_JOBS = 'Cron Jobs', } @@ -48,4 +49,5 @@ export enum TELEMETRY_VALUES { CRON_JOB_UPDATE_CLICKED = 'cron-job-update-clicked', CRON_JOB_CREATE_CLICKED = 'cron-job-create-clicked', CRON_JOBS_VIEW_PREVIOUS_RUNS = 'view-previous-runs-clicked', + COPY_CONNECTION_STRING = 'copy-connection-string', } diff --git a/apps/studio/pages/api/ai/sql/generate-v3.ts b/apps/studio/pages/api/ai/sql/generate-v3.ts index 55517ba1c0b3a..4218aa062d60d 100644 --- a/apps/studio/pages/api/ai/sql/generate-v3.ts +++ b/apps/studio/pages/api/ai/sql/generate-v3.ts @@ -1,25 +1,20 @@ import { openai } from '@ai-sdk/openai' -import { streamText } from 'ai' -import { getTools } from './tools' import pgMeta from '@supabase/pg-meta' -import { executeSql } from 'data/sql/execute-sql-query' +import { streamText } from 'ai' import { NextApiRequest, NextApiResponse } from 'next' +import { executeSql } from 'data/sql/execute-sql-query' +import { getTools } from './tools' + export const maxDuration = 30 const openAiKey = process.env.OPENAI_API_KEY const pgMetaSchemasList = pgMeta.schemas.list() export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (!openAiKey) { - return new Response( - JSON.stringify({ - error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.', - }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) + return res.status(400).json({ + error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.', + }) } const { method } = req @@ -28,13 +23,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) case 'POST': return handlePost(req, res) default: - return new Response( - JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }), - { - status: 405, - headers: { 'Content-Type': 'application/json', Allow: 'POST' }, - } - ) + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) } } diff --git a/apps/studio/pages/project/[ref]/index.tsx b/apps/studio/pages/project/[ref]/index.tsx index 836b11b3f7a71..b30d2e4dce7af 100644 --- a/apps/studio/pages/project/[ref]/index.tsx +++ b/apps/studio/pages/project/[ref]/index.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef } from 'react' import { useParams } from 'common' +import Connect from 'components/interfaces/Connect/Connect' import { ClientLibrary, ExampleProject } from 'components/interfaces/Home' -import Connect from 'components/interfaces/Home/Connect/Connect' import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants' import ProjectUsageSection from 'components/interfaces/Home/ProjectUsageSection' import { SecurityStatus } from 'components/interfaces/Home/SecurityStatus' @@ -13,6 +13,7 @@ import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper' import ProjectUpgradeFailedBanner from 'components/ui/ProjectUpgradeFailedBanner' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useIsOrioleDb, useSelectedProject } from 'hooks/misc/useSelectedProject' +import { useFlag } from 'hooks/ui/useFlag' import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import type { NextPageWithLayout } from 'types' @@ -28,6 +29,8 @@ import { } from 'ui' const Home: NextPageWithLayout = () => { + const connectDialogUpdate = useFlag('connectDialogUpdate') + const organization = useSelectedOrganization() const project = useSelectedProject() @@ -87,7 +90,9 @@ const Home: NextPageWithLayout = () => {
{project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && } {IS_PLATFORM && project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && } - {IS_PLATFORM && project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && } + {IS_PLATFORM && + project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && + !connectDialogUpdate && }
diff --git a/apps/studio/pages/project/[ref]/reports/database.tsx b/apps/studio/pages/project/[ref]/reports/database.tsx index 6584073bb36c6..0bc73dd479566 100644 --- a/apps/studio/pages/project/[ref]/reports/database.tsx +++ b/apps/studio/pages/project/[ref]/reports/database.tsx @@ -22,7 +22,6 @@ import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mu import { useDatabaseSizeQuery } from 'data/database/database-size-query' import { useDatabaseReport } from 'data/reports/database-report-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useFlag } from 'hooks/ui/useFlag' import { TIME_PERIODS_INFRA } from 'lib/constants/metrics' import { formatBytes } from 'lib/helpers' @@ -46,7 +45,6 @@ const DatabaseUsage = () => { const { project } = useProjectContext() const diskManagementV2 = useFlag('diskManagementV2') - const org = useSelectedOrganization() const state = useDatabaseSelectorStateSnapshot() const [dateRange, setDateRange] = useState(undefined) @@ -220,23 +218,20 @@ const DatabaseUsage = () => { renderer={(props) => { return (
-

- The data refreshes every 24 hours. -

Space used
{formatBytes(databaseSizeBytes, 2, 'GB')}
-
Total size
+
Provisioned disk size
{currentDiskSize} GB
{showNewDiskManagementUI ? ( @@ -309,11 +304,11 @@ const DatabaseUsage = () => {
diff --git a/apps/studio/pages/project/[ref]/settings/database.tsx b/apps/studio/pages/project/[ref]/settings/database.tsx index 902109f8a29ea..1ed247a0807ac 100644 --- a/apps/studio/pages/project/[ref]/settings/database.tsx +++ b/apps/studio/pages/project/[ref]/settings/database.tsx @@ -1,25 +1,25 @@ +import { DiskManagementPanelForm } from 'components/interfaces/DiskManagement/DiskManagementPanelForm' import { ConnectionPooling, DatabaseSettings, NetworkRestrictions, } from 'components/interfaces/Settings/Database' -import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout' -import type { NextPageWithLayout } from 'types' - -import { DiskManagementPanelForm } from 'components/interfaces/DiskManagement/DiskManagementPanelForm' import BannedIPs from 'components/interfaces/Settings/Database/BannedIPs' +import { ConnectionStringMoved } from 'components/interfaces/Settings/Database/ConnectionStringMoved' import { DatabaseReadOnlyAlert } from 'components/interfaces/Settings/Database/DatabaseReadOnlyAlert' import { DatabaseConnectionString } from 'components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString' +import DiskSizeConfiguration from 'components/interfaces/Settings/Database/DiskSizeConfiguration' import { PoolingModesModal } from 'components/interfaces/Settings/Database/PoolingModesModal' import SSLConfiguration from 'components/interfaces/Settings/Database/SSLConfiguration' +import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout' import { ScaffoldContainer, ScaffoldHeader, ScaffoldTitle } from 'components/layouts/Scaffold' -import DiskSizeConfiguration from 'components/interfaces/Settings/Database/DiskSizeConfiguration' -import { useFlag } from 'hooks/ui/useFlag' import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { useFlag } from 'hooks/ui/useFlag' +import type { NextPageWithLayout } from 'types' const ProjectSettings: NextPageWithLayout = () => { const diskManagementV2 = useFlag('diskManagementV2') - const showDiskAndComputeForm = useFlag('diskAndComputeForm') + const connectDialogUpdate = useFlag('connectDialogUpdate') const project = useSelectedProject() const showNewDiskManagementUI = diskManagementV2 && project?.cloud_provider === 'AWS' @@ -35,8 +35,14 @@ const ProjectSettings: NextPageWithLayout = () => {
- - + {connectDialogUpdate ? ( + + ) : ( + <> + + + + )}
diff --git a/apps/studio/state/app-state.ts b/apps/studio/state/app-state.ts index 23efeaa44da21..9a9f4450a3b4b 100644 --- a/apps/studio/state/app-state.ts +++ b/apps/studio/state/app-state.ts @@ -68,6 +68,7 @@ const getInitialState = () => { showGenerateSqlModal: false, navigationPanelOpen: false, navigationPanelJustClosed: false, + showConnectDialog: false, } } @@ -111,6 +112,7 @@ const getInitialState = () => { showGenerateSqlModal: false, navigationPanelOpen: false, navigationPanelJustClosed: false, + showConnectDialog: false, } } @@ -210,6 +212,11 @@ export const appState = proxy({ ...value, } }, + + showConnectDialog: false, + setShowConnectDialog: (value: boolean) => { + appState.showConnectDialog = value + }, }) // Set up localStorage subscription diff --git a/apps/studio/styles/main.scss b/apps/studio/styles/main.scss index ad2f81cd43fcc..a4ed805f793c4 100644 --- a/apps/studio/styles/main.scss +++ b/apps/studio/styles/main.scss @@ -198,11 +198,6 @@ input[type='number'] { display: none; } -code { - // use supabase-ui code style - @apply text-code; -} - div[data-radix-portal]:not(.portal--toast) { z-index: 2147483646 !important; } diff --git a/packages/ui/src/components/CodeBlock/CodeBlock.tsx b/packages/ui/src/components/CodeBlock/CodeBlock.tsx index 2013320d5bafc..ccf629e76c114 100644 --- a/packages/ui/src/components/CodeBlock/CodeBlock.tsx +++ b/packages/ui/src/components/CodeBlock/CodeBlock.tsx @@ -1,10 +1,12 @@ 'use client' +import { noop } from 'lodash' import { Check, Copy } from 'lucide-react' import { useTheme } from 'next-themes' import { Children, ReactNode, useState } from 'react' import { CopyToClipboard } from 'react-copy-to-clipboard' import { Light as SyntaxHighlighter, SyntaxHighlighterProps } from 'react-syntax-highlighter' + import { cn } from '../../lib/utils/cn' import { Button } from '../Button/Button' import { monokaiCustomTheme } from './CodeBlock.utils' @@ -13,29 +15,38 @@ import curl from 'highlightjs-curl' import bash from 'react-syntax-highlighter/dist/cjs/languages/hljs/bash' import csharp from 'react-syntax-highlighter/dist/cjs/languages/hljs/csharp' import dart from 'react-syntax-highlighter/dist/cjs/languages/hljs/dart' +import go from 'react-syntax-highlighter/dist/cjs/languages/hljs/go' import http from 'react-syntax-highlighter/dist/cjs/languages/hljs/http' import js from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript' import json from 'react-syntax-highlighter/dist/cjs/languages/hljs/json' import kotlin from 'react-syntax-highlighter/dist/cjs/languages/hljs/kotlin' -import py from 'react-syntax-highlighter/dist/cjs/languages/hljs/python' +import php from 'react-syntax-highlighter/dist/cjs/languages/hljs/php' +import { + default as py, + default as python, +} from 'react-syntax-highlighter/dist/cjs/languages/hljs/python' import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql' import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript' +export type CodeBlockLang = + | 'js' + | 'jsx' + | 'sql' + | 'py' + | 'bash' + | 'ts' + | 'dart' + | 'json' + | 'csharp' + | 'kotlin' + | 'curl' + | 'http' + | 'php' + | 'python' + | 'go' export interface CodeBlockProps { title?: ReactNode - language?: - | 'js' - | 'jsx' - | 'sql' - | 'py' - | 'bash' - | 'ts' - | 'dart' - | 'json' - | 'csharp' - | 'kotlin' - | 'curl' - | 'http' + language?: CodeBlockLang linesToHighlight?: number[] highlightBorder?: boolean styleConfig?: { @@ -52,6 +63,7 @@ export interface CodeBlockProps { children?: string renderer?: SyntaxHighlighterProps['renderer'] focusable?: boolean + onCopyCallback?: () => void wrapLines?: boolean } @@ -88,6 +100,7 @@ export const CodeBlock = ({ wrapLines = true, renderer, focusable = true, + onCopyCallback = noop, }: CodeBlockProps) => { const { resolvedTheme } = useTheme() const isDarkTheme = resolvedTheme?.includes('dark')! @@ -97,6 +110,7 @@ export const CodeBlock = ({ const handleCopy = () => { setCopied(true) + onCopyCallback() setTimeout(() => { setCopied(false) }, 1000) @@ -129,6 +143,9 @@ export const CodeBlock = ({ SyntaxHighlighter.registerLanguage('kotlin', kotlin) SyntaxHighlighter.registerLanguage('curl', curl) SyntaxHighlighter.registerLanguage('http', http) + SyntaxHighlighter.registerLanguage('php', php) + SyntaxHighlighter.registerLanguage('python', python) + SyntaxHighlighter.registerLanguage('go', go) const large = false // don't show line numbers if bash == lang @@ -157,7 +174,7 @@ export const CodeBlock = ({ style={monokaiTheme} className={cn( 'code-block border border-surface p-4 w-full !my-0 !bg-surface-100 outline-none focus:border-foreground-lighter/50', - `${!title ? '!rounded-md' : '!rounded-t-none !rounded-b-md'}`, + `${!title ? 'rounded-md' : 'rounded-t-none rounded-b-md'}`, `${!showLineNumbers ? 'pl-6' : ''}`, className )} diff --git a/packages/ui/src/components/shadcn/ui/select.tsx b/packages/ui/src/components/shadcn/ui/select.tsx index 8fd409dbcaebf..8b519980d132f 100644 --- a/packages/ui/src/components/shadcn/ui/select.tsx +++ b/packages/ui/src/components/shadcn/ui/select.tsx @@ -49,6 +49,7 @@ const SelectTrigger = React.forwardRef< className={cn( 'flex w-full items-center justify-between rounded-md border border-strong hover:border-stronger bg-alternative dark:bg-muted hover:bg-selection text-xs ring-offset-background-control data-[placeholder]:text-foreground-lighter focus:outline-none ring-border-control focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200', 'data-[state=open]:bg-selection data-[state=open]:border-stronger', + 'gap-2', SelectTriggerVariants({ size }), className )} @@ -56,7 +57,7 @@ const SelectTrigger = React.forwardRef< > {children} - + ))