diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 29b12e7ca05ab..96ad2af0b610c 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -69,6 +69,12 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ href: '/guides/local-development', level: 'local_development', }, + { + label: 'Deployment', + icon: 'deployment', + href: '/guides/deployment', + level: 'deployment', + }, { label: 'Self-Hosting', icon: 'self-hosting', @@ -82,12 +88,6 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ href: '/guides/integrations', level: 'integrations', }, - { - label: 'Deployment', - icon: 'deployment', - href: '/guides/deployment', - level: 'deployment', - }, ], ], }, @@ -828,6 +828,16 @@ export const database: NavMenuConstant = { }, ], }, + { + name: 'OrioleDB', + url: undefined, + items: [ + { + name: 'Overview', + url: '/guides/database/orioledb', + }, + ], + }, { name: 'Access and security', url: undefined, diff --git a/apps/docs/content/guides/database/orioledb.mdx b/apps/docs/content/guides/database/orioledb.mdx new file mode 100644 index 0000000000000..ba8e3279502b9 --- /dev/null +++ b/apps/docs/content/guides/database/orioledb.mdx @@ -0,0 +1,137 @@ +--- +id: 'orioledb' +title: 'OrioleDB Overview' +description: "A storage extension for PostgreSQL which uses PostgreSQL's pluggable storage system" +--- + +The [OrioleDB](https://www.orioledb.com/) Postgres extension provides a drop-in replacement storage engine for the default heap storage method. It is designed to improve Postgres' scalability and performance. + +OrioleDB addresses PostgreSQL's scalability limitations by removing bottlenecks in the shared memory cache under high concurrency. It also optimizes write-ahead-log (WAL) insertion through row-level WAL logging. These changes lead to significant improvements in the industry standard TPC-C benchmark, which approximates a real-world transactional workload. The following benchmark was performed on a c7g.metal instance and shows OrioleDB's performance outperforming the default PostgreSQL heap method with a 3.3x speedup. + +TPC-C (warehouses = 500) + + + +OrioleDB is in active development and currently has [certain limitations](https://www.orioledb.com/docs/usage/getting-started#current-limitations). Currently, only B-tree indexes are supported, so features like pg_vector's HNSW indexes are not yet available. An Index Access Method bridge to unlock support for all index types used with heap storage is under active development. In the Supabase OrioleDB image the default storage method has been updated to use OrioleDB, granting better performance out of the box. + + + +## Concepts + +### Index-organized tables + +OrioleDB uses index-organized tables, where table data is stored in the index structure. This design eliminates the need for separate heap storage, reduces overhead and improves lookup performance for primary key queries. + +### No buffer mapping + +In-memory pages are connected to the storage pages using direct links. This allows OrioleDB to bypass PostgreSQL's shared buffer pool and eliminate the associated complexity and contention in buffer mapping. + +### Undo log + +Multi-Version Concurrency Control (MVCC) is implemented using an undo log. The undo log stores previous row versions and transaction information, which enables consistent reads while removing the need for table vacuuming completely. + +### Copy-on-write checkpoints + +OrioleDB implements copy-on-write checkpoints to persist data efficiently. This approach writes only modified data during a checkpoint, reducing the I/O overhead compared to traditional PostgreSQL checkpointing and allowing row-level WAL logging. + +## Usage + +### Creating OrioleDB project + +You can get started with OrioleDB by enabling the extension in your Supabase dashboard. +To get started with OrioleDB you need to [create a new Supabase project](https://supabase.com/dashboard/new/_) and choose `OrioleDB Public Alpha` Postgres version. + +Creating OrioleDB project + +### Creating tables + +To create a table using the OrioleDB storage engine just execute the standard `CREATE TABLE` statement. By default it will create a table using OrioleDB storage engine. For example: + +```sql +-- Create a table +create table blog_post ( + id int8 not null, + title text not null, + body text not null, + author text not null, + published_at timestamptz not null default CURRENT_TIMESTAMP, + views bigint not null, + primary key (id) +); +``` + +### Creating indexes + +OrioleDB tables always have a primary key. If it wasn't defined explicitly, a hidden primary key is created using the `ctid` column. +Additionally you can create secondary indexes. + + + +Currently, only B-tree indexes are supported, so features like pg_vector's HNSW indexes are not yet available. + + + +```sql +-- Create an index +create index blog_post_published_at on blog_post (published_at); + +create index blog_post_views on blog_post (views) where (views > 1000); +``` + +### Data manipulation + +You can query and modify data in OrioleDB tables using standard SQL statements, including `SELECT`, `INSERT`, `UPDATE`, `DELETE` and `INSERT ... ON CONFLICT`. + +```sql +INSERT INTO blog_post (id, title, body, author, views) +VALUES (1, 'Hello, World!', 'This is my first blog post.', 'John Doe', 1000); + +SELECT * FROM blog_post ORDER BY published_at DESC LIMIT 10; + id │ title │ body │ author │ published_at │ views +────┼───────────────┼─────────────────────────────┼──────────┼───────────────────────────────┼─────── + 1 │ Hello, World! │ This is my first blog post. │ John Doe │ 2024-11-15 12:04:18.756824+01 │ 1000 +``` + +### Viewing query plans + +You can see the execution plan using standard `EXPLAIN` statement. + +```sql +EXPLAIN SELECT * FROM blog_post ORDER BY published_at DESC LIMIT 10; + QUERY PLAN +──────────────────────────────────────────────────────────────────────────────────────────────────────────── + Limit (cost=0.15..1.67 rows=10 width=120) + -> Index Scan Backward using blog_post_published_at on blog_post (cost=0.15..48.95 rows=320 width=120) + +EXPLAIN SELECT * FROM blog_post WHERE id = 1; + QUERY PLAN +────────────────────────────────────────────────────────────────────────────────── + Index Scan using blog_post_pkey on blog_post (cost=0.15..8.17 rows=1 width=120) + Index Cond: (id = 1) + +EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM blog_post ORDER BY published_at DESC LIMIT 10; + QUERY PLAN +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Limit (cost=0.15..1.67 rows=10 width=120) (actual time=0.052..0.054 rows=1 loops=1) + -> Index Scan Backward using blog_post_published_at on blog_post (cost=0.15..48.95 rows=320 width=120) (actual time=0.050..0.052 rows=1 loops=1) + Planning Time: 0.186 ms + Execution Time: 0.088 ms +``` + +## Resources + +- [Official OrioleDB documentation](https://www.orioledb.com/docs) +- [OrioleDB GitHub repository](https://github.com/orioledb/orioledb) diff --git a/apps/docs/public/img/database/orioledb-creating-project--light.png b/apps/docs/public/img/database/orioledb-creating-project--light.png new file mode 100644 index 0000000000000..e033f70d77f3d Binary files /dev/null and b/apps/docs/public/img/database/orioledb-creating-project--light.png differ diff --git a/apps/docs/public/img/database/orioledb-creating-project.png b/apps/docs/public/img/database/orioledb-creating-project.png new file mode 100644 index 0000000000000..93aef47733f83 Binary files /dev/null and b/apps/docs/public/img/database/orioledb-creating-project.png differ diff --git a/apps/docs/public/img/database/orioledb-tpc-c-500-warehouse.png b/apps/docs/public/img/database/orioledb-tpc-c-500-warehouse.png new file mode 100644 index 0000000000000..99a7f444c26ad Binary files /dev/null and b/apps/docs/public/img/database/orioledb-tpc-c-500-warehouse.png differ diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index e07ad7fb852e6..45cda3234db27 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 1.223.10 + version: 1.226.3 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -2907,7 +2907,7 @@ commands: Recreates the local Postgres container and applies all local migrations found in `supabase/migrations` directory. If test data is defined in `supabase/seed.sql`, it will be seeded after the migrations are run. Any other data or schema changes made during local development will be discarded. - Note that since Postgres roles are cluster level entities, those changes will persist between resets. In order to reset custom roles, you need to restart the local development stack. + When running db reset with `--linked` or `--db-url` flag, a SQL script is executed to identify and drop all user created entities in the remote database. Since Postgres roles are cluster level entities, any custom roles created through the dashboard or `supabase/roles.sql` will not be deleted by remote reset. examples: - id: basic-usage name: Basic usage diff --git a/apps/studio/components/grid/components/editor/TextEditor.tsx b/apps/studio/components/grid/components/editor/TextEditor.tsx index 32dbf6b4196b7..16353b0abae94 100644 --- a/apps/studio/components/grid/components/editor/TextEditor.tsx +++ b/apps/studio/components/grid/components/editor/TextEditor.tsx @@ -49,7 +49,8 @@ export const TextEditor = ({ }) const gridColumn = state.gridColumns.find((x) => x.name == column.key) - const initialValue = row[column.key as keyof TRow] as unknown as string + const rawValue = row[column.key as keyof TRow] as unknown + const initialValue = rawValue ? String(rawValue) : null const [isPopoverOpen, setIsPopoverOpen] = useState(true) const [value, setValue] = useState(initialValue) const [isConfirmNextModalOpen, setIsConfirmNextModalOpen] = useState(false) diff --git a/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx b/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx index 53b5e0b957bf0..3acf25000593c 100644 --- a/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx @@ -19,6 +19,11 @@ import { Modal, WarningIcon, } from 'ui' +import { Admonition } from 'ui-patterns' +import { DocsButton } from 'components/ui/DocsButton' +import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' + +const orioleExtCallOuts = ['vector', 'postgis'] interface EnableExtensionModalProps { visible: boolean @@ -28,6 +33,7 @@ interface EnableExtensionModalProps { const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionModalProps) => { const { project } = useProjectContext() + const isOrioleDb = useIsOrioleDb() const [defaultSchema, setDefaultSchema] = useState() const [fetchingSchemaInfo, setFetchingSchemaInfo] = useState(false) @@ -129,7 +135,17 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM {({ values }: any) => { return ( <> - + + {isOrioleDb && orioleExtCallOuts.includes(extension.name) && ( + + + {extension.name} cannot be accelerated by indexes on tables that are using the + OrioleDB access method + + + + )} + {fetchingSchemaInfo || isSchemasLoading ? (
diff --git a/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx b/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx index f86edb50bc418..44386c6351f34 100644 --- a/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx @@ -8,8 +8,16 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import { useDatabaseExtensionDisableMutation } from 'data/database-extensions/database-extension-disable-mutation' import { DatabaseExtension } from 'data/database-extensions/database-extensions-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' import { extensions } from 'shared-data' -import { Button, cn, Switch } from 'ui' +import { + Button, + cn, + Switch, + Tooltip_Shadcn_, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, +} from 'ui' import { Admonition } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import EnableExtensionModal from './EnableExtensionModal' @@ -23,6 +31,7 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => { const { project } = useProjectContext() const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || '' const isOn = extension.installed_version !== null + const isOrioleDb = useIsOrioleDb() const [isDisableModalOpen, setIsDisableModalOpen] = useState(false) const [showConfirmEnableModal, setShowConfirmEnableModal] = useState(false) @@ -31,6 +40,8 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => { PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions' ) + const orioleDbCheck = isOrioleDb && extension.name === 'orioledb' + const disabled = !canUpdateExtensions || orioleDbCheck const X_PADDING = 'px-5' const extensionMeta = extensions.find((item: any) => item.name === extension.name) @@ -76,13 +87,26 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => { {isDisabling ? ( ) : ( - - isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true) - } - /> + + + + isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true) + } + /> + + {disabled && ( + + {!canUpdateExtensions + ? 'You need additional permissions to toggle extensions' + : orioleDbCheck + ? 'Project is using OrioleDB and cannot be disabled' + : null} + + )} + )}
diff --git a/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx b/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx index 04ee9e2916237..4b922043dba85 100644 --- a/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx +++ b/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx @@ -37,6 +37,9 @@ import { MultiSelectOption } from 'ui-patterns/MultiSelectDeprecated' import { MultiSelectV2 } from 'ui-patterns/MultiSelectDeprecated/MultiSelectV2' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { INDEX_TYPES } from './Indexes.constants' +import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' +import { Admonition } from 'ui-patterns' +import { DocsButton } from 'components/ui/DocsButton' interface CreateIndexSidePanelProps { visible: boolean @@ -46,6 +49,8 @@ interface CreateIndexSidePanelProps { const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) => { const queryClient = useQueryClient() const { project } = useProjectContext() + const isOrioleDb = useIsOrioleDb() + const [selectedSchema, setSelectedSchema] = useState('public') const [selectedEntity, setSelectedEntity] = useState(undefined) const [selectedColumns, setSelectedColumns] = useState([]) @@ -336,6 +341,7 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp isReactForm={false} > + {isOrioleDb && ( + + {/* [Joshen Oriole] Hook up proper docs URL */} + + + )} diff --git a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx index 1c001ad2d9da4..24c0638a1ebb5 100644 --- a/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx +++ b/apps/studio/components/interfaces/DiskManagement/fields/ComputeSizeField.tsx @@ -135,7 +135,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) { free={showUpgradeBadge && form.watch('computeSize') === 'ci_micro' ? true : false} />

- Hardware resources allocated to your postgres database + Hardware resources allocated to your Postgres database

{ export const IntegrationCard = ({ id, - beta, + status, name, icon, description, @@ -51,9 +51,9 @@ export const IntegrationCard = ({

{name}

- {beta && ( - - Beta + {status && ( + + {status} )}
diff --git a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx index f016a84e281ab..291a10951b7ae 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx @@ -26,7 +26,7 @@ const Loading = () => ( export type IntegrationDefinition = { id: string name: string - beta?: boolean + status?: 'alpha' | 'beta' icon: (props?: { className?: string; style?: Record }) => ReactNode description: string docsUrl: string @@ -170,7 +170,7 @@ const supabaseIntegrations: IntegrationDefinition[] = [ type: 'postgres_extension' as const, requiredExtensions: ['supabase_vault'], name: `Vault`, - beta: true, + status: 'alpha', icon: ({ className, ...props } = {}) => ( ), diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx index 5181053db5933..f0eaf0d2e76a0 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx @@ -202,12 +202,12 @@ export const QueuesSettings = () => { queues via any Supabase client library or PostgREST endpoints:

- queue_send,{' '} - queue_send_batch,{' '} - queue_read,{' '} - queue_pop, - queue_archive, and - queue_delete + send,{' '} + send_batch,{' '} + read,{' '} + pop, + archive, and + delete

} diff --git a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/Queue.utils.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/Queue.utils.tsx index 3f72dc4ede477..2d236a9e7f050 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/Queue.utils.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/Queue.utils.tsx @@ -7,3 +7,18 @@ export const QUEUE_MESSAGE_OPTIONS = QUEUE_MESSAGE_TYPES.map((type) => ({ id: type, name: capitalize(type), })) + +export const getQueueFunctionsMapping = (command: string) => { + switch (command) { + case 'select': + return ['send', 'send_batch', 'read', 'pop', 'archive', 'delete'] + case 'insert': + return ['send', 'send_batch'] + case 'update': + return ['read', 'pop'] + case 'delete': + return ['archive', 'delete'] + default: + return [] + } +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx index fd475eb5acb85..b4bf5834669cc 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx @@ -1,5 +1,5 @@ import { isEqual } from 'lodash' -import { Settings } from 'lucide-react' +import { HelpCircle, Settings } from 'lucide-react' import { useEffect, useState } from 'react' import { toast } from 'sonner' @@ -20,6 +20,9 @@ import { useTablesQuery } from 'data/tables/tables-query' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { Button, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, Sheet, SheetContent, SheetDescription, @@ -35,10 +38,18 @@ import { TableHead, TableHeader, TableRow, + Tooltip_Shadcn_, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { useQueuesExposePostgrestStatusQuery } from 'data/database-queues/database-queues-expose-postgrest-status-query' +import { getQueueFunctionsMapping } from './Queue.utils' +import { Admonition } from 'ui-patterns' +import Link from 'next/link' const ACTIONS = ['select', 'insert', 'update', 'delete'] +const ROLES = ['anon', 'authenticated', 'postgres', 'service_role'] type Privileges = { select?: boolean; insert?: boolean; update?: boolean; delete?: boolean } interface QueueSettingsProps {} @@ -51,11 +62,18 @@ export const QueueSettings = ({}: QueueSettingsProps) => { const [isSaving, setIsSaving] = useState(false) const [privileges, setPrivileges] = useState<{ [key: string]: Privileges }>({}) + const { data: isExposed } = useQueuesExposePostgrestStatusQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const { data, error, isLoading, isSuccess, isError } = useDatabaseRolesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) - const roles = (data ?? []).sort((a, b) => a.name.localeCompare(b.name)) + const roles = (data ?? []) + .filter((x) => ROLES.includes(x.name)) + .sort((a, b) => a.name.localeCompare(b.name)) const { data: queueTables } = useTablesQuery({ projectRef: project?.ref, @@ -223,20 +241,79 @@ export const QueueSettings = ({}: QueueSettingsProps) => { Manage queue permissions on {name} - Configure permissions for each role to grant access to the relevant actions on the queue + Configure permissions for the following roles to grant access to the relevant actions on + the queue.{' '} + {isExposed && ( + <> + These will also determine access to each function available from the{' '} + pgmq_public schema. + + )} + {!isExposed ? ( + + You may opt to manage your queues via any Supabase client libraries or PostgREST + endpoints by enabling this in the{' '} + + queues settings + + + } + /> + ) : ( + + )} Role - {ACTIONS.map((x) => ( - - {x} - - ))} + {ACTIONS.map((x) => { + const relatedFunctions = getQueueFunctionsMapping(x) + return ( + + + + {x} + {isExposed && } + + {isExposed && ( + +

+ Required for{' '} + {relatedFunctions.length === 6 + ? 'all' + : `the following ${relatedFunctions.length}`}{' '} + functions: +

+
+ {relatedFunctions.map((y) => ( + {y} + ))} +
+
+ )} +
+
+ ) + })}
@@ -272,7 +349,7 @@ export const QueueSettings = ({}: QueueSettingsProps) => { {role.name} {ACTIONS.map((x) => ( - + { connectionString: project?.connectionString, }) + const wrappers = useMemo( + () => + integration && integration.type === 'wrapper' && data + ? data.filter((wrapper) => wrapperMetaComparator(integration.meta, wrapper)) + : [], + [data, integration] + ) + if (!integration || integration.type !== 'wrapper') { return (

@@ -36,8 +47,6 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => { ) } - const wrappers = (data ?? []).filter((x) => x.handler === integration.meta.handlerName) || [] - return ( diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/Wrappers.constants.ts b/apps/studio/components/interfaces/Integrations/Wrappers/Wrappers.constants.ts index 2a3674c7f9305..150e733287544 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/Wrappers.constants.ts +++ b/apps/studio/components/interfaces/Integrations/Wrappers/Wrappers.constants.ts @@ -16,6 +16,7 @@ export const WRAPPER_HANDLERS = { PADDLE: 'wasm_fdw_handler', SNOWFLAKE: 'wasm_fdw_handler', CAL: 'wasm_fdw_handler', + CALENDLY: 'wasm_fdw_handler', } export const WRAPPERS: WrapperMeta[] = [ @@ -2292,4 +2293,241 @@ export const WRAPPERS: WrapperMeta[] = [ }, ], }, + { + name: 'calendly_wrapper', + description: 'Calendly is a scheduling platform', + handlerName: WRAPPER_HANDLERS.CALENDLY, + validatorName: 'wasm_fdw_validator', + icon: `${BASE_PATH}/img/icons/calendly-icon.svg`, + extensionName: 'calendlyFdw', + label: 'Calendly', + docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/calendly', + minimumExtensionVersion: '0.4.0', + server: { + options: [ + { + name: 'fdw_package_url', + label: 'FDW Package URL', + required: true, + encrypted: false, + secureEntry: false, + defaultValue: + 'https://github.com/supabase/wrappers/releases/download/wasm_calendly_fdw_v0.1.0/calendly_fdw.wasm', + hidden: true, + }, + { + name: 'fdw_package_name', + label: 'FDW Package Name', + required: true, + encrypted: false, + secureEntry: false, + defaultValue: 'supabase:calendly-fdw', + hidden: true, + }, + { + name: 'fdw_package_version', + label: 'FDW Package Version', + required: true, + encrypted: false, + secureEntry: false, + defaultValue: '0.1.0', + hidden: true, + }, + { + name: 'fdw_package_checksum', + label: 'FDW Package Checksum', + required: true, + encrypted: false, + secureEntry: false, + defaultValue: 'aa17f1ce2b48b5d8d6cee4f61df4d6b23e9a333c3e5c7a10cec9aae619c156b9', + hidden: true, + }, + { + name: 'organization', + label: 'Organization URL', + required: true, + encrypted: false, + secureEntry: false, + defaultValue: + 'https://api.calendly.com/organizations/00000000-0000-0000-0000-000000000000', + }, + { + name: 'api_url', + label: 'API URL', + required: false, + encrypted: false, + secureEntry: false, + defaultValue: 'https://api.calendly.com', + }, + { + name: 'api_key_id', + label: 'API Key ID', + required: true, + encrypted: true, + secureEntry: true, + }, + ], + }, + tables: [ + { + label: 'Current User', + description: 'Get the current user used for the API request', + availableColumns: [ + { + name: 'uri', + type: 'text', + }, + { + name: 'slug', + type: 'text', + }, + { + name: 'created_at', + type: 'timestamp', + }, + { + name: 'updated_at', + type: 'timestamp', + }, + { + name: 'attrs', + type: 'jsonb', + }, + ], + options: [ + { + name: 'object', + defaultValue: 'current_user', + editable: false, + required: true, + type: 'text', + }, + ], + }, + { + label: 'Event Types', + description: 'Shows your Event Types', + availableColumns: [ + { + name: 'uri', + type: 'text', + }, + { + name: 'created_at', + type: 'timestamp', + }, + { + name: 'updated_at', + type: 'timestamp', + }, + { + name: 'attrs', + type: 'jsonb', + }, + ], + options: [ + { + name: 'object', + defaultValue: 'event_types', + editable: false, + required: true, + type: 'text', + }, + ], + }, + { + label: 'Groups', + description: 'Shows your groups', + availableColumns: [ + { + name: 'uri', + type: 'text', + }, + { + name: 'created_at', + type: 'timestamp', + }, + { + name: 'updated_at', + type: 'timestamp', + }, + { + name: 'attrs', + type: 'jsonb', + }, + ], + options: [ + { + name: 'object', + defaultValue: 'groups', + editable: false, + required: true, + type: 'text', + }, + ], + }, + { + label: 'Organization Memberships', + description: 'Shows your Organization Memberships', + availableColumns: [ + { + name: 'uri', + type: 'text', + }, + { + name: 'created_at', + type: 'timestamp', + }, + { + name: 'updated_at', + type: 'timestamp', + }, + { + name: 'attrs', + type: 'jsonb', + }, + ], + options: [ + { + name: 'object', + defaultValue: 'organization_memberships', + editable: false, + required: true, + type: 'text', + }, + ], + }, + { + label: 'Scheduled Events', + description: 'Shows your scheduled events', + availableColumns: [ + { + name: 'uri', + type: 'text', + }, + { + name: 'created_at', + type: 'timestamp', + }, + { + name: 'updated_at', + type: 'timestamp', + }, + { + name: 'attrs', + type: 'jsonb', + }, + ], + options: [ + { + name: 'object', + defaultValue: 'scheduled_events', + editable: false, + required: true, + type: 'text', + }, + ], + }, + ], + }, ] diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index 76f834a19bef3..edb8ef3160355 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -16,6 +16,7 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' import { organizationKeys } from 'data/organizations/keys' import { useOrganizationBillingSubscriptionPreview } from 'data/organizations/organization-billing-subscription-preview' +import { useOrganizationQuery } from 'data/organizations/organization-query' import { useProjectsQuery } from 'data/projects/projects-query' import { useOrgPlansQuery } from 'data/subscriptions/org-plans-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' @@ -23,6 +24,7 @@ import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscri import type { OrgPlan, SubscriptionTier } from 'data/subscriptions/types' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { useFlag } from 'hooks/ui/useFlag' import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants' import { formatCurrency } from 'lib/helpers' import { pickFeatures, pickFooter, plans as subscriptionsPlans } from 'shared-data/plans' @@ -41,8 +43,8 @@ const PlanUpdateSidePanel = () => { const slug = selectedOrganization?.slug const queryClient = useQueryClient() - const originalPlanRef = useRef() + const allowOrioleDB = useFlag('allowOrioleDb') const [showExitSurvey, setShowExitSurvey] = useState(false) const [showUpgradeSurvey, setShowUpgradeSurvey] = useState(false) @@ -60,6 +62,9 @@ const PlanUpdateSidePanel = () => { (it) => it.organization_id === selectedOrganization?.id ) + const { data } = useOrganizationQuery({ slug }, { enabled: allowOrioleDB }) + const hasOrioleProjects = allowOrioleDB ? false : !!data?.has_oriole_project + const snap = useOrgSettingsPageStateSnapshot() const visible = snap.panelKey === 'subscriptionPlan' const onClose = () => { @@ -181,10 +186,10 @@ const PlanUpdateSidePanel = () => { header={

Change subscription plan for {selectedOrganization?.name}

-
} @@ -213,13 +218,14 @@ const PlanUpdateSidePanel = () => { return (
-

{plan.name}

+

{plan.name}

{isCurrentPlan ? (
Current plan @@ -228,9 +234,7 @@ const PlanUpdateSidePanel = () => {
{plan.nameBadge}
- ) : ( - <> - )} + ) : null}
{(price ?? 0) > 0 &&

From

} @@ -251,17 +255,24 @@ const PlanUpdateSidePanel = () => { setSelectedTier(plan.id as any)} tooltip={{ content: { side: 'bottom', + className: hasOrioleProjects ? 'w-96 text-center' : '', text: subscription?.plan?.id === 'enterprise' ? 'Reach out to us via support to update your plan from Enterprise' - : !canUpdateSubscription - ? 'You do not have permission to change the subscription plan' - : undefined, + : hasOrioleProjects + ? 'Your organization has projects that are using the OrioleDB extension which is only available on the Free plan. Remove all OrioleDB projects before changing your plan.' + : !canUpdateSubscription + ? 'You do not have permission to change the subscription plan' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx b/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx new file mode 100644 index 0000000000000..25dca6d4d0797 --- /dev/null +++ b/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx @@ -0,0 +1,126 @@ +import { ChevronRight } from 'lucide-react' +import { UseFormReturn } from 'react-hook-form' + +import Panel from 'components/ui/Panel' +import { CreateProjectForm } from 'pages/new/[slug]' +import { + Badge, + cn, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + FormItem_Shadcn_, + RadioGroupStacked, + RadioGroupStackedItem, +} from 'ui' +import { Admonition } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { DocsButton } from 'components/ui/DocsButton' + +interface AdvancedConfigurationProps { + form: UseFormReturn +} + +export const AdvancedConfiguration = ({ form }: AdvancedConfigurationProps) => { + return ( + + + + Advanced Configuration + + + + ( + <> + + + field.onChange(value === 'true')} + defaultValue={field.value.toString()} + > + + + + Postgres + + Default + + + } + description="Recommended for production workloads" + className="[&>div>div>p]:text-left [&>div>div>p]:text-xs" + /> + + + + + + Postgres with OrioleDB + + Alpha + + + } + description="Not recommended for production workloads" + className={cn( + '[&>div>div>p]:text-left [&>div>div>p]:text-xs', + form.getValues('useOrioleDb') ? '!rounded-b-none' : '' + )} + /> + + + + + {form.getValues('useOrioleDb') && ( + + + + )} + + + )} + /> + +

+ These settings cannot be changed after the project is created +

+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx index 0056c482d0198..c0b550643b338 100644 --- a/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx @@ -54,42 +54,60 @@ export const PostgresVersionSelector = ({ field, form, }: PostgresVersionSelectorProps) => { - const { data, isLoading: isLoadingProjectVersions } = useProjectCreationPostgresVersionsQuery({ + const { + data, + isLoading: isLoadingProjectVersions, + isSuccess, + } = useProjectCreationPostgresVersionsQuery({ cloudProvider, dbRegion, organizationSlug, }) + const availableVersions = (data?.available_versions ?? []).sort((a, b) => + a.version.localeCompare(b.version) + ) useEffect(() => { - const defaultValue = data?.available_versions?.[0] - ? formatValue(data.available_versions[0]) - : undefined + const defaultValue = availableVersions[0] ? formatValue(availableVersions[0]) : undefined form.setValue('postgresVersionSelection', defaultValue) - }, [data, form]) + }, [isSuccess, form]) return ( - + - {(data?.available_versions || [])?.map((value) => { - const postgresVersion = value.version.split('supabase-postgres-')[1] + {availableVersions.map((value) => { + const postgresVersion = value.version + .split('supabase-postgres-')[1] + .replace('-orioledb', '') return ( - -
+ +
{postgresVersion} - {value.release_channel !== 'ga' && ( - - {value.release_channel} - - )} +
+ {value.release_channel !== 'ga' && ( + + {value.release_channel} + + )} + {value.postgres_engine.includes('oriole') && ( + + OrioleDB + + )} +
) diff --git a/apps/studio/components/interfaces/ProjectCreation/SecurityOptions.tsx b/apps/studio/components/interfaces/ProjectCreation/SecurityOptions.tsx new file mode 100644 index 0000000000000..2e31f73e0fa27 --- /dev/null +++ b/apps/studio/components/interfaces/ProjectCreation/SecurityOptions.tsx @@ -0,0 +1,169 @@ +import { ChevronRight } from 'lucide-react' +import { UseFormReturn } from 'react-hook-form' + +import Panel from 'components/ui/Panel' +import { CreateProjectForm } from 'pages/new/[slug]' +import { + Badge, + cn, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + FormItem_Shadcn_, + RadioGroupStacked, + RadioGroupStackedItem, +} from 'ui' +import { Admonition } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +interface SecurityOptionsProps { + form: UseFormReturn +} + +export const SecurityOptions = ({ form }: SecurityOptionsProps) => { + return ( + + + + Security options + + + + ( + <> + + + field.onChange(value === 'true')} + defaultValue={field.value.toString()} + > + + + + + + + + div>div>p]:text-left [&>div>div>p]:text-xs' + )} + onClick={() => form.setValue('useApiSchema', false)} + /> + + + + + {!form.getValues('dataApi') && ( + + PostgREST which powers the Data API will have no schemas available to it. + + )} + + + )} + /> + + {form.getValues('dataApi') && ( + ( + <> + + + field.onChange(value === 'true')} + > + + + + Use public schema for Data API + + Default + + + } + // @ts-ignore + description={ + <> + Query all tables in the public{' '} + schema + + } + className="[&>div>div>p]:text-left [&>div>div>p]:text-xs" + /> + + + + + + Query allowlisted tables in a dedicated{' '} + api schema + + } + className="[&>div>div>p]:text-left [&>div>div>p]:text-xs" + /> + + + + + + + )} + /> + )} +

+ These settings can be changed after the project is created via the project's settings +

+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Realtime/Inspector/NoChannelEmptyState.tsx b/apps/studio/components/interfaces/Realtime/Inspector/NoChannelEmptyState.tsx index eff1b6944e8f7..342aca9b110e9 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/NoChannelEmptyState.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/NoChannelEmptyState.tsx @@ -1,6 +1,5 @@ import { DocsButton } from 'components/ui/DocsButton' -import { ExternalLink } from 'lucide-react' -import { Button } from 'ui' +import { cn } from 'ui' const NoChannelEmptyState = () => { return ( @@ -14,7 +13,7 @@ const NoChannelEmptyState = () => {

-
+

Not sure what to do?

Browse our documentation

diff --git a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx index 86f0096033303..23af80059d7ec 100644 --- a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx @@ -31,7 +31,7 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import type { ProjectAddonVariantMeta } from 'data/subscriptions/types' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useProjectByRef } from 'hooks/misc/useSelectedProject' +import { useIsOrioleDb, useProjectByRef } from 'hooks/misc/useSelectedProject' import { useFlag } from 'hooks/ui/useFlag' import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils' import { BASE_PATH, INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS } from 'lib/constants' @@ -44,6 +44,7 @@ import CustomDomainSidePanel from './CustomDomainSidePanel' import IPv4SidePanel from './IPv4SidePanel' import PITRSidePanel from './PITRSidePanel' import { NoticeBar } from 'components/interfaces/DiskManagement/ui/NoticeBar' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' const Addons = () => { const { resolvedTheme } = useTheme() @@ -54,6 +55,7 @@ const Addons = () => { const parentProject = useProjectByRef(selectedProject?.parent_project_ref) const isBranch = parentProject !== undefined const isProjectActive = useIsProjectActive() + const isOrioleDb = useIsOrioleDb() const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrg?.slug }) @@ -568,6 +570,20 @@ const Addons = () => { + ) : isOrioleDb ? ( + + Change point in time recovery + ) : ( { const reactFlow = useReactFlow() + const isOrioleDb = useIsOrioleDb() const { resolvedTheme } = useTheme() const { ref: projectRef } = useParams() const numTransition = useRef() @@ -225,7 +227,7 @@ const InstanceConfigurationUI = () => {
0 ? 'rounded-r-none' : '')} onClick={() => setShowNewReplicaPanel(true)} tooltip={{ @@ -233,7 +235,9 @@ const InstanceConfigurationUI = () => { side: 'bottom', text: !canManageReplicas ? 'You need additional permissions to deploy replicas' - : undefined, + : isOrioleDb + ? 'Read replicas are not supported with OrioleDB' + : undefined, }, }} > diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx index efaa3666ad5bb..a2c8d2026e6b0 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx @@ -55,11 +55,14 @@ const InfrastructureInfo = () => { const { current_app_version, current_app_version_release_channel, latest_app_version } = data || {} const isOnLatestVersion = current_app_version === latest_app_version - const currentPgVersion = (current_app_version ?? '').split('supabase-postgres-')[1] + const currentPgVersion = (current_app_version ?? '') + .split('supabase-postgres-')[1] + ?.replace('-orioledb', '') const isOnNonGenerallyAvailableReleaseChannel = current_app_version_release_channel && current_app_version_release_channel !== 'ga' ? current_app_version_release_channel : undefined + const isOrioleDb = (current_app_version ?? '').includes('orioledb') const latestPgVersion = (latest_app_version ?? '').split('supabase-postgres-')[1] const isInactive = project?.status === 'INACTIVE' @@ -143,6 +146,23 @@ const InfrastructureInfo = () => { ), + isOrioleDb && ( + <> + + + + OrioleDB + + + + This project uses OrioleDB + + + + ), isOnLatestVersion && ( diff --git a/apps/studio/components/layouts/AppLayout/AssistantButton.tsx b/apps/studio/components/layouts/AppLayout/AssistantButton.tsx index c5711c9c02a75..2854c85248c17 100644 --- a/apps/studio/components/layouts/AppLayout/AssistantButton.tsx +++ b/apps/studio/components/layouts/AppLayout/AssistantButton.tsx @@ -9,6 +9,7 @@ const AssistantButton = () => { type="text" size="tiny" id="assistant-trigger" + className="h-full w-full rounded-none" onClick={() => { setAiAssistantPanel({ open: !aiAssistantPanel.open }) }} diff --git a/apps/studio/components/layouts/Integrations/header.tsx b/apps/studio/components/layouts/Integrations/header.tsx index ebb5ff7d5c8ff..b81f358e97507 100644 --- a/apps/studio/components/layouts/Integrations/header.tsx +++ b/apps/studio/components/layouts/Integrations/header.tsx @@ -157,9 +157,9 @@ export const Header = forwardRef(({ scroll }, ref)
{integration.name} - {integration.beta && ( - - Beta + {integration.status && ( + + {integration.status} )}
diff --git a/apps/studio/components/layouts/Integrations/layout.tsx b/apps/studio/components/layouts/Integrations/layout.tsx index 4c94e4c30744a..35d7334504471 100644 --- a/apps/studio/components/layouts/Integrations/layout.tsx +++ b/apps/studio/components/layouts/Integrations/layout.tsx @@ -91,7 +91,7 @@ const IntegrationTopHeaderLayout = ({ ...props }: PropsWithChildren) => { } = useInstalledIntegrations() const installedIntegrationItems = integrations.map((integration) => ({ name: integration.name, - label: integration.beta ? 'Beta' : undefined, + label: integration.status, key: `integrations/${integration.id}`, url: `/project/${project?.ref}/integrations/${integration.id}/overview`, icon: ( @@ -167,7 +167,7 @@ const IntegrationsLayoutSide = ({ ...props }: PropsWithChildren) => { } = useInstalledIntegrations() const installedIntegrationItems = integrations.map((integration) => ({ name: integration.name, - label: integration.beta ? 'Beta' : undefined, + label: integration.status, key: `integrations/${integration.id}`, url: `/project/${project?.ref}/integrations/${integration.id}/overview`, icon: ( diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index 13a481d1a3521..6285bcc57619f 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -47,83 +47,91 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = return (
-
- {/* Organization is selected */} - {projectRef && ( - <> - +
+
+ {/* Organization is selected */} + {projectRef && ( + <> + - {projectRef && ( - <> - - - - - + {projectRef && ( + <> + + + + + - + - {exceedingLimits && ( -
- - Exceeding usage limits - -
- )} - - )} + {exceedingLimits && ( +
+ + Exceeding usage limits + +
+ )} + + )} - {selectedProject && ( + {selectedProject && ( + <> + + + + + + {isBranchingEnabled ? : } + + )} + + )} + + {/* Additional breadcrumbs are supplied */} + +
+
+
+ {customHeaderComponents && customHeaderComponents} + {IS_PLATFORM && ( <> - - - - - - {isBranchingEnabled ? : } + + + )} - - )} - - {/* Additional breadcrumbs are supplied */} - -
-
- {customHeaderComponents && customHeaderComponents} - {IS_PLATFORM && ( - <> - - - - {isAssistantV2Enabled && !!projectRef && } - - )} +
+
+ {isAssistantV2Enabled && !!projectRef && ( +
+ +
+ )}
) } diff --git a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx index a64d8ccff441f..929345207e736 100644 --- a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx @@ -107,6 +107,7 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { const { data: availablePostgresVersions } = useProjectUnpausePostgresVersionsQuery({ projectRef: project?.ref, }) + const availableVersions = availablePostgresVersions?.available_versions || [] const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 const [showConfirmRestore, setShowConfirmRestore] = useState(false) @@ -322,33 +323,49 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { render={({ field }) => ( - - + + - {(availablePostgresVersions?.available_versions || [])?.map( - (value) => { - const postgresVersion = - value.version.split('supabase-postgres-')[1] - return ( - -
- {postgresVersion} + {availableVersions.map((value) => { + const postgresVersion = value.version + .split('supabase-postgres-')[1] + ?.replace('-orioledb', '') + return ( + +
+ {postgresVersion} +
{value.release_channel !== 'ga' && ( {value.release_channel} )} + {value.postgres_engine.includes('oriole-preview') && ( + + + OrioleDB + + + Preview + + + )}
- - ) - } - )} +
+
+ ) + })} diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index 601fd07558a88..81c7849f019a6 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -114,7 +114,7 @@ const ProjectLayout = forwardRef { const handler = (e: KeyboardEvent) => { - if (e.metaKey && e.code === 'KeyI') { + if (e.metaKey && e.code === 'KeyI' && !e.altKey && !e.shiftKey) { setAiAssistantPanel({ open: !open }) e.preventDefault() e.stopPropagation() diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.test.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.test.ts new file mode 100644 index 0000000000000..abe0881783d99 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.test.ts @@ -0,0 +1,62 @@ +import { isReadOnlySelect } from './AIAssistant.utils' + +describe('AIAssistant.utils.ts:isReadOnlySelect', () => { + test('Should return true for SQL that only contains SELECT operation', () => { + const sql = 'select * from countries where id > 100 order by id asc;' + const result = isReadOnlySelect(sql) + expect(result).toBe(true) + }) + test('Should return false for SQL that contains INSERT operation', () => { + const sql = `insert into countries (id, name) values (1, 'hello');` + const result = isReadOnlySelect(sql) + expect(result).toBe(false) + }) + test('Should return false for SQL that contains UPDATE operation', () => { + const sql = `update countries set name = 'hello' where id = 2;` + const result = isReadOnlySelect(sql) + expect(result).toBe(false) + }) + test('Should return false for SQL that contains DELETE operation', () => { + const sql = `delete from countries where id = 2;` + const result = isReadOnlySelect(sql) + expect(result).toBe(false) + }) + test('Should return false for SQL that contains ALTER operation', () => { + const sql = `alter table countries drop column id if exists;` + const result = isReadOnlySelect(sql) + expect(result).toBe(false) + }) + test('Should return false for SQL that contains DROP operation', () => { + const sql = `drop table if exists countries;` + const result = isReadOnlySelect(sql) + expect(result).toBe(false) + }) + test('Should return false for SQL that contains CREATE operation', () => { + const sql = `create schema test_schema;` + const result = isReadOnlySelect(sql) + expect(result).toBe(false) + }) + test('Should return false for SQL that contains REPLACE operation', () => { + const sql = `create or replace view test_view as select * from countries where id > 500;` + const result = isReadOnlySelect(sql) + expect(result).toBe(false) + }) + test('Should return false for SQL that calls a function not whitelisted', () => { + const sql = `select create_new_user();` + const result = isReadOnlySelect(sql) + expect(result).toBe(false) + }) + test('Should return true for SQL that calls a function that is whitelisted', () => { + const sql = `select count(select * from countries);` + const result = isReadOnlySelect(sql) + expect(result).toBe(true) + }) + test('Should return false for SQL that contains a write operation with a read operation', () => { + const sql1 = `select count(select * from countries); create schema joshen;` + const result1 = isReadOnlySelect(sql1) + expect(result1).toBe(false) + const sql2 = `create schema joshen; select count(select * from countries);` + const result2 = isReadOnlySelect(sql2) + expect(result2).toBe(false) + }) +}) diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts index 5bbc060d26e9e..d69c482fa7a23 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts @@ -7,6 +7,7 @@ import { enumeratedTypesKeys } from 'data/enumerated-types/keys' import { tableKeys } from 'data/tables/keys' import { CommonDatabaseEntity } from 'state/app-state' import { SupportedAssistantEntities } from './AIAssistant.types' +import { SAFE_FUNCTIONS } from './AiAssistant.constants' const PLACEHOLDER_PREFIX = `-- Press tab to use this code \n \n` @@ -109,47 +110,44 @@ export const identifyQueryType = (query: string) => { } } +// Check for function calls that aren't in the safe list +export const containsUnknownFunction = (query: string) => { + const normalizedQuery = query.trim().toLowerCase() + const functionCallRegex = /\w+\s*\(/g + const functionCalls = normalizedQuery.match(functionCallRegex) || [] + + return functionCalls.some((func) => { + const isReadOnlyFunc = SAFE_FUNCTIONS.some((safeFunc) => func.trim().toLowerCase() === safeFunc) + return !isReadOnlyFunc + }) +} + export const isReadOnlySelect = (query: string): boolean => { const normalizedQuery = query.trim().toLowerCase() // Check if it starts with SELECT - if (!normalizedQuery.startsWith('select')) { - return false - } - - // List of keywords that indicate write operations or function calls - const disallowedPatterns = [ - // Write operations - 'insert', - 'update', - 'delete', - 'alter', - 'drop', - 'create', - 'truncate', - 'replace', - 'with', + if (!normalizedQuery.startsWith('select')) return false - // Function patterns - 'function', - 'procedure', - ] + // List of keywords that indicate write operations + const writeOperations = ['insert', 'update', 'delete', 'alter', 'drop', 'create', 'replace'] - const allowedPatterns = ['created', 'inserted', 'updated', 'deleted'] + // Words that may appear in column names etc + const allowedPatterns = ['created', 'inserted', 'updated', 'deleted', 'truncate'] - // Check if query contains any disallowed patterns, but allow if part of allowedPatterns - return !disallowedPatterns.some((pattern) => { - // Check if the found disallowed pattern is actually part of an allowed pattern - const isPartOfAllowedPattern = allowedPatterns.some( - (allowed) => normalizedQuery.includes(allowed) && allowed.includes(pattern) + // Check for any write operations + const hasWriteOperation = writeOperations.some((op) => { + // Ignore if part of allowed pattern + const isAllowed = allowedPatterns.some( + (allowed) => normalizedQuery.includes(allowed) && allowed.includes(op) ) + return !isAllowed && normalizedQuery.includes(op) + }) + if (hasWriteOperation) return false - if (isPartOfAllowedPattern) { - return false - } + const hasUnknownFunction = containsUnknownFunction(normalizedQuery) + if (hasUnknownFunction) return false - return normalizedQuery.includes(pattern) - }) + return true } const getContextKey = (pathname: string) => { diff --git a/apps/studio/components/ui/AIAssistantPanel/AiAssistant.constants.ts b/apps/studio/components/ui/AIAssistantPanel/AiAssistant.constants.ts index e9bc5bdeb55b0..7762e28bd2704 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AiAssistant.constants.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AiAssistant.constants.ts @@ -8,3 +8,25 @@ export const ASSISTANT_SUPPORT_ENTITIES: { { id: 'rls-policies', label: 'RLS Policies', name: 'RLS policy' }, { id: 'functions', label: 'Functions', name: 'database function' }, ] + +export const SAFE_FUNCTIONS = [ + 'count(', + 'sum(', + 'avg(', + 'min(', + 'max(', + 'coalesce(', + 'nullif(', + 'current_timestamp', + 'current_date', + 'length(', + 'lower(', + 'upper(', + 'trim(', + 'substring(', + 'to_char(', + 'to_date(', + 'extract(', + 'date_trunc(', + 'string_agg(', +] diff --git a/apps/studio/components/ui/AIAssistantPanel/Message.tsx b/apps/studio/components/ui/AIAssistantPanel/Message.tsx index a4e3852e65d40..bcd29442f0443 100644 --- a/apps/studio/components/ui/AIAssistantPanel/Message.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/Message.tsx @@ -35,10 +35,11 @@ export const Message = function Message({ return ( diff --git a/apps/studio/components/ui/AIAssistantPanel/SqlSnippet.tsx b/apps/studio/components/ui/AIAssistantPanel/SqlSnippet.tsx index 9e162427f3bd8..ff9f68ca87306 100644 --- a/apps/studio/components/ui/AIAssistantPanel/SqlSnippet.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/SqlSnippet.tsx @@ -27,6 +27,7 @@ import { import { Admonition } from 'ui-patterns' import { ButtonTooltip } from '../ButtonTooltip' import { + containsUnknownFunction, getContextualInvalidationKeys, identifyQueryType, isReadOnlySelect, @@ -61,6 +62,7 @@ const SqlSnippetWrapper = ({ isChart={props.isChart === 'true'} xAxis={props.xAxis} yAxis={props.yAxis} + runQuery={props.runQuery === 'true'} title={title} readOnly={readOnly} isLoading={isLoading} @@ -75,6 +77,7 @@ interface ParsedSqlProps { isLoading?: boolean readOnly?: boolean isChart: boolean + runQuery?: boolean xAxis: string yAxis: string } @@ -82,6 +85,7 @@ interface ParsedSqlProps { export const SqlCard = ({ sql, isChart, + runQuery = false, xAxis, yAxis, title, @@ -99,11 +103,11 @@ export const SqlCard = ({ const isInSQLEditor = router.pathname.includes('/sql') const isInNewSnippet = router.pathname.endsWith('/sql') - const [showCode, setShowCode] = useState(readOnly || !isReadOnlySelect(sql)) + const [showCode, setShowCode] = useState(readOnly || !runQuery || !isReadOnlySelect(sql)) const [showResults, setShowResults] = useState(false) const [results, setResults] = useState() const [error, setError] = useState() - const [showWarning, setShowWarning] = useState(false) + const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>() const { mutate: sendEvent } = useSendEventMutation() @@ -117,12 +121,12 @@ export const SqlCard = ({ setShowResults(true) setResults(res.result) - setShowWarning(false) + setShowWarning(undefined) }, onError: (error) => { setError(error) setResults([]) - setShowWarning(false) + setShowWarning(undefined) }, }) @@ -130,8 +134,10 @@ export const SqlCard = ({ if (!project?.ref || !sql || readOnly) return if (!isReadOnlySelect(sql)) { + const hasUnknownFunctions = containsUnknownFunction(sql) + setShowCode(true) - setShowWarning(true) + setShowWarning(hasUnknownFunctions ? 'hasUnknownFunctions' : 'hasWriteOperation') return } @@ -160,24 +166,29 @@ export const SqlCard = ({ (error?.formattedError?.split('\n') ?? [])?.filter((x: string) => x.length > 0) ?? [] useEffect(() => { - if (isReadOnlySelect(sql) && !results && !readOnly && !isLoading) { + if (runQuery && isReadOnlySelect(sql) && !results && !readOnly && !isLoading) { handleExecute() } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sql, readOnly, isLoading]) + }, [sql, readOnly, isLoading, runQuery]) return ( -
+
- {showWarning ? ( + {!!showWarning ? ( -

This query contains write operations. Are you sure you want to execute it?

+

+ {showWarning === 'hasWriteOperation' + ? 'This query contains write operations.' + : 'This query involves running a function.'}{' '} + Are you sure you want to execute it? +

@@ -186,7 +197,7 @@ export const SqlCard = ({ size="tiny" className="w-full flex-1" onClick={() => { - setShowWarning(false) + setShowWarning(undefined) executeSql({ sql: suffixWithLimit(sql, 100), projectRef: project?.ref, diff --git a/apps/studio/components/ui/PasswordStrengthBar.tsx b/apps/studio/components/ui/PasswordStrengthBar.tsx index f1c6f4cd402bf..b1b2b5fdc5302 100644 --- a/apps/studio/components/ui/PasswordStrengthBar.tsx +++ b/apps/studio/components/ui/PasswordStrengthBar.tsx @@ -37,7 +37,7 @@ const PasswordStrengthBar = ({

{passwordStrengthMessage ? passwordStrengthMessage - : 'This is the password to your postgres database, so it must be strong and hard to guess.'}{' '} + : 'This is the password to your Postgres database, so it must be strong and hard to guess.'}{' '} {name} {label !== undefined && ( - + {label} )} diff --git a/apps/studio/data/auth/validate-spam-mutation.ts b/apps/studio/data/auth/validate-spam-mutation.ts index 78a46ac777d13..9b74c3becb3b5 100644 --- a/apps/studio/data/auth/validate-spam-mutation.ts +++ b/apps/studio/data/auth/validate-spam-mutation.ts @@ -7,7 +7,7 @@ import type { ResponseError } from 'types' export type ValidateSpamVariables = { projectRef: string - template: components['schemas']['ValidateSpamBody'] + template: components['schemas']['ValidateSpamBodyDto'] } export type ValidateSpamResponse = components['schemas']['ValidateSpamResponse'] diff --git a/apps/studio/data/config/project-creation-postgres-versions-query.ts b/apps/studio/data/config/project-creation-postgres-versions-query.ts index a54d42beb5e0d..119151c5587eb 100644 --- a/apps/studio/data/config/project-creation-postgres-versions-query.ts +++ b/apps/studio/data/config/project-creation-postgres-versions-query.ts @@ -31,7 +31,7 @@ export async function getPostgresCreationVersions( }) if (error) handleError(error) - return data as ProjectCreationPostgresVersionsResponse + return data } export type ProjectCreationPostgresVersionData = Awaited< @@ -60,3 +60,16 @@ export const useProjectCreationPostgresVersionsQuery = { + const { data } = useProjectCreationPostgresVersionsQuery({ + cloudProvider, + dbRegion, + organizationSlug, + }) + return (data?.available_versions ?? []).find((x) => x.postgres_engine === '17-oriole') +} diff --git a/apps/studio/data/database/backups-query.ts b/apps/studio/data/database/backups-query.ts index f1ebb9dde031a..5c26794708882 100644 --- a/apps/studio/data/database/backups-query.ts +++ b/apps/studio/data/database/backups-query.ts @@ -4,6 +4,7 @@ import type { components } from 'data/api' import { get, handleError } from 'data/fetchers' import type { ResponseError } from 'types' import { databaseKeys } from './keys' +import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' export type BackupsVariables = { projectRef?: string @@ -29,9 +30,13 @@ export type BackupsError = ResponseError export const useBackupsQuery = ( { projectRef }: BackupsVariables, { enabled = true, ...options }: UseQueryOptions = {} -) => - useQuery( +) => { + // [Joshen] Check for specifically false to account for project not loaded yet + const isOrioleDb = useIsOrioleDb() + + return useQuery( databaseKeys.backups(projectRef), ({ signal }) => getBackups({ projectRef }, signal), - { enabled: enabled && typeof projectRef !== 'undefined', ...options } + { enabled: enabled && isOrioleDb === false && typeof projectRef !== 'undefined', ...options } ) +} diff --git a/apps/studio/data/organizations/keys.ts b/apps/studio/data/organizations/keys.ts index 4cd9bd10ae908..fb74eee4f15d7 100644 --- a/apps/studio/data/organizations/keys.ts +++ b/apps/studio/data/organizations/keys.ts @@ -1,5 +1,6 @@ export const organizationKeys = { list: () => ['organizations'] as const, + detail: (slug?: string) => ['organizations', slug] as const, members: (slug?: string) => ['organizations', slug, 'members'] as const, paymentMethods: (slug: string | undefined) => ['organizations', slug, 'payment-methods'] as const, roles: (slug: string | undefined) => ['organizations', slug, 'roles'] as const, diff --git a/apps/studio/data/organizations/organization-query.ts b/apps/studio/data/organizations/organization-query.ts new file mode 100644 index 0000000000000..2ee9543f5bed3 --- /dev/null +++ b/apps/studio/data/organizations/organization-query.ts @@ -0,0 +1,49 @@ +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { components } from 'api-types' +import { get, handleError } from 'data/fetchers' +import type { ResponseError } from 'types' +import { organizationKeys } from './keys' + +export type OrganizationVariables = { slug?: string } +export type OrganizationDetail = components['schemas']['OrganizationSlugResponse'] + +function castOrganizationSlugResponseToOrganization( + org: components['schemas']['OrganizationSlugResponse'] +) { + return { + ...org, + billing_email: org.billing_email ?? 'Unknown', + managed_by: org.slug.startsWith('vercel_icfg_') ? 'vercel-marketplace' : 'supabase', + partner_id: org.slug.startsWith('vercel_') ? org.slug.replace('vercel_', '') : undefined, + } +} + +export async function getOrganization({ slug }: OrganizationVariables, signal?: AbortSignal) { + if (!slug) throw new Error('Organization slug is required') + + const { data, error } = await get('/platform/organizations/{slug}', { + params: { path: { slug } }, + signal, + }) + if (error) handleError(error) + return castOrganizationSlugResponseToOrganization(data) +} + +export type OrganizationsData = Awaited> +export type OrganizationsError = ResponseError + +export const useOrganizationQuery = ( + { slug }: OrganizationVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => { + return useQuery( + organizationKeys.detail(slug), + ({ signal }) => getOrganization({ slug }, signal), + { enabled: enabled && typeof slug !== 'undefined', ...options, staleTime: 30 * 60 * 1000 } + ) +} + +export function invalidateOrganizationsQuery(client: QueryClient) { + return client.invalidateQueries(organizationKeys.list()) +} diff --git a/apps/studio/data/projects/project-delete-mutation.ts b/apps/studio/data/projects/project-delete-mutation.ts index 3d298680eddcc..31e2a941aa2ca 100644 --- a/apps/studio/data/projects/project-delete-mutation.ts +++ b/apps/studio/data/projects/project-delete-mutation.ts @@ -4,9 +4,11 @@ import { toast } from 'sonner' import { del, handleError } from 'data/fetchers' import type { ResponseError } from 'types' import { projectKeys } from './keys' +import { organizationKeys } from 'data/organizations/keys' export type ProjectDeleteVariables = { projectRef: string + organizationSlug?: string } export async function deleteProject({ projectRef }: ProjectDeleteVariables) { @@ -34,8 +36,15 @@ export const useProjectDeleteMutation = ({ (vars) => deleteProject(vars), { async onSuccess(data, variables, context) { - await queryClient.invalidateQueries(projectKeys.list()), - await onSuccess?.(data, variables, context) + await queryClient.invalidateQueries(projectKeys.list()) + + if (variables.organizationSlug) { + queryClient.invalidateQueries( + organizationKeys.freeProjectLimitCheck(variables.organizationSlug) + ) + } + + await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { if (onError === undefined) { diff --git a/apps/studio/hooks/misc/useSelectedOrganization.ts b/apps/studio/hooks/misc/useSelectedOrganization.ts index 8f82650b48241..05c0276a167d2 100644 --- a/apps/studio/hooks/misc/useSelectedOrganization.ts +++ b/apps/studio/hooks/misc/useSelectedOrganization.ts @@ -4,6 +4,30 @@ import { useMemo } from 'react' import { LOCAL_STORAGE_KEYS } from 'lib/constants' import { useProjectByRef } from './useSelectedProject' +import { useOrganizationQuery } from 'data/organizations/organization-query' + +// [Joshen] Scaffolding this first - will need to double check if this can replace useSelectedOrganization +export function useSelectedOrganizationV2({ enabled = true } = {}) { + const isLoggedIn = useIsLoggedIn() + const { ref, slug } = useParams() + + const selectedProject = useProjectByRef(ref) + const localStorageSlug = useMemo(() => { + return typeof window !== 'undefined' + ? localStorage.getItem(LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION) + : null + }, []) + + const orgSlug = slug ?? selectedProject?.organization_slug ?? localStorageSlug + const { data } = useOrganizationQuery( + { slug: orgSlug as string }, + { enabled: enabled && isLoggedIn && typeof orgSlug === 'string' } + ) + + return useMemo(() => { + return data + }, [data, slug, selectedProject, localStorageSlug]) +} export function useSelectedOrganization({ enabled = true } = {}) { const isLoggedIn = useIsLoggedIn() diff --git a/apps/studio/hooks/misc/useSelectedProject.ts b/apps/studio/hooks/misc/useSelectedProject.ts index e90ec47fbb6fd..4d5ebd6b6e16b 100644 --- a/apps/studio/hooks/misc/useSelectedProject.ts +++ b/apps/studio/hooks/misc/useSelectedProject.ts @@ -21,3 +21,9 @@ export function useProjectByRef(ref?: string) { return projects?.find((project) => project.ref === ref) }, [projects, ref]) } + +export const useIsOrioleDb = () => { + const project = useSelectedProject() + const isOrioleDb = project?.dbVersion?.endsWith('orioledb') + return isOrioleDb +} diff --git a/apps/studio/package.json b/apps/studio/package.json index 62e41e283474e..0dd1d48a1e081 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -122,6 +122,7 @@ "tus-js-client": "^4.1.0", "ui": "*", "ui-patterns": "*", + "use-debounce": "^7.0.1", "uuid": "^9.0.1", "valtio": "^1.12.0", "vite-tsconfig-paths": "^4.3.2", diff --git a/apps/studio/pages/api/ai/sql/generate-v3.ts b/apps/studio/pages/api/ai/sql/generate-v3.ts index 0e4f60b6af7f1..55517ba1c0b3a 100644 --- a/apps/studio/pages/api/ai/sql/generate-v3.ts +++ b/apps/studio/pages/api/ai/sql/generate-v3.ts @@ -76,11 +76,14 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { - Always use semicolons - Output as markdown - Always include code snippets if available - - If a code snippet is SQL, the first line of the snippet should always be -- props: {"title": "Query title", "isChart": "true", "xAxis": "columnOrAlias", "yAxis": "columnOrAlias"} + - If a code snippet is SQL, the first line of the snippet should always be -- props: {"title": "Query title", "runQuery": "false", "isChart": "true", "xAxis": "columnOrAlias", "yAxis": "columnOrAlias"} + - Only include one line of comment props per markdown snippet, even if the snippet has multiple queries - Only set chart to true if the query makes sense as a chart. xAxis and yAxis need to be columns or aliases returned by the query. + - Only set runQuery to true if the query has no risk of writing data and is not a debugging request. Set it to false if there are any values that need to be replaced with real data. - Explain what the snippet does in a sentence or two before showing it - Use vector(384) data type for any embedding/vector related query - When debugging, retrieve sql schema details to ensure sql is correct + - In Supabase, the auth schema already has a users table which is used to store users. It is common practice to create a profiles table in the public schema that links to auth.users to store user information instead. You don't need to create a new users table. When generating tables, do the following: - For primary keys, always use "id bigint primary key generated always as identity" (not serial) @@ -109,13 +112,15 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { # You convert sql to supabase-js client code Use the convertSqlToSupabaseJs tool to convert select sql to supabase-js client code. Only provide js code snippets if explicitly asked. If conversion isn't supported, build a postgres function instead and suggest using supabase-js to call it via "const { data, error } = await supabase.rpc('echo', { say: '👋'})" - Follow these instructions: - - First look at the list of provided schemas and if needed, get more information about a schema. You will almost always need to retrieve information about the public schema before answering a question. If the question is about users, also retrieve the auth schema. + # For all your abilities, follow these instructions: + - First look at the list of provided schemas and if needed, get more information about a schema. You will almost always need to retrieve information about the public schema before answering a question. + - If the question is about users or involves creating a users table, also retrieve the auth schema. + Here are the existing database schema names you can retrieve: ${schemas} - ${schema !== undefined ? `The user is currently looking at the ${schema} schema.` : ''} - ${table !== undefined ? `The user is currently looking at the ${table} table.` : ''} + ${schema !== undefined && includeSchemaMetadata ? `The user is currently looking at the ${schema} schema.` : ''} + ${table !== undefined && includeSchemaMetadata ? `The user is currently looking at the ${table} table.` : ''} `, messages, tools: getTools({ projectRef, connectionString, authorization, includeSchemaMetadata }), diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 85f5080819514..1d9d736d17ad6 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -1,7 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { debounce } from 'lodash' -import { ChevronRight, ExternalLink } from 'lucide-react' +import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { PropsWithChildren, useEffect, useRef, useState } from 'react' @@ -16,11 +16,13 @@ import { FreeProjectLimitWarning, NotOrganizationOwnerWarning, } from 'components/interfaces/Organization/NewProject' +import { AdvancedConfiguration } from 'components/interfaces/ProjectCreation/AdvancedConfiguration' import { PostgresVersionSelector, extractPostgresVersionDetails, } from 'components/interfaces/ProjectCreation/PostgresVersionSelector' import { RegionSelector } from 'components/interfaces/ProjectCreation/RegionSelector' +import { SecurityOptions } from 'components/interfaces/ProjectCreation/SecurityOptions' import { WizardLayoutWithoutAuth } from 'components/layouts/WizardLayout' import DisabledWarningDueToIncident from 'components/ui/DisabledWarningDueToIncident' import Panel from 'components/ui/Panel' @@ -57,16 +59,10 @@ import type { NextPageWithLayout } from 'types' import { Badge, Button, - CollapsibleContent_Shadcn_, - CollapsibleTrigger_Shadcn_, - Collapsible_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, - FormItem_Shadcn_, Form_Shadcn_, Input_Shadcn_, - RadioGroupStacked, - RadioGroupStackedItem, SelectContent_Shadcn_, SelectGroup_Shadcn_, SelectItem_Shadcn_, @@ -79,12 +75,12 @@ import { TableHead, TableHeader, TableRow, - cn, } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' +import { useAvailableOrioleImageVersion } from 'data/config/project-creation-postgres-versions-query' type DesiredInstanceSize = components['schemas']['DesiredInstanceSize'] @@ -101,6 +97,39 @@ const sizes: DesiredInstanceSize[] = [ '16xlarge', ] +const FormSchema = z.object({ + organization: z.string({ + required_error: 'Please select an organization', + }), + projectName: z + .string() + .min(1, 'Please enter a project name.') // Required field check + .min(3, 'Project name must be at least 3 characters long.') // Minimum length check + .max(64, 'Project name must be no longer than 64 characters.'), // Maximum length check + postgresVersion: z.string({ + required_error: 'Please enter a Postgres version.', + }), + dbRegion: z.string({ + required_error: 'Please select a region.', + }), + cloudProvider: z.string({ + required_error: 'Please select a cloud provider.', + }), + dbPassStrength: z.number(), + dbPass: z + .string({ + required_error: 'Please enter a database password.', + }) + .min(1, 'Password is required.'), + instanceSize: z.string(), + dataApi: z.boolean(), + useApiSchema: z.boolean(), + postgresVersionSelection: z.string(), + useOrioleDb: z.boolean(), +}) + +export type CreateProjectForm = z.infer + const Wizard: NextPageWithLayout = () => { const router = useRouter() const { slug, projectName } = useParams() @@ -108,6 +137,7 @@ const Wizard: NextPageWithLayout = () => { const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate') const projectVersionSelectionDisabled = useFlag('disableProjectVersionSelection') const cloudProviderEnabled = useFlag('enableFlyCloudProvider') + const allowOrioleDB = useFlag('allowOrioleDb') const { data: membersExceededLimit, isLoading: isLoadingFreeProjectLimitCheck } = useFreeProjectLimitCheckQuery({ slug }) @@ -186,45 +216,15 @@ const Wizard: NextPageWithLayout = () => { setPasswordStrengthMessage(message) } - const FormSchema = z - .object({ - organization: z.string({ - required_error: 'Please select an organization', - }), - projectName: z - .string() - .min(1, 'Please enter a project name.') // Required field check - .min(3, 'Project name must be at least 3 characters long.') // Minimum length check - .max(64, 'Project name must be no longer than 64 characters.'), // Maximum length check - postgresVersion: z.string({ - required_error: 'Please enter a Postgres version.', - }), - dbRegion: z.string({ - required_error: 'Please select a region.', - }), - cloudProvider: z.string({ - required_error: 'Please select a cloud provider.', - }), - dbPassStrength: z.number(), - dbPass: z - .string({ - required_error: 'Please enter a database password.', - }) - .min(1, 'Password is required.'), - instanceSize: z.string(), - dataApi: z.boolean(), - useApiSchema: z.boolean(), - postgresVersionSelection: z.string(), - }) - .superRefine(({ dbPassStrength }, refinementContext) => { - if (dbPassStrength < DEFAULT_MINIMUM_PASSWORD_STRENGTH) { - refinementContext.addIssue({ - code: z.ZodIssueCode.custom, - path: ['dbPass'], - message: passwordStrengthWarning || 'Password not secure enough', - }) - } - }) + FormSchema.superRefine(({ dbPassStrength }, refinementContext) => { + if (dbPassStrength < DEFAULT_MINIMUM_PASSWORD_STRENGTH) { + refinementContext.addIssue({ + code: z.ZodIssueCode.custom, + path: ['dbPass'], + message: passwordStrengthWarning || 'Password not secure enough', + }) + } + }) const form = useForm>({ resolver: zodResolver(FormSchema), @@ -241,10 +241,17 @@ const Wizard: NextPageWithLayout = () => { dataApi: true, useApiSchema: false, postgresVersionSelection: '', + useOrioleDb: false, }, }) - const { instanceSize } = form.watch() + const { instanceSize, cloudProvider, dbRegion, organization } = form.watch() + + const availableOrioleVersion = useAvailableOrioleImageVersion({ + cloudProvider: cloudProvider as CloudProvider, + dbRegion, + organizationSlug: organization, + }) // [kevin] This will eventually all be provided by a new API endpoint to preview and validate project creation, this is just for kaizen now const monthlyComputeCosts = @@ -278,8 +285,13 @@ const Wizard: NextPageWithLayout = () => { dataApi, useApiSchema, postgresVersionSelection, + useOrioleDb, } = values + if (useOrioleDb && !availableOrioleVersion) { + return toast.error('No available OrioleDB image found, only Postgres is available') + } + const { postgresEngine, releaseChannel } = extractPostgresVersionDetails(postgresVersionSelection) @@ -296,9 +308,10 @@ const Wizard: NextPageWithLayout = () => { orgSubscription?.plan.id === 'free' ? undefined : (instanceSize as DesiredInstanceSize), dataApiExposedSchemas: !dataApi ? [] : undefined, dataApiUseApiSchema: !dataApi ? false : useApiSchema, - postgresEngine: postgresEngine, - releaseChannel: releaseChannel, + postgresEngine: useOrioleDb ? availableOrioleVersion?.postgres_engine : postgresEngine, + releaseChannel: useOrioleDb ? availableOrioleVersion?.release_channel : releaseChannel, } + if (postgresVersion) { if (!postgresVersion.match(/1[2-9]\..*/)) { toast.error( @@ -838,144 +851,10 @@ const Wizard: NextPageWithLayout = () => { )} - - - - Security options - - - - ( - <> - - - field.onChange(value === 'true')} - defaultValue={field.value.toString()} - > - - - - - - - - - - - - - {!form.getValues('dataApi') && ( - - PostgREST which powers the Data API will have no schemas - available to it. - - )} - - - )} - /> - - {form.getValues('dataApi') && ( - ( - <> - - - field.onChange(value === 'true')} - > - - - - Use public schema for Data API - - Default - - - } - // @ts-ignore - description={ - <> - Query all tables in the{' '} - public schema - - } - /> - - - - - - Query allowlisted tables in a dedicated{' '} - api schema - - } - /> - - - - - - - )} - /> - )} -

- These settings can be changed after the project is created via the - project's settings -

- - - + + {allowOrioleDB && !!availableOrioleVersion && ( + + )} )} diff --git a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx index 913ad9d7f74cb..e6db1c7bde410 100644 --- a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -8,6 +8,7 @@ import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' +import { DocsButton } from 'components/ui/DocsButton' import { FormHeader } from 'components/ui/Forms/FormHeader' import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' @@ -16,9 +17,11 @@ import { useBackupsQuery } from 'data/database/backups-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_ } from 'ui' +import { Admonition } from 'ui-patterns' const DatabasePhysicalBackups: NextPageWithLayout = () => { return ( @@ -46,6 +49,7 @@ const PITR = () => { const { ref: projectRef } = useParams() const { project } = useProjectContext() const organization = useSelectedOrganization() + const isOrioleDb = useIsOrioleDb() const { data: backups, error, isLoading, isError, isSuccess } = useBackupsQuery({ projectRef }) const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) @@ -61,6 +65,18 @@ const PITR = () => { return } + if (isOrioleDb) { + return ( + + + + ) + } + return ( <> {isLoading && } diff --git a/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx b/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx index 5c9d1c349c082..9fa1a73b3eac2 100644 --- a/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/restore-to-new-project.tsx @@ -14,21 +14,23 @@ import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' +import { DocsButton } from 'components/ui/DocsButton' import { FormHeader } from 'components/ui/Forms/FormHeader' import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useCloneBackupsQuery } from 'data/projects/clone-query' import { useCloneStatusQuery } from 'data/projects/clone-status-query' +import { useProjectsQuery } from 'data/projects/projects-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' import { getDatabaseMajorVersion } from 'lib/helpers' import type { NextPageWithLayout } from 'types' import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui' import { Admonition } from 'ui-patterns' -import { useProjectsQuery } from 'data/projects/projects-query' const RestoreToNewProjectPage: NextPageWithLayout = () => { return ( @@ -57,6 +59,7 @@ const RestoreToNewProject = () => { const organization = useSelectedOrganization() const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const isFreePlan = subscription?.plan?.id === 'free' + const isOrioleDb = useIsOrioleDb() const [refetchInterval, setRefetchInterval] = useState(false) const [selectedBackupId, setSelectedBackupId] = useState(null) @@ -113,6 +116,18 @@ const RestoreToNewProject = () => { (p) => p.ref === cloneStatus?.clones?.[0]?.target_project.ref ) + if (isOrioleDb) { + return ( + + + + ) + } + if (isLoading) { return } diff --git a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx index ee52cf9c265ca..46f2c0d8761bb 100644 --- a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -7,19 +7,23 @@ import DatabaseBackupsNav from 'components/interfaces/Database/Backups/DatabaseB import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' +import { DocsButton } from 'components/ui/DocsButton' import { FormHeader } from 'components/ui/Forms/FormHeader' import InformationBox from 'components/ui/InformationBox' import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useBackupsQuery } from 'data/database/backups-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' import type { NextPageWithLayout } from 'types' +import { Admonition } from 'ui-patterns' const DatabaseScheduledBackups: NextPageWithLayout = () => { const { ref: projectRef } = useParams() const { data: backups, error, isLoading, isError, isSuccess } = useBackupsQuery({ projectRef }) + const isOrioleDb = useIsOrioleDb() const isPitrEnabled = backups?.pitr_enabled const isPermissionsLoaded = usePermissionsLoaded() @@ -33,52 +37,63 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => { -
- {isLoading && } - {isError && ( - - )} + {isOrioleDb ? ( + + + + ) : ( +
+ {isLoading && } - {isSuccess && ( - <> - {!isPitrEnabled && ( -

- Projects are backed up daily around midnight of your project's region and can - be restored at any time. -

- )} + {isError && ( + + )} - {isPitrEnabled && ( - } - title="Point-In-Time-Recovery (PITR) enabled" - description={ -
- Your project uses PITR and full daily backups are no longer taken. They're - not needed, as PITR supports a superset of functionality, in terms of the - granular recovery that can be performed.{' '} - - Learn more - -
- } - /> - )} + {isSuccess && ( + <> + {!isPitrEnabled && ( +

+ Projects are backed up daily around midnight of your project's region and + can be restored at any time. +

+ )} - {isPermissionsLoaded && !canReadScheduledBackups ? ( - - ) : ( - - )} - - )} -
+ {isPitrEnabled && ( + } + title="Point-In-Time-Recovery (PITR) enabled" + description={ +
+ Your project uses PITR and full daily backups are no longer taken. + They're not needed, as PITR supports a superset of functionality, in + terms of the granular recovery that can be performed.{' '} + + Learn more + +
+ } + /> + )} + + {isPermissionsLoaded && !canReadScheduledBackups ? ( + + ) : ( + + )} + + )} +
+ )}
diff --git a/apps/studio/pages/project/[ref]/index.tsx b/apps/studio/pages/project/[ref]/index.tsx index 34669382870f0..836b11b3f7a71 100644 --- a/apps/studio/pages/project/[ref]/index.tsx +++ b/apps/studio/pages/project/[ref]/index.tsx @@ -12,16 +12,26 @@ import { ProjectLayoutWithAuth } from 'components/layouts/ProjectLayout/ProjectL import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper' import ProjectUpgradeFailedBanner from 'components/ui/ProjectUpgradeFailedBanner' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { useIsOrioleDb, useSelectedProject } from 'hooks/misc/useSelectedProject' import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import type { NextPageWithLayout } from 'types' -import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' +import { + Badge, + Tabs_Shadcn_, + TabsContent_Shadcn_, + TabsList_Shadcn_, + TabsTrigger_Shadcn_, + Tooltip_Shadcn_, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, +} from 'ui' const Home: NextPageWithLayout = () => { const organization = useSelectedOrganization() const project = useSelectedProject() + const isOrioleDb = useIsOrioleDb() const snap = useAppStateSnapshot() const { enableBranching } = useParams() @@ -44,6 +54,27 @@ const Home: NextPageWithLayout = () => {

{projectName}

+ {isOrioleDb && ( + + + OrioleDB + + + This project is using Postgres with OrioleDB which is currently in preview and not + suitable for production workloads. View our{' '} + {/* [Refactor] Make this into a reusable component to use links inline */} + + documentation + {' '} + for all limitations. + + + )} { const [sentCategory, setSentCategory] = useState() const [selectedProject, setSelectedProject] = useState('no-project') const { data, isLoading } = usePlatformStatusQuery() const isHealthy = data?.isHealthy - const router = useRouter() - const { ref } = router.query - const { data: projectsData, isLoading: isLoadingProjects } = useProjectsQuery() + const { data: projectsData } = useProjectsQuery() return (
diff --git a/apps/studio/public/img/icons/calendly-icon.svg b/apps/studio/public/img/icons/calendly-icon.svg new file mode 100644 index 0000000000000..292335251934c --- /dev/null +++ b/apps/studio/public/img/icons/calendly-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/studio/static-data/integrations/calendly_wrapper/overview.md b/apps/studio/static-data/integrations/calendly_wrapper/overview.md new file mode 100644 index 0000000000000..b04fd1909cb0b --- /dev/null +++ b/apps/studio/static-data/integrations/calendly_wrapper/overview.md @@ -0,0 +1,3 @@ +Calendly is a scheduling platform used for teams to schedule, prepare and follow up on external meetings. + +The Calendly Wrapper is a WebAssembly(Wasm) foreign data wrapper which allows you to read data from your Calendly for use within your Postgres database. diff --git a/apps/www/_blog/2024-12-01-orioledb-launch.mdx b/apps/www/_blog/2024-12-01-orioledb-launch.mdx new file mode 100644 index 0000000000000..e07df745d5b65 --- /dev/null +++ b/apps/www/_blog/2024-12-01-orioledb-launch.mdx @@ -0,0 +1,48 @@ +--- +title: 'OrioleDB Public Alpha' +description: 'Launching OrioleDB Public Alpha' +author: pavel +image: 2024-12-01-orioledb-release/thumb.png +thumb: 2024-12-01-orioledb-release/thumb.png +categories: + - engineering +tags: + - supabase-engineering +date: '2024-12-01' +toc_depth: 3 +--- + +# OrioleDB Public Alpha + +Today, we're releasing the Public Alpha of [OrioleDB](https://www.orioledb.com/) on the Supabase platform. + +### What’s OrioleDB? + +OrioleDB is a **storage extension** which uses Postgres' pluggable storage system. It’s designed to be a drop-in replacement for Postgres’ default Heap storage. + +You can read more about OrioleDB [here](https://www.orioledb.com/blog/orioledb-beta7-benchmarks) and learn why you might choose it over the default Postgres storage engine. + +### Limitations + +This initial release is a Public Alpha and you should _not_ use it for Production workloads. The release comes with several limitations: + +- The release is restricted to Free organizations. You will not be able to upgrade OrioleDB projects to larger instance sizes. If you want to run OrioleDB on a larger instance we suggest following the [Getting Started](https://www.orioledb.com/docs/usage/getting-started) guide on OrioleDB’s official website. +- Index support is restricted to the Postgres default B-Tree index type. Other indexs like GIN/GiST/BRIN/Hash, and pgvector's HNSW/IVFFlat are not supported. +- No support for Supabase Realtime Postgres Changes. Realtime Broadcast and Presence will continue to work. + +### Should you use it today? + +At this stage, the goal of adding OrioleDB to the platform is to make it easier for testers to give feedback. If you’re running Production workloads, stick to the standard options available. + +### Getting started and more info + +To get started today, go to [database.new](http://database.new) and choose “Postgres with OrioleDB” under the "Advanced Configuration" section when launching a new database. + +orioledb-project-create + +If you want to learn more about OrioleDB and their vision for the future, check out the [blog post the OrioleDB team released today](https://www.orioledb.com/blog/orioledb-beta7-benchmarks). diff --git a/apps/www/components/AIDemo/DotGrid.tsx b/apps/www/components/AIDemo/DotGrid.tsx new file mode 100644 index 0000000000000..64ef0697bdef1 --- /dev/null +++ b/apps/www/components/AIDemo/DotGrid.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { motion } from 'framer-motion' + +interface DotGridProps { + rows: number + columns: number + count: number +} + +const DotGrid: React.FC = ({ rows, columns, count }) => { + const container = { + hidden: { opacity: 1 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.01, + delayChildren: 0.01, + }, + }, + } + + const item = { + hidden: { opacity: 0 }, + visible: { opacity: 0.3 }, + } + + const highlightedVariants = { + visible: { + opacity: [1, 0.5, 1], + transition: { + repeat: Infinity, + duration: 0.5, + repeatDelay: 1.5, + ease: 'easeInOut', + }, + }, + } + + return ( +
+ + {Array.from({ length: rows * columns }).map((_, index) => { + const isHighlighted = index < count + return ( + + ) + })} +
+
+ +
+ ) +} + +export default DotGrid diff --git a/apps/www/components/AIDemo/Panel.tsx b/apps/www/components/AIDemo/Panel.tsx new file mode 100644 index 0000000000000..b1f87ad216bfc --- /dev/null +++ b/apps/www/components/AIDemo/Panel.tsx @@ -0,0 +1,215 @@ +// components/AIDemoPanel.tsx +import { useState, useEffect, useRef } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { Button, Input } from 'ui' +import { AiIconAnimation, cn } from 'ui' +import errorGif from 'public/images/ai/error.gif' +import Image from 'next/image' +import styles from './assistant.module.css' + +interface DemoMessage { + id: string + role: 'user' | 'assistant' + content: string | React.ReactNode + render?: React.ReactNode + createdAt: Date +} + +export const AIDemoPanel = ({ incomingMessages = [] }: { incomingMessages?: DemoMessage[] }) => { + const [messages, setMessages] = useState([]) + const [isComplete, setIsComplete] = useState(false) + const [pendingMessages, setPendingMessages] = useState([]) + const [input, setInput] = useState('') + const [showFade, setShowFade] = useState(false) + const messagesEndRef = useRef(null) + const scrollContainerRef = useRef(null) + + const scrollToBottom = () => { + const container = messagesEndRef.current?.closest('.overflow-y-auto') + if (container) { + container.scrollTo({ + top: container.scrollHeight, + behavior: 'smooth', + }) + } + } + + const handleScroll = () => { + const container = scrollContainerRef.current + if (container) { + const scrollPercentage = + (container.scrollTop / (container.scrollHeight - container.clientHeight)) * 100 + const isScrollable = container.scrollHeight > container.clientHeight + const isAtBottom = scrollPercentage >= 100 + + setShowFade(isScrollable && !isAtBottom) + } + } + + useEffect(() => { + scrollToBottom() + handleScroll() + }, [messages]) + + useEffect(() => { + const container = scrollContainerRef.current + if (container) { + container.addEventListener('scroll', handleScroll) + handleScroll() + } + + return () => { + if (container) { + container.removeEventListener('scroll', handleScroll) + } + } + }, []) + + useEffect(() => { + if (pendingMessages.length > 0) { + const timer = setTimeout(() => { + setMessages((prev) => [...prev, pendingMessages[0]]) + setPendingMessages((prev) => prev.slice(1)) + }, 3000) + return () => clearTimeout(timer) + } else if (!isComplete) { + setIsComplete(true) + } + }, [isComplete, pendingMessages]) + + useEffect(() => { + if (incomingMessages.length > 0) { + setMessages((prev) => [...prev, ...pendingMessages, incomingMessages[0]]) + setPendingMessages(incomingMessages.slice(1)) + } + }, [incomingMessages]) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!input.trim()) return + + const userMessage: DemoMessage = { + id: Date.now().toString(), + role: 'user', + content: input, + createdAt: new Date(), + } + + const assistantMessage: DemoMessage = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: ( +
+ +
+ ), + createdAt: new Date(), + } + + const followUp: DemoMessage = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: "Sorry, custom queries aren't supported in this demo.", + createdAt: new Date(), + } + + setMessages((prev) => [...prev, userMessage]) + setPendingMessages((prev) => [...prev, assistantMessage, followUp]) + setInput('') + } + + return ( +
+
+ {/* Header */} +
+ +
Assistant
+
+ + {/* Messages */} +
+ {messages.map((message, index) => ( + <> + + {message.content} + {message.render &&
{message.render}
} +
+ + ))} + {messages.length > 0 && messages[messages.length - 1].role === 'user' && ( + + Thinking +
+ + . + + + . + + + . + +
+
+ )} +
+
+ + {/* Input */} + + {showFade && ( + +
+ + )} + +
+
+ setInput(e.target.value)} + placeholder="Ask your data anything..." + className={cn( + 'w-full bg-background-muted rounded-md [&>textarea]:border-1 [&>textarea]:rounded-md [&>textarea]:!outline-none [&>textarea]:!ring-offset-0 [&>textarea]:!ring-0 focus:outline-none' + )} + /> + +
+
+
+ ) +} diff --git a/apps/www/components/AIDemo/SqlSnippet.tsx b/apps/www/components/AIDemo/SqlSnippet.tsx new file mode 100644 index 0000000000000..c25c9b6f85213 --- /dev/null +++ b/apps/www/components/AIDemo/SqlSnippet.tsx @@ -0,0 +1,236 @@ +import { useState, useEffect } from 'react' +import { motion } from 'framer-motion' +import { Code, Play, DatabaseIcon } from 'lucide-react' +import { Button, cn, ChartContainer, ChartTooltip, ChartTooltipContent, SQL_ICON } from 'ui' +import { BarChart, Bar, XAxis, CartesianGrid } from 'recharts' +import CodeBlock from '../CodeBlock/CodeBlock' + +interface SqlSnippetProps { + id: string + sql: string + title?: string + resultType?: 'chart' | 'table' + mockData?: any +} + +// Example mock data matching the Results component format +const MOCK_TABLE_DATA = { + rows: [ + { + id: 1, + name: 'John Doe', + email: 'john@example.com', + created_at: '2024-01-15T10:00:00Z', + status: 'active', + metadata: { role: 'admin', last_login: '2024-01-14' }, + }, + { + id: 2, + name: 'Jane Smith', + email: 'jane@example.com', + created_at: '2024-01-14T15:30:00Z', + status: 'pending', + metadata: null, + }, + { + id: 3, + name: 'Bob Wilson', + email: 'bob@example.com', + created_at: '2024-01-13T09:15:00Z', + status: 'active', + metadata: { role: 'user', last_login: '2024-01-12' }, + }, + ], +} + +const MOCK_CHART_DATA = [ + { name: 'Jan', value: 400 }, + { name: 'Feb', value: 300 }, + { name: 'Mar', value: 600 }, + { name: 'Apr', value: 800 }, + { name: 'May', value: 700 }, +] + +export const SqlSnippet = ({ + id, + sql, + title = 'SQL Query', + resultType = 'table', + mockData, +}: SqlSnippetProps) => { + const [showCode, setShowCode] = useState(!mockData) + const [isExecuting, setIsExecuting] = useState(false) + const [showResults, setShowResults] = useState(false) + const [results, setResults] = useState(null) + + const handleExecute = () => { + setIsExecuting(true) + setShowResults(false) + + // Simulate SQL execution delay + setTimeout(() => { + setIsExecuting(false) + if (mockData) { + setShowResults(true) + // Use provided mock data or default mock data + setResults( + resultType === 'chart' + ? mockData || MOCK_CHART_DATA + : mockData?.rows || MOCK_TABLE_DATA.rows + ) + } + }, 1000) + } + + // Auto-execute on mount + useEffect(() => { + handleExecute() + }, []) + + const ResultsDisplay = () => { + if (resultType === 'chart') { + return ( +
+ + + + + } + /> + + + +
+ ) + } + + return ( + <> +
+
+ + + {results?.length > 0 && + Object.keys(results[0]).map((key) => ( + + ))} + + + + {results?.map((row: any, i: number) => ( + + {Object.values(row).map((value: any, j: number) => ( + + ))} + + ))} + +
+ {key} +
+ {value === null + ? 'NULL' + : typeof value === 'object' + ? JSON.stringify(value) + : String(value)} +
+
+
+

+ {results.length} rows + {results.length >= 100 && ' (Limited to only 100 rows)'} +

+
+ + ) + } + + return ( +
+
+
+ +

{title}

+
+
+
+ + {showCode && ( +
+ + {sql} + +
+ )} + + {isExecuting ? ( + + Executing query +
+ + . + + + . + + + . + +
+
+ ) : showResults && results ? ( + + ) : null} +
+
+ ) +} diff --git a/apps/www/components/AIDemo/assistant.module.css b/apps/www/components/AIDemo/assistant.module.css new file mode 100644 index 0000000000000..30adaf69f8ce7 --- /dev/null +++ b/apps/www/components/AIDemo/assistant.module.css @@ -0,0 +1,113 @@ +.border-gradient { + --color1: #0a5c36; + --color2: #2ecc71; + --color3: #b44bff; + --angle: 0deg; + + border: none; + position: relative; + transform-style: preserve-3d; +} + +.border-gradient::after { + content: ''; + position: absolute; + left: 0; + right: 0; + margin-inline: auto; + width: calc(100% + 2px); + height: calc(100% + 2px); + left: 50%; + top: 50%; + border-radius: 6px; + transform: translate3d(-50%, -50%, -2px); + background: linear-gradient(var(--angle), var(--color1), var(--color2), var(--color3)); + filter: blur(0); + animation: animate-bg 10s linear infinite alternate; +} + +.border-gradient::before { + transform: translate3d(-50%, -50%, -1px); + backdrop-filter: blur(0); +} + +@property --angle { + syntax: ''; + initial-value: 0deg; + inherits: false; +} + +@property --color1 { + syntax: ''; + initial-value: #0a5c36; + inherits: false; +} + +@property --color2 { + syntax: ''; + initial-value: #2ecc71; + inherits: false; +} + +@property --color3 { + syntax: ''; + initial-value: #b44bff; + inherits: false; +} + +@keyframes animate-bg { + 0% { + --angle: 0deg; + --color1: #0a5c36; + --color2: #2ecc71; + --color3: #b44bff; + } + 12.5% { + --angle: 45deg; + --color1: #2ecc71; + --color2: #b44bff; + --color3: #0a5c36; + } + 25% { + --angle: 90deg; + --color1: #b44bff; + --color2: #0a5c36; + --color3: #2ecc71; + } + 37.5% { + --angle: 135deg; + --color1: #0a5c36; + --color2: #2ecc71; + --color3: #b44bff; + } + 50% { + --angle: 180deg; + --color1: #2ecc71; + --color2: #b44bff; + --color3: #0a5c36; + } + 62.5% { + --angle: 225deg; + --color1: #b44bff; + --color2: #0a5c36; + --color3: #2ecc71; + } + 75% { + --angle: 270deg; + --color1: #0a5c36; + --color2: #2ecc71; + --color3: #b44bff; + } + 87.5% { + --angle: 315deg; + --color1: #2ecc71; + --color2: #b44bff; + --color3: #0a5c36; + } + 100% { + --angle: 360deg; + --color1: #0a5c36; + --color2: #2ecc71; + --color3: #b44bff; + } +} diff --git a/apps/www/pages/assistant.tsx b/apps/www/pages/assistant.tsx new file mode 100644 index 0000000000000..9730ba19c5556 --- /dev/null +++ b/apps/www/pages/assistant.tsx @@ -0,0 +1,773 @@ +import { Button } from 'ui' +import dynamic from 'next/dynamic' +import { AIDemoPanel } from '~/components/AIDemo/Panel' +import { useEffect, useState } from 'react' +import { SqlSnippet } from '~/components/AIDemo/SqlSnippet' +import { NextSeo } from 'next-seo' +import { motion } from 'framer-motion' +import { useRouter } from 'next/router' +import { useIsLoggedIn, useIsUserLoading } from 'common' +import Link from 'next/link' +import DotGrid from '~/components/AIDemo/DotGrid' +import { EASE_OUT } from '../lib/animations' + +const DefaultLayout = dynamic(() => import('~/components/Layouts/Default')) +const SectionContainer = dynamic(() => import('~/components/Layouts/SectionContainer')) + +interface Message { + id: string + role: 'user' | 'assistant' + content: string | JSX.Element + createdAt: Date + render?: JSX.Element +} + +const welcomeMessages = [ + { + id: 'ph-1', + role: 'user' as const, + content: 'I have come from ProductHunt. Show me the goods!', + createdAt: new Date(), + }, + { + id: 'ph-2', + role: 'assistant' as const, + content: ( +
+ + + + + + +

+ Welcome Product Hunter! 👋 Thanks for checking out the Supabase assistant. Let me show you + what I can do! +

+
+ ), + createdAt: new Date(), + }, +] + +const demoQueries = [ + { + label: 'User Growth Over Time', + messages: [ + { + id: '1', + role: 'user' as const, + content: 'Show me user signups over the past 12 months', + createdAt: new Date(), + }, + { + id: '2', + role: 'assistant' as const, + content: 'Here is a chart showing user growth by month:', + createdAt: new Date(), + render: ( + + ), + }, + ], + }, + { + label: 'Latest Products', + messages: [ + { + id: '23', + role: 'user' as const, + content: 'Show me our latest products', + createdAt: new Date(), + }, + { + id: '24', + role: 'assistant' as const, + content: 'Here are the 5 most recently added products:', + createdAt: new Date(), + render: ( + + ), + }, + ], + }, + { + label: 'Create Function', + messages: [ + { + id: '19', + role: 'user' as const, + content: 'Help me create a function to calculate user points', + createdAt: new Date(), + }, + { + id: '20', + role: 'assistant' as const, + content: "Here's a Postgres function that calculates user points based on their activity:", + createdAt: new Date(), + render: ( + + ), + }, + ], + }, + { + label: 'RLS Policy', + messages: [ + { + id: '21', + role: 'user' as const, + content: 'Show me how to create an RLS policy for a team members table', + createdAt: new Date(), + }, + { + id: '22', + role: 'assistant' as const, + content: + "Here's an RLS policy that allows team members to only see other members in their team:", + createdAt: new Date(), + render: ( + + ), + }, + ], + }, + { + label: 'SQL to Supabase-js', + messages: [ + { + id: '23', + role: 'user' as const, + content: 'How do I query all projects?', + createdAt: new Date(), + }, + { + id: '24', + role: 'assistant' as const, + content: "Here's the SQL query to get all projects:", + createdAt: new Date(), + render: ( + + ), + }, + { + id: '25', + role: 'user' as const, + content: 'Can you show me how to do this with the Supabase client?', + createdAt: new Date(), + }, + { + id: '26', + role: 'assistant' as const, + content: "Here's how to perform the same query using the Supabase JavaScript client:", + createdAt: new Date(), + render: ( + + ), + }, + ], + }, + { + label: 'Recent Orders', + messages: [ + { + id: '3', + role: 'user' as const, + content: 'Show me the most recent orders', + createdAt: new Date(), + }, + { + id: '4', + role: 'assistant' as const, + content: 'Here are the 5 most recent orders:', + createdAt: new Date(), + render: ( + + ), + }, + ], + }, + { + label: 'Revenue by Category', + messages: [ + { + id: '5', + role: 'user' as const, + content: 'What are our top performing product categories?', + createdAt: new Date(), + }, + { + id: '6', + role: 'assistant' as const, + content: 'Here is the revenue breakdown by product category:', + createdAt: new Date(), + render: ( + + ), + }, + ], + }, + { + label: 'Active Users', + messages: [ + { + id: '7', + role: 'user' as const, + content: 'Show me our currently active users', + createdAt: new Date(), + }, + { + id: '8', + role: 'assistant' as const, + content: 'Here are the currently active users:', + createdAt: new Date(), + render: ( + now() - interval '24 hours' +ORDER BY last_login DESC;`} + mockData={{ + rows: [ + { + username: 'alice_smith', + last_login: '2024-01-15T15:30:00Z', + session_count: 45, + }, + { + username: 'bob_jones', + last_login: '2024-01-15T15:15:00Z', + session_count: 32, + }, + { + username: 'carol_white', + last_login: '2024-01-15T14:45:00Z', + session_count: 28, + }, + ], + }} + /> + ), + }, + ], + }, + + { + label: 'Daily Page Views', + messages: [ + { + id: '11', + role: 'user' as const, + content: 'Show me daily page views for the last 2 weeks', + createdAt: new Date(), + }, + { + id: '12', + role: 'assistant' as const, + content: 'Here are the daily page views:', + createdAt: new Date(), + render: ( + now() - interval '14 days' +GROUP BY day +ORDER BY day;`} + mockData={[ + { name: '2024-01-01', value: 15234 }, + { name: '2024-01-02', value: 14567 }, + { name: '2024-01-03', value: 16789 }, + { name: '2024-01-04', value: 15678 }, + { name: '2024-01-05', value: 17890 }, + { name: '2024-01-06', value: 13456 }, + { name: '2024-01-07', value: 12345 }, + { name: '2024-01-08', value: 16543 }, + { name: '2024-01-09', value: 18765 }, + { name: '2024-01-10', value: 19876 }, + { name: '2024-01-11', value: 20123 }, + { name: '2024-01-12', value: 21234 }, + { name: '2024-01-13', value: 19876 }, + { name: '2024-01-14', value: 18765 }, + ]} + /> + ), + }, + ], + }, + { + label: 'Error Distribution', + messages: [ + { + id: '1', + role: 'user' as const, + content: 'Show me the distribution of error types', + createdAt: new Date(), + }, + { + id: '15', + role: 'assistant' as const, + content: 'Here is the distribution of error types:', + createdAt: new Date(), + render: ( + + ), + }, + ], + }, + { + label: 'API Usage Trends', + messages: [ + { + id: '17', + role: 'user' as const, + content: 'Show me API usage trends over time', + createdAt: new Date(), + }, + { + id: '18', + role: 'assistant' as const, + content: 'Here are the API usage trends:', + createdAt: new Date(), + render: ( + now() - interval '24 hours' +GROUP BY hour +ORDER BY hour;`} + mockData={[ + { name: '00:00', value: 1200 }, + { name: '02:00', value: 800 }, + { name: '04:00', value: 600 }, + { name: '06:00', value: 900 }, + { name: '08:00', value: 2500 }, + { name: '10:00', value: 3500 }, + { name: '12:00', value: 4000 }, + { name: '14:00', value: 3800 }, + { name: '16:00', value: 3200 }, + { name: '18:00', value: 2800 }, + { name: '20:00', value: 2000 }, + { name: '22:00', value: 1500 }, + ]} + /> + ), + }, + ], + }, +] + +function Assistant() { + const [incomingMessages, setIncomingMessages] = useState([]) + const isLoggedIn = useIsLoggedIn() + const isUserLoading = useIsUserLoading() + const meta_title = 'AI | Supabase' + const meta_description = 'Build AI-powered applications with Supabase' + const router = useRouter() + const { query } = router + + useEffect(() => { + if (incomingMessages.length === 0) { + const timeoutId = setTimeout(() => { + const isFromProductHunt = query.ref === 'producthunt' + const initialMessages = isFromProductHunt + ? [...welcomeMessages, ...demoQueries[0].messages] + : demoQueries[Math.floor(Math.random() * demoQueries.length)].messages + + setIncomingMessages(initialMessages) + }, 3000) + return () => clearTimeout(timeoutId) + } + }, [query.ref]) + + const handleNewMessage = (messages: Message[]) => { + setIncomingMessages(messages.map((message) => ({ ...message, id: Date.now().toString() }))) + } + + return ( + <> + + + + + + +
+ {/* Left Column */} +
+ {/* Main content */} +
+

Chat with Postgres

+

+ Generate, run and debug queries, chart your data, create functions, policies and + more. The assistant is here to help. +

+
+ {!isUserLoading && + (isLoggedIn ? ( + + ) : ( + + ))} +
+
+ + {/* Grid of secondary buttons */} +
+

Try me

+
+ + {demoQueries.map((query, i) => ( + + + + ))} + +
+
+
+ + {/* Right Column */} + + + + + + + + + +
+
+
+ + ) +} + +export default Assistant diff --git a/apps/www/public/images/ai/error.gif b/apps/www/public/images/ai/error.gif new file mode 100644 index 0000000000000..37b9e76b26087 Binary files /dev/null and b/apps/www/public/images/ai/error.gif differ diff --git a/apps/www/public/images/blog/2024-12-01-orioledb-release/orioledb-project-create.png b/apps/www/public/images/blog/2024-12-01-orioledb-release/orioledb-project-create.png new file mode 100644 index 0000000000000..98cdfe1366a40 Binary files /dev/null and b/apps/www/public/images/blog/2024-12-01-orioledb-release/orioledb-project-create.png differ diff --git a/apps/www/public/images/blog/2024-12-01-orioledb-release/thumb.png b/apps/www/public/images/blog/2024-12-01-orioledb-release/thumb.png new file mode 100644 index 0000000000000..8693d18b5017c Binary files /dev/null and b/apps/www/public/images/blog/2024-12-01-orioledb-release/thumb.png differ diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml index 87ef290ce6772..68cb3d5cc7e83 100644 --- a/apps/www/public/rss.xml +++ b/apps/www/public/rss.xml @@ -5,9 +5,16 @@ https://supabase.com Latest news from Supabase en - Wed, 13 Nov 2024 00:00:00 -0700 + Fri, 29 Nov 2024 00:00:00 -0700 + https://supabase.com/blog/orioledb-launch + OrioleDB Public Alpha + https://supabase.com/blog/orioledb-launch + Launching OrioleDB Public Alpha + Fri, 29 Nov 2024 00:00:00 -0700 + + https://supabase.com/blog/supabase-dynamic-functions Executing Dynamic JavaScript Code on Supabase with Edge Functions https://supabase.com/blog/supabase-dynamic-functions diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 21da9cb3c5971..803c5af8f3427 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -185,7 +185,7 @@ services: realtime: # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain container_name: realtime-dev.supabase-realtime - image: supabase/realtime:v2.30.34 + image: supabase/realtime:v2.33.58 depends_on: db: # Disable this if you are using an external Postgres database @@ -306,7 +306,7 @@ services: functions: container_name: supabase-edge-functions - image: supabase/edge-runtime:v1.62.2 + image: supabase/edge-runtime:v1.65.3 restart: unless-stopped depends_on: analytics: diff --git a/examples/ai/aws_bedrock_image_gen/supabase/config.toml b/examples/ai/aws_bedrock_image_gen/supabase/config.toml index a32c2d06a2160..de3c5ea0f71a7 100644 --- a/examples/ai/aws_bedrock_image_gen/supabase/config.toml +++ b/examples/ai/aws_bedrock_image_gen/supabase/config.toml @@ -3,169 +3,18 @@ project_id = "aws_bedrock_image_gen" [api] -enabled = true -# Port to use for the API URL. -port = 54321 -# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API -# endpoints. `public` is always included. -schemas = ["public", "graphql_public"] -# Extra schemas to add to the search_path of every request. `public` is always included. -extra_search_path = ["public", "extensions"] -# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size -# for accidental or malicious requests. -max_rows = 1000 - -[db] -# Port to use for the local database URL. -port = 54322 -# Port used by db diff command to initialize the shadow database. -shadow_port = 54320 -# The database major version to use. This has to be the same as your remote database's. Run `SHOW -# server_version;` on the remote database to check. -major_version = 15 - -[db.pooler] +# Disable data API since we are not using the PostgREST client in this example. enabled = false -# Port to use for the local connection pooler. -port = 54329 -# Specifies when a server connection can be reused by other clients. -# Configure one of the supported pooler modes: `transaction`, `session`. -pool_mode = "transaction" -# How many server connections to allow per user/database pair. -default_pool_size = 20 -# Maximum number of client connections allowed. -max_client_conn = 100 - -[realtime] -enabled = true -# Bind realtime via either IPv4 or IPv6. (default: IPv4) -# ip_version = "IPv6" -# The maximum length in bytes of HTTP request headers. (default: 4096) -# max_header_length = 4096 - -[studio] -enabled = true -# Port to use for Supabase Studio. -port = 54323 -# External URL of the API server that frontend connects to. -api_url = "http://127.0.0.1" -# OpenAI API Key to use for Supabase AI in the Supabase Studio. -openai_api_key = "env(OPENAI_API_KEY)" - -# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they -# are monitored, and you can view the emails that would have been sent from the web interface. -[inbucket] -enabled = true -# Port to use for the email testing server web interface. -port = 54324 -# Uncomment to expose additional ports for testing user applications that send emails. -# smtp_port = 54325 -# pop3_port = 54326 [storage] enabled = true -# The maximum file size allowed (e.g. "5MB", "500KB"). +# The maximum file size allowed for all buckets in the project. file_size_limit = "50MiB" -[storage.image_transformation] +[functions.image_gen] enabled = true - -[auth] -enabled = true -# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used -# in emails. -site_url = "http://127.0.0.1:3000" -# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000"] -# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). -jwt_expiry = 3600 -# If disabled, the refresh token will never expire. -enable_refresh_token_rotation = true -# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. -# Requires enable_refresh_token_rotation = true. -refresh_token_reuse_interval = 10 -# Allow/disallow new user signups to your project. -enable_signup = true -# Allow/disallow anonymous sign-ins to your project. -enable_anonymous_sign_ins = false -# Allow/disallow testing manual linking of accounts -enable_manual_linking = false - -[auth.email] -# Allow/disallow new user signups via email to your project. -enable_signup = true -# If enabled, a user will be required to confirm any email change on both the old, and new email -# addresses. If disabled, only the new email is required to confirm. -double_confirm_changes = true -# If enabled, users need to confirm their email address before signing in. -enable_confirmations = false -# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. -max_frequency = "1s" - -# Uncomment to customize email template -# [auth.email.template.invite] -# subject = "You have been invited" -# content_path = "./supabase/templates/invite.html" - -[auth.sms] -# Allow/disallow new user signups via SMS to your project. -enable_signup = true -# If enabled, users need to confirm their phone number before signing in. -enable_confirmations = false -# Template for sending OTP to users -template = "Your code is {{ .Code }} ." -# Controls the minimum amount of time that must pass before sending another sms otp. -max_frequency = "5s" - -# Use pre-defined map of phone number to OTP for testing. -# [auth.sms.test_otp] -# 4152127777 = "123456" - -# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. -# [auth.hook.custom_access_token] -# enabled = true -# uri = "pg-functions:////" - -# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. -[auth.sms.twilio] -enabled = false -account_sid = "" -message_service_sid = "" -# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: -auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" - -# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, -# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, -# `twitter`, `slack`, `spotify`, `workos`, `zoom`. -[auth.external.apple] -enabled = false -client_id = "" -# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: -secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" -# Overrides the default auth redirectUrl. -redirect_uri = "" -# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, -# or any other third-party OIDC providers. -url = "" -# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. -skip_nonce_check = false - -[analytics] -enabled = false -port = 54327 -vector_port = 54328 -# Configure one of the supported backends: `postgres`, `bigquery`. -backend = "postgres" - -# Experimental features may be deprecated any time -[experimental] -# Configures Postgres storage engine to use OrioleDB (S3) -orioledb_version = "" -# Configures S3 bucket URL, eg. .s3-.amazonaws.com -s3_host = "env(S3_HOST)" -# Configures S3 bucket region, eg. us-east-1 -s3_region = "env(S3_REGION)" -# Configures AWS_ACCESS_KEY_ID for S3 bucket -s3_access_key = "env(S3_ACCESS_KEY)" -# Configures AWS_SECRET_ACCESS_KEY for S3 bucket -s3_secret_key = "env(S3_SECRET_KEY)" +verify_jwt = true +# import_map = "./functions/image_gen/deno.json" +# Uncomment to specify a custom file path to the entrypoint. +# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx +# entrypoint = "./functions/image_gen/index.ts" diff --git a/examples/ai/aws_bedrock_image_gen/supabase/migrations/20240523074718_storage_bucket.sql b/examples/ai/aws_bedrock_image_gen/supabase/migrations/20240523074718_storage_bucket.sql index 0c756ff94cd30..f4868df710be3 100644 --- a/examples/ai/aws_bedrock_image_gen/supabase/migrations/20240523074718_storage_bucket.sql +++ b/examples/ai/aws_bedrock_image_gen/supabase/migrations/20240523074718_storage_bucket.sql @@ -12,4 +12,4 @@ insert into "storage"."buckets" "owner_id" ) values - ('images', 'images', null, now(), now(), true, false, null, null, null); + ('images', 'images', null, now(), now(), true, false, null, '{image/png}', null); diff --git a/examples/ai/aws_bedrock_image_gen/supabase/seed.sql b/examples/ai/aws_bedrock_image_gen/supabase/seed.sql deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/examples/ai/aws_bedrock_image_search/supabase/config.toml b/examples/ai/aws_bedrock_image_search/supabase/config.toml index 2af8d3e181377..2571a2f783d0e 100644 --- a/examples/ai/aws_bedrock_image_search/supabase/config.toml +++ b/examples/ai/aws_bedrock_image_search/supabase/config.toml @@ -3,17 +3,8 @@ project_id = "aws_bedrock_image_search" [api] -enabled = true -# Port to use for the API URL. -port = 54321 -# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API -# endpoints. public and storage are always included. -schemas = ["public", "storage", "graphql_public"] -# Extra schemas to add to the search_path of every request. public is always included. -extra_search_path = ["public", "extensions"] -# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size -# for accidental or malicious requests. -max_rows = 1000 +# Disable data API since we are not using the PostgREST client in this example. +enabled = false [db] # Port to use for the local database URL. @@ -35,127 +26,3 @@ pool_mode = "transaction" default_pool_size = 20 # Maximum number of client connections allowed. max_client_conn = 100 - -[realtime] -enabled = true -# Bind realtime via either IPv4 or IPv6. (default: IPv6) -# ip_version = "IPv6" -# The maximum length in bytes of HTTP request headers. (default: 4096) -# max_header_length = 4096 - -[studio] -enabled = true -# Port to use for Supabase Studio. -port = 54323 -# External URL of the API server that frontend connects to. -api_url = "http://127.0.0.1" -# OpenAI API Key to use for Supabase AI in the Supabase Studio. -openai_api_key = "env(OPENAI_API_KEY)" - -# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they -# are monitored, and you can view the emails that would have been sent from the web interface. -[inbucket] -enabled = true -# Port to use for the email testing server web interface. -port = 54324 -# Uncomment to expose additional ports for testing user applications that send emails. -# smtp_port = 54325 -# pop3_port = 54326 - -[storage] -enabled = true -# The maximum file size allowed (e.g. "5MB", "500KB"). -file_size_limit = "50MiB" - -[auth] -enabled = true -# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used -# in emails. -site_url = "http://127.0.0.1:3000" -# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000"] -# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). -jwt_expiry = 3600 -# If disabled, the refresh token will never expire. -enable_refresh_token_rotation = true -# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. -# Requires enable_refresh_token_rotation = true. -refresh_token_reuse_interval = 10 -# Allow/disallow new user signups to your project. -enable_signup = true -# Allow/disallow testing manual linking of accounts -enable_manual_linking = false - -[auth.email] -# Allow/disallow new user signups via email to your project. -enable_signup = true -# If enabled, a user will be required to confirm any email change on both the old, and new email -# addresses. If disabled, only the new email is required to confirm. -double_confirm_changes = true -# If enabled, users need to confirm their email address before signing in. -enable_confirmations = false - -# Uncomment to customize email template -# [auth.email.template.invite] -# subject = "You have been invited" -# content_path = "./supabase/templates/invite.html" - -[auth.sms] -# Allow/disallow new user signups via SMS to your project. -enable_signup = true -# If enabled, users need to confirm their phone number before signing in. -enable_confirmations = false -# Template for sending OTP to users -template = "Your code is {{ .Code }} ." - -# Use pre-defined map of phone number to OTP for testing. -[auth.sms.test_otp] -# 4152127777 = "123456" - -# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. -[auth.hook.custom_access_token] -# enabled = true -# uri = "pg-functions:////" - - -# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. -[auth.sms.twilio] -enabled = false -account_sid = "" -message_service_sid = "" -# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: -auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" - -# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, -# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, -# `twitter`, `slack`, `spotify`, `workos`, `zoom`. -[auth.external.apple] -enabled = false -client_id = "" -# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: -secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" -# Overrides the default auth redirectUrl. -redirect_uri = "" -# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, -# or any other third-party OIDC providers. -url = "" - -[analytics] -enabled = false -port = 54327 -vector_port = 54328 -# Configure one of the supported backends: `postgres`, `bigquery`. -backend = "postgres" - -# Experimental features may be deprecated any time -[experimental] -# Configures Postgres storage engine to use OrioleDB (S3) -orioledb_version = "" -# Configures S3 bucket URL, eg. .s3-.amazonaws.com -s3_host = "env(S3_HOST)" -# Configures S3 bucket region, eg. us-east-1 -s3_region = "env(S3_REGION)" -# Configures AWS_ACCESS_KEY_ID for S3 bucket -s3_access_key = "env(S3_ACCESS_KEY)" -# Configures AWS_SECRET_ACCESS_KEY for S3 bucket -s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/examples/ai/aws_bedrock_image_search/supabase/seed.sql b/examples/ai/aws_bedrock_image_search/supabase/seed.sql deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/examples/storage/protomaps/supabase/config.toml b/examples/storage/protomaps/supabase/config.toml index c38bfcfcba2ce..b5611e5b58207 100644 --- a/examples/storage/protomaps/supabase/config.toml +++ b/examples/storage/protomaps/supabase/config.toml @@ -2,18 +2,14 @@ # working directory name when running `supabase init`. project_id = "protomaps" -# Disable data API since we are not using the PostgREST client in this example. [api] +# Disable data API since we are not using the PostgREST client in this example. enabled = false [storage] -enabled = true # The maximum file size allowed for all buckets in the project. file_size_limit = "50MiB" -[storage.image_transformation] -enabled = true - [storage.buckets.maps-private] public = false # file_size_limit = "50MiB" @@ -21,14 +17,6 @@ public = false # Uncomment to specify a local directory to upload objects to the bucket. # objects_path = "./buckets/maps-private" -[edge_runtime] -enabled = true -# Configure one of the supported request policies: `oneshot`, `per_worker`. -# Use `oneshot` for hot reload, or `per_worker` for load testing. -policy = "oneshot" -# Port to attach the Chrome inspector for debugging Edge Functions locally. -inspector_port = 8083 - [functions.maps-private] enabled = true verify_jwt = false diff --git a/examples/storage/resumable-upload-uppy/supabase/config.toml b/examples/storage/resumable-upload-uppy/supabase/config.toml index c103cd7b57ab5..78e915c30a546 100644 --- a/examples/storage/resumable-upload-uppy/supabase/config.toml +++ b/examples/storage/resumable-upload-uppy/supabase/config.toml @@ -2,12 +2,11 @@ # working directory name when running `supabase init`. project_id = "resumable-upload-uppy" -# Disable data API since we are not using the PostgREST client in this example. [api] +# Disable data API since we are not using the PostgREST client in this example. enabled = false [storage] -enabled = true # The maximum file size allowed for all buckets in the project. file_size_limit = "50MiB" diff --git a/package-lock.json b/package-lock.json index 0dfcff6546960..01f31d93ddb62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1881,6 +1881,7 @@ "tus-js-client": "^4.1.0", "ui": "*", "ui-patterns": "*", + "use-debounce": "^7.0.1", "uuid": "^9.0.1", "valtio": "^1.12.0", "vite-tsconfig-paths": "^4.3.2", diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 081229cb3ecda..1e34bfb048ad9 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -317,6 +317,8 @@ export interface paths { post: operations['OrganizationsController_createOrganizationWithTier'] } '/platform/organizations/{slug}': { + /** Gets a specific organization that belongs to the authenticated user */ + get: operations['OrganizationSlugController_getOrganization'] /** Deletes organization */ delete: operations['OrganizationSlugController_deleteOrganization'] /** Updates organization */ @@ -1195,9 +1197,13 @@ export interface paths { /** Updates the database password */ patch: operations['DatabasePasswordController_updatePassword'] } + '/system/email/send': { + /** Send email using Postmark template */ + post: operations['SystemEmailController_sendEmail'] + } '/system/github-secret-alert': { - /** Reset JWT if leaked keys found by GitHub secret scanning */ - post: operations['GithubSecretAlertController_resetJwt'] + /** Respond to GitHub secret scanning webhook request */ + post: operations['GithubSecretAlertController_githubSecretScanningEndpoint'] } '/system/health': { /** Get API health status */ @@ -1499,6 +1505,8 @@ export interface paths { post: operations['OrganizationsController_createOrganizationWithTier'] } '/v0/organizations/{slug}': { + /** Gets a specific organization that belongs to the authenticated user */ + get: operations['OrganizationSlugController_getOrganization'] /** Deletes organization */ delete: operations['OrganizationSlugController_deleteOrganization'] /** Updates organization */ @@ -3597,6 +3605,7 @@ export interface components { query: string } FunctionResponse: { + compute_multiplier?: number /** Format: int64 */ created_at: number entrypoint_path?: string @@ -3604,7 +3613,6 @@ export interface components { import_map?: boolean import_map_path?: string name: string - resource_multiplier?: string slug: string /** @enum {string} */ status: 'ACTIVE' | 'REMOVED' | 'THROTTLED' @@ -3614,6 +3622,7 @@ export interface components { version: number } FunctionSlugResponse: { + compute_multiplier?: number /** Format: int64 */ created_at: number entrypoint_path?: string @@ -3621,7 +3630,6 @@ export interface components { import_map?: boolean import_map_path?: string name: string - resource_multiplier?: string slug: string /** @enum {string} */ status: 'ACTIVE' | 'REMOVED' | 'THROTTLED' @@ -4429,20 +4437,55 @@ export interface components { name: string project_ids: number[] | null } - OrganizationSlugAvailableVersionsBody: { + OrganizationSlugAvailableVersionsBodyDto: { provider: string region: string } OrganizationSlugAvailableVersionsResponse: { available_versions: components['schemas']['ProjectCreationVersionInfo'][] } + OrganizationSlugProject: { + cloud_provider: string + disk_volume_size_gb?: number + engine?: string + id: number + /** @enum {string} */ + infra_compute_size?: + | 'nano' + | 'micro' + | 'small' + | 'medium' + | 'large' + | 'xlarge' + | '2xlarge' + | '4xlarge' + | '8xlarge' + | '12xlarge' + | '16xlarge' + inserted_at: string | null + is_branch_enabled: boolean + is_physical_backups_enabled: boolean | null + name: string + organization_id: number + organization_slug: string + preview_branch_refs: string[] + ref: string + region: string + status: string + subscription_id: string | null + } OrganizationSlugResponse: { - billing_email?: string + billing_email: string | null + billing_metadata?: Record + has_oriole_project: boolean id: number name: string opt_in_tags: string[] + projects: components['schemas']['OrganizationSlugProject'][] + restriction_data: unknown + /** @enum {string|null} */ + restriction_status: 'grace_period' | 'grace_period_over' | 'restricted' | null slug: string - stripe_customer_id?: string } OrgDocumentUrlResponse: { fileUrl: string @@ -4640,7 +4683,7 @@ export interface components { work_mem?: string } /** @enum {string} */ - PostgresEngine: '15' + PostgresEngine: '15' | '17-oriole' PostgresExtension: { comment: string | null default_version: string @@ -5199,7 +5242,7 @@ export interface components { target_table_schema: string } /** @enum {string} */ - ReleaseChannel: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' + ReleaseChannel: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview' RemoveNetworkBanRequest: { ipv4_addresses: string[] } @@ -5442,6 +5485,16 @@ export interface components { team?: string title: string } + SendEmailBodyDto: { + addresses: string[] + custom_properties: { + [key: string]: unknown + } + template_alias: string + } + SendEmailResponseBodyDto: { + message: string + } SendExitSurveyBody: { additionalFeedback?: string exitAction?: string @@ -5730,7 +5783,7 @@ export interface components { db_pass: string db_pass_supabase: string jwt_secret: string - /** @description Name of your project, should not contain dots */ + /** @description Name of your project */ name: string /** @description Slug of your organization */ organization_id: string @@ -6416,10 +6469,19 @@ export interface components { UpdateNotificationsBodyV1: { ids: string[] } - UpdateOrganizationBody: { - billing_email: string + UpdateOrganizationBodyDto: { + /** Format: email */ + billing_email?: string + name?: string + opt_in_tags: 'AI_SQL_GENERATOR_OPT_IN'[] + } + UpdateOrganizationResponse: { + billing_email?: string + id: number name: string opt_in_tags: string[] + slug: string + stripe_customer_id?: string } UpdatePasswordBody: { password: string @@ -6813,8 +6875,8 @@ export interface components { } V1CreateFunctionBody: { body: string + compute_multiplier?: number name: string - resource_multiplier?: string slug: string verify_jwt?: boolean } @@ -6838,7 +6900,7 @@ export interface components { * @description This field is deprecated and is ignored in this request */ kps_enabled?: boolean - /** @description Name of your project, should not contain dots */ + /** @description Name of your project */ name: string /** @description Slug of your organization */ organization_id: string @@ -6852,7 +6914,7 @@ export interface components { * @description Postgres engine version. If not provided, the latest version will be used. * @enum {string} */ - postgres_engine?: '15' + postgres_engine?: '15' | '17-oriole' /** * @description Region you want your server to reside in * @enum {string} @@ -6880,7 +6942,7 @@ export interface components { * @description Release channel. If not provided, GA will be used. * @enum {string} */ - release_channel?: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' + release_channel?: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview' /** * Format: uri * @description Template URL used to create the project from the CLI. @@ -7038,8 +7100,8 @@ export interface components { } V1UpdateFunctionBody: { body?: string + compute_multiplier?: number name?: string - resource_multiplier?: string verify_jwt?: boolean } ValidateQueryBody: { @@ -7048,7 +7110,7 @@ export interface components { ValidateQueryResponse: { valid: boolean } - ValidateSpamBody: { + ValidateSpamBodyDto: { content: string subject: string } @@ -7773,7 +7835,7 @@ export interface operations { ValidateController_validateSpam: { requestBody: { content: { - 'application/json': components['schemas']['ValidateSpamBody'] + 'application/json': components['schemas']['ValidateSpamBodyDto'] } } responses: { @@ -8663,11 +8725,25 @@ export interface operations { } } } + /** Gets a specific organization that belongs to the authenticated user */ + OrganizationSlugController_getOrganization: { + parameters: { + path: { + slug: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['OrganizationSlugResponse'] + } + } + } + } /** Deletes organization */ OrganizationSlugController_deleteOrganization: { parameters: { path: { - /** @description Organization slug */ slug: string } } @@ -8688,19 +8764,18 @@ export interface operations { OrganizationSlugController_updateOrganization: { parameters: { path: { - /** @description Organization slug */ slug: string } } requestBody: { content: { - 'application/json': components['schemas']['UpdateOrganizationBody'] + 'application/json': components['schemas']['UpdateOrganizationBodyDto'] } } responses: { 200: { content: { - 'application/json': components['schemas']['OrganizationSlugResponse'] + 'application/json': components['schemas']['UpdateOrganizationResponse'] } } /** @description Failed to update organization */ @@ -8742,13 +8817,12 @@ export interface operations { OrganizationSlugController_getAvailableImageVersions: { parameters: { path: { - /** @description Organization slug */ slug: string } } requestBody: { content: { - 'application/json': components['schemas']['OrganizationSlugAvailableVersionsBody'] + 'application/json': components['schemas']['OrganizationSlugAvailableVersionsBodyDto'] } } responses: { @@ -14771,25 +14845,26 @@ export interface operations { } } } - /** Reset JWT if leaked keys found by GitHub secret scanning */ - GithubSecretAlertController_resetJwt: { - parameters: { - header: { - 'github-public-key-identifier': string - 'github-public-key-signature': string - } - } + /** Send email using Postmark template */ + SystemEmailController_sendEmail: { requestBody: { content: { - 'application/json': string + 'application/json': components['schemas']['SendEmailBodyDto'] } } responses: { - 200: { - content: never + /** @description Email queued successfully */ + 201: { + content: { + 'application/json': components['schemas']['SendEmailResponseBodyDto'] + } } - /** @description Failed to reset JWT */ - 500: { + } + } + /** Respond to GitHub secret scanning webhook request */ + GithubSecretAlertController_githubSecretScanningEndpoint: { + responses: { + 200: { content: never } } @@ -15383,7 +15458,7 @@ export interface operations { import_map?: boolean entrypoint_path?: string import_map_path?: string - resource_multiplier?: string + compute_multiplier?: number } path: { /** @description Project ref */ @@ -15464,7 +15539,7 @@ export interface operations { import_map?: boolean entrypoint_path?: string import_map_path?: string - resource_multiplier?: string + compute_multiplier?: number } path: { /** @description Project ref */ @@ -17400,7 +17475,7 @@ export interface operations { 'v1-get-a-snippet': { parameters: { path: { - id: Record + id: string } } responses: {