diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index aeef0676125..96ad2af0b61 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -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 00000000000..ba8e3279502 --- /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 00000000000..e033f70d77f 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 00000000000..93aef47733f 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 00000000000..99a7f444c26 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 e07ad7fb852..45cda3234db 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 32dbf6b4196..16353b0abae 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/App/FeaturePreview/AssistantV2Preview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/AssistantV2Preview.tsx deleted file mode 100644 index a10d9531f87..00000000000 --- a/apps/studio/components/interfaces/App/FeaturePreview/AssistantV2Preview.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import Image from 'next/image' - -import { useParams } from 'common' -import { Markdown } from 'components/interfaces/Markdown' -import { BASE_PATH } from 'lib/constants' -import { detectOS } from 'lib/helpers' - -export const AssistantV2Preview = () => { - const os = detectOS() - const { ref } = useParams() - - return ( -
-

- We're changing the way our AI Assistant integrates with the dashboard by making it shared - and accessible universally across the whole dashboard. This hopes to make using the - Assistant as a supporting tool more seamless while you build your project. -

- api-docs-side-panel-preview -

- The Assistant will also be automatically provided with contexts depending on where you are - in the dashboard to generate more relevant and higher quality outputs. You may also ask for - insights on your own data apart from help with SQL and Postgres! -

-

- We believe that this should further lower the barrier of working with databases especially - if you're not well acquainted with Postgres (yet!), so please do feel free to let us know - what you think in the attached GitHub discussion above! -

-
-

Enabling this preview will:

-
    -
  • - -
  • -
  • - -
  • -
-
-
- ) -} diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx index 03ef6fa224f..2d897e8b14e 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx @@ -5,16 +5,8 @@ import { LOCAL_STORAGE_KEYS } from 'lib/constants' import { EMPTY_OBJ } from 'lib/void' import { APISidePanelPreview } from './APISidePanelPreview' import { CLSPreview } from './CLSPreview' -import { AssistantV2Preview } from './AssistantV2Preview' export const FEATURE_PREVIEWS = [ - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_ASSISTANT_V2, - name: 'Supabase AI Assistant V2', - content: , - discussionsUrl: undefined, - isNew: true, - }, { key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL, name: 'Project API documentation', @@ -85,8 +77,3 @@ export const useIsColumnLevelPrivilegesEnabled = () => { const { flags } = useFeaturePreviewContext() return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS] } - -export const useIsAssistantV2Enabled = () => { - const { flags } = useFeaturePreviewContext() - return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ASSISTANT_V2] -} diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 01c76d67997..c04cdd98f66 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useFlag } from 'hooks/ui/useFlag' -import { LOCAL_STORAGE_KEYS } from 'lib/constants' import { TELEMETRY_EVENTS } from 'lib/constants/telemetry' import { useAppStateSnapshot } from 'state/app-state' import { Badge, Button, Modal, ScrollArea, cn } from 'ui' @@ -42,13 +41,6 @@ const FeaturePreviewModal = () => { label: selectedFeatureKey, value: isSelectedFeatureEnabled ? 'disabled' : 'enabled', }) - - if ( - selectedFeatureKey === LOCAL_STORAGE_KEYS.UI_PREVIEW_ASSISTANT_V2 && - isSelectedFeatureEnabled - ) { - snap.setAiAssistantPanel({ open: false }) - } } function handleCloseFeaturePreviewModal() { diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/QueryError.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/QueryError.tsx index 8faa934e563..0ac3fa00896 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/QueryError.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/QueryError.tsx @@ -72,7 +72,8 @@ const QueryError = ({ {open ? 'Hide error details' : 'Show error details'} - {!hasHipaaAddon && ( + {/* [Joshen] Temp hidden as new assistant doesnt support this. Current assistant's UX is not great too tbh so okay to hide this */} + {/* {!hasHipaaAddon && ( - )} + )} */} {formattedError.length > 0 ? ( diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx index f0c174e11e7..22c4bb7e252 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx @@ -1,13 +1,14 @@ import type { PostgresPolicy } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { noop } from 'lodash' +import { Edit, MoreVertical, Trash } from 'lucide-react' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' import Panel from 'components/ui/Panel' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { Edit, MoreVertical, Trash } from 'lucide-react' +import { useAppStateSnapshot } from 'state/app-state' import { Badge, Button, @@ -21,8 +22,6 @@ import { TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, } from 'ui' -import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import { useAppStateSnapshot } from 'state/app-state' import { generatePolicyCreateSQL } from './PolicyTableRow.utils' interface PolicyRowProps { @@ -39,7 +38,6 @@ const PolicyRow = ({ onSelectDeletePolicy = noop, }: PolicyRowProps) => { const { setAiAssistantPanel } = useAppStateSnapshot() - const isAssistantV2Enabled = useIsAssistantV2Enabled() const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies') const { project } = useProjectContext() @@ -106,39 +104,33 @@ const PolicyRow = ({ diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx index 1beebad7e4d..8070ed705b9 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx @@ -1,7 +1,7 @@ import { toString as CronToString } from 'cronstrue' import { List } from 'lucide-react' import Link from 'next/link' -import { UIEvent, useCallback, useMemo } from 'react' +import { UIEvent, useCallback, useEffect, useMemo } from 'react' import DataGrid, { Column, Row } from 'react-data-grid' import { useParams } from 'common' @@ -146,6 +146,7 @@ export const PreviousRunsTab = () => { data, isLoading: isLoadingCronJobRuns, fetchNextPage, + refetch, isFetching, } = useCronJobRunsInfiniteQuery( { @@ -156,6 +157,15 @@ export const PreviousRunsTab = () => { { enabled: !!currentJobState?.jobid, staleTime: 30 } ) + useEffect(() => { + // Refetch only the first page + const timerId = setInterval(() => { + refetch({ refetchPage: (_page, index) => index === 0 }) + }, 30000) + + return () => clearInterval(timerId) + }, [refetch]) + const handleScroll = useCallback( (event: UIEvent) => { if (isLoadingCronJobRuns || !isAtBottom(event)) return diff --git a/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx b/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx index 0e21237fc78..ddb7cd0e690 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx @@ -1,10 +1,10 @@ +import { BadgeCheck } from 'lucide-react' import Link from 'next/link' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { Badge, cn, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui' +import { Badge, cn } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { IntegrationDefinition } from './Integrations.constants' -import { BadgeCheck, Maximize } from 'lucide-react' type IntegrationCardProps = IntegrationDefinition & { isInstalled?: boolean @@ -33,7 +33,7 @@ export const IntegrationLoadingCard = () => { 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 f016a84e281..185d58209a0 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 @@ -110,7 +110,7 @@ const supabaseIntegrations: IntegrationDefinition[] = [ }, }, { - id: 'cron-jobs', + id: 'cron', type: 'postgres_extension' as const, requiredExtensions: ['pg_cron'], name: `Cron`, @@ -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/SingleQueue/Queue.utils.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/Queue.utils.tsx index 3f72dc4ede4..2d236a9e7f0 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 fd475eb5acb..b4bf5834669 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 2a3674c7f93..150e7332875 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/ProjectCreation/AdvancedConfiguration.tsx b/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx index e5ea278d2e9..25dca6d4d07 100644 --- a/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/AdvancedConfiguration.tsx @@ -84,7 +84,7 @@ export const AdvancedConfiguration = ({ form }: AdvancedConfigurationProps) => { <> Postgres with OrioleDB - Preview + Alpha } diff --git a/apps/studio/components/interfaces/Realtime/Inspector/NoChannelEmptyState.tsx b/apps/studio/components/interfaces/Realtime/Inspector/NoChannelEmptyState.tsx index 25be0511733..342aca9b110 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/NoChannelEmptyState.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/NoChannelEmptyState.tsx @@ -1,11 +1,7 @@ import { DocsButton } from 'components/ui/DocsButton' -import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' import { cn } from 'ui' -import { Admonition } from 'ui-patterns' const NoChannelEmptyState = () => { - const isOrioleDb = useIsOrioleDb() - return (

@@ -17,28 +13,13 @@ const NoChannelEmptyState = () => {

-
+

Not sure what to do?

Browse our documentation

- {/* [Joshen] This is temporary as a bug was identified that might not be patched on time for release */} - {/* [Joshen] Just making a simple callout as this is intended to be patched soon either way, not a long term limitation */} - {isOrioleDb && ( - - )}
) } diff --git a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx index 8b2003a699a..86d7cabdca8 100644 --- a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx +++ b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx @@ -20,6 +20,7 @@ interface AiMessagePreProps { className?: string } +// [DEPRECATED] To delete export const AiMessagePre = ({ onDiff, children, className }: AiMessagePreProps) => { const [copied, setCopied] = useState(false) diff --git a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/Message.tsx b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/Message.tsx index e7bbd9dd31c..03c2156d57c 100644 --- a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/Message.tsx +++ b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/Message.tsx @@ -21,6 +21,7 @@ interface MessageProps { action?: React.ReactNode } +// [DEPRECATED] To delete const Message = memo(function Message({ name, role, diff --git a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx index d27eebd1d8f..0424b2d477b 100644 --- a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx +++ b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx @@ -51,6 +51,7 @@ interface AiAssistantPanelProps { onClose: () => void } +// [DEPRECATED] To delete export const AiAssistantPanel = ({ selectedMessage, existingSql, diff --git a/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx b/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx index 08c0dfd01ff..4b05881848d 100644 --- a/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx @@ -12,7 +12,6 @@ import { useAppStateSnapshot } from 'state/app-state' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { cn } from 'ui' import { Admonition } from 'ui-patterns' -import { useIsAssistantV2Enabled } from '../App/FeaturePreview/FeaturePreviewContext' import { untitledSnippetTitle } from './SQLEditor.constants' import type { IStandaloneCodeEditor } from './SQLEditor.types' import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils' @@ -42,7 +41,6 @@ const MonacoEditor = ({ const project = useSelectedProject() const snapV2 = useSqlEditorV2StateSnapshot() - const isAssistantV2Enabled = useIsAssistantV2Enabled() const { setAiAssistantPanel } = useAppStateSnapshot() const [intellisenseEnabled] = useLocalStorageQuery( @@ -77,24 +75,22 @@ const MonacoEditor = ({ }, }) - if (isAssistantV2Enabled) { - editor.addAction({ - id: 'explain-code', - label: 'Explain Code', - contextMenuGroupId: 'operation', - contextMenuOrder: 1, - run: () => { - const selectedValue = (editorRef?.current as any) - .getModel() - .getValueInRange((editorRef?.current as any)?.getSelection()) - setAiAssistantPanel({ - open: true, - sqlSnippets: [selectedValue], - initialInput: 'Can you explain this section to me in more detail?', - }) - }, - }) - } + editor.addAction({ + id: 'explain-code', + label: 'Explain Code', + contextMenuGroupId: 'operation', + contextMenuOrder: 1, + run: () => { + const selectedValue = (editorRef?.current as any) + .getModel() + .getValueInRange((editorRef?.current as any)?.getSelection()) + setAiAssistantPanel({ + open: true, + sqlSnippets: [selectedValue], + initialInput: 'Can you explain this section to me in more detail?', + }) + }, + }) editor.onDidChangeCursorSelection(({ selection }) => { const noSelection = diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index a958b2ffcb8..620c265b844 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -6,7 +6,6 @@ import dynamic from 'next/dynamic' import { useRouter } from 'next/router' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' -import { format } from 'sql-formatter' import { Separator } from '@ui/components/SidePanel/SidePanel' import { useParams } from 'common' @@ -35,14 +34,12 @@ import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { isRoleImpersonationEnabled, useGetImpersonatedRole } from 'state/role-impersonation-state' import { getSqlEditorV2StateSnapshot, useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { - AiIconAnimation, Button, DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, - ImperativePanelHandle, ResizableHandle, ResizablePanel, ResizablePanelGroup, @@ -52,9 +49,7 @@ import { cn, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import { useIsAssistantV2Enabled } from '../App/FeaturePreview/FeaturePreviewContext' import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils' -import { AiAssistantPanel } from './AiAssistantPanel' import AISchemaSuggestionPopover from './AISchemaSuggestionPopover' import { DiffActionBar } from './DiffActionBar' import { @@ -106,7 +101,6 @@ const SQLEditor = () => { const databaseSelectorState = useDatabaseSelectorStateSnapshot() const queryClient = useQueryClient() - const isAssistantV2Enabled = useIsAssistantV2Enabled() const { open } = appSnap.aiAssistantPanel const { mutate: formatQuery } = useFormatQueryMutation() @@ -121,26 +115,20 @@ const SQLEditor = () => { const editorRef = useRef(null) const monacoRef = useRef(null) const diffEditorRef = useRef(null) - const aiPanelRef = useRef(null) const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const { data: databases, isSuccess: isSuccessReadReplicas } = useReadReplicasQuery({ projectRef: ref, }) - // Customers on HIPAA plans should not have access to Supabase AI - const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) - - // [Joshen] This eventually needs to be in the app state - const [isAiOpen, setIsAiOpen] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_OPEN, true) - const isAssistantOpen = (!isAssistantV2Enabled && isAiOpen) || (isAssistantV2Enabled && open) - const [showPotentialIssuesModal, setShowPotentialIssuesModal] = useState(false) const [queryHasDestructiveOperations, setQueryHasDestructiveOperations] = useState(false) const [queryHasUpdateWithoutWhere, setQueryHasUpdateWithoutWhere] = useState(false) const isOptedInToAI = useOrgOptedIntoAi() const [selectedSchemas] = useSchemasForAi(project?.ref!) + // Customers on HIPAA plans should not have access to Supabase AI + const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) const includeSchemaMetadata = isOptedInToAI || !IS_PLATFORM const [isAcceptDiffLoading, setIsAcceptDiffLoading] = useState(false) @@ -411,33 +399,11 @@ const SQLEditor = () => { try { const snippet = snapV2.snippets[id] const result = snapV2.results[id]?.[0] - if (isAssistantV2Enabled) { - appSnap.setAiAssistantPanel({ - open: true, - sqlSnippets: [snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim()], - initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${result.error.message}`, - }) - } else { - const { solution, sql } = await debugSql({ - sql: snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim(), - errorMessage: result.error.message, - entityDefinitions, - }) - - const formattedSql = - sqlAiDisclaimerComment + - '\n\n' + - format(sql, { - language: 'postgresql', - keywordCase: 'lower', - }) - setDebugSolution(solution) - setSourceSqlDiff({ - original: snippet.snippet.content.sql, - modified: formattedSql, - }) - setSelectedDiffType(DiffType.Modification) - } + appSnap.setAiAssistantPanel({ + open: true, + sqlSnippets: [snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim()], + initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${result.error.message}`, + }) } catch (error: unknown) { // [Joshen] There's a tendency for the SQL debug to chuck a lengthy error message // that's not relevant for the user - so we prettify it here by avoiding to return the @@ -693,7 +659,7 @@ const SQLEditor = () => { direction="vertical" autoSaveId={LOCAL_STORAGE_KEYS.SQL_EDITOR_SPLIT_SIZE} > - {(isAssistantOpen || isDiffOpen) && !hasHipaaAddon && ( + {(open || isDiffOpen) && !hasHipaaAddon && ( { appSnap.setShowAiSettingsModal(true) @@ -729,41 +695,6 @@ const SQLEditor = () => { )}
- {!isAssistantV2Enabled && !isAiOpen && ( - { - if (isAssistantV2Enabled) { - const state = getSqlEditorV2StateSnapshot() - const snippet = state.snippets[id] - const editor = editorRef.current - const selection = editor?.getSelection() - const selectedValue = selection - ? editor?.getModel()?.getValueInRange(selection) - : undefined - const sql = snippet - ? (selectedValue || editorRef.current?.getValue()) ?? - snippet.snippet.content.sql - : selectedValue || editorRef.current?.getValue() - - appSnap.setAiAssistantPanel({ - open: true, - sqlSnippets: sql ? [sql] : [], - initialInput: sql - ? `Help me make a change to the attached sql snippet` - : '', - }) - } else { - aiPanelRef.current?.expand() - } - }} - > - - - )} - {isLoading ? (
@@ -895,28 +826,6 @@ const SQLEditor = () => { - - {!isAssistantV2Enabled && ( - <> - - setIsAiOpen(false)} - onExpand={() => setIsAiOpen(true)} - > - aiPanelRef.current?.collapse()} - /> - - - )} ) diff --git a/apps/studio/components/layouts/AppLayout/AssistantButton.tsx b/apps/studio/components/layouts/AppLayout/AssistantButton.tsx index c5711c9c02a..2854c85248c 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 ebb5ff7d5c8..b81f358e975 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 4c94e4c3074..35d73345044 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/LogsLayout/LogsSidebarMenuV2.tsx b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx index d24b38bfa3f..f1d5f67405e 100644 --- a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx +++ b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx @@ -165,7 +165,7 @@ export function LogsSidebarMenuV2() { items: [], }, { - name: 'Cron Jobs', + name: 'Cron', key: 'pg_cron', url: `/project/${ref}/logs/pgcron-logs`, items: [], diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsMenu.utils.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsMenu.utils.tsx index 85a9c5501b6..1811d28d2ad 100644 --- a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsMenu.utils.tsx +++ b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsMenu.utils.tsx @@ -44,9 +44,9 @@ export const generateProjectIntegrationsMenu = ( ...(!!cronUiEnabled ? [ { - name: 'Cron Jobs', + name: 'Cron', key: 'cron-jobs', - url: `/project/${ref}/integrations/cron-jobs`, + url: `/project/${ref}/integrations/cron`, items: [], }, ] diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index 13a481d1a35..66cc136d935 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -1,7 +1,8 @@ -import { useParams } from 'common' import Link from 'next/link' import { useMemo } from 'react' +import { useParams } from 'common' +import AssistantButton from 'components/layouts/AppLayout/AssistantButton' import BranchDropdown from 'components/layouts/AppLayout/BranchDropdown' import EnableBranchingButton from 'components/layouts/AppLayout/EnableBranchingButton/EnableBranchingButton' import OrganizationDropdown from 'components/layouts/AppLayout/OrganizationDropdown' @@ -17,14 +18,11 @@ import BreadcrumbsView from './BreadcrumbsView' import { FeedbackDropdown } from './FeedbackDropdown' import HelpPopover from './HelpPopover' import NotificationsPopoverV2 from './NotificationsPopoverV2/NotificationsPopover' -import AssistantButton from 'components/layouts/AppLayout/AssistantButton' -import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => { const { ref: projectRef } = useParams() const selectedProject = useSelectedProject() const selectedOrganization = useSelectedOrganization() - const isAssistantV2Enabled = useIsAssistantV2Enabled() const isBranchingEnabled = selectedProject?.is_branch_enabled === true const { data: subscription } = useOrgSubscriptionQuery({ @@ -47,83 +45,91 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = return (
-
- {/* Organization is selected */} - {projectRef && ( - <> - +
+
+ {/* Organization is selected */} + {projectRef && ( + <> + - {projectRef && ( + {projectRef && ( + <> + + + + + + + + + {exceedingLimits && ( +
+ + Exceeding usage limits + +
+ )} + + )} + + {selectedProject && ( + <> + + + + + + {isBranchingEnabled ? : } + + )} + + )} + + {/* Additional breadcrumbs are supplied */} + +
+
+
+ {customHeaderComponents && customHeaderComponents} + {IS_PLATFORM && ( <> - - - - - - - - - {exceedingLimits && ( -
- - Exceeding usage limits - -
- )} + + + )} - - {selectedProject && ( - <> - - - - - - {isBranchingEnabled ? : } - - )} - - )} - - {/* Additional breadcrumbs are supplied */} - -
-
- {customHeaderComponents && customHeaderComponents} - {IS_PLATFORM && ( - <> - - - - {isAssistantV2Enabled && !!projectRef && } - - )} +
+
+ {!!projectRef && ( +
+ +
+ )}
) } diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index 256621653f9..06089b1163e 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -1,4 +1,3 @@ -import { useParams } from 'common' import { Home, User } from 'icons' import { isUndefined } from 'lodash' import { Command, FileText, FlaskConical, Search, Settings } from 'lucide-react' @@ -7,10 +6,8 @@ import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' -import { - useIsAPIDocsSidePanelEnabled, - useIsAssistantV2Enabled, -} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { useParams } from 'common' +import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { useProjectLintsQuery } from 'data/lint/lint-query' import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' @@ -73,7 +70,6 @@ const NavigationBar = () => { const navLayoutV2 = useFlag('navigationLayoutV2') const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled() - const isFunctionsAssistantEnabled = useIsAssistantV2Enabled() const [userDropdownOpen, setUserDropdownOpenState] = useState(false) const [allowNavPanelToExpand] = useLocalStorageQuery( @@ -120,37 +116,7 @@ const NavigationBar = () => { ) } - const CommandButton = !isFunctionsAssistantEnabled ? ( - { - setCommandMenuOpen(true) - snap.setNavigationPanelOpen(false) - }} - type="text" - icon={} - rightText={ -
- {os === 'macos' || true ? ( // todo: issue with `os` and hydration fail - - ) : ( -

CTRL

- )} -

K

-
- } - > - Search -
- ) : ( + const CommandButton = ( window.removeEventListener('keydown', handler) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isAssistantV2Enabled, open]) + }, [open]) return ( 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 00000000000..abe0881783d --- /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 5bbc060d26e..d69c482fa7a 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 - } + 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', + // List of keywords that indicate write operations + const writeOperations = ['insert', 'update', 'delete', 'alter', 'drop', 'create', 'replace'] - // Function patterns - 'function', - 'procedure', - ] + // Words that may appear in column names etc + const allowedPatterns = ['created', 'inserted', 'updated', 'deleted', 'truncate'] - const allowedPatterns = ['created', 'inserted', 'updated', 'deleted'] - - // 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) ) - - if (isPartOfAllowedPattern) { - return false - } - - return normalizedQuery.includes(pattern) + return !isAllowed && normalizedQuery.includes(op) }) + if (hasWriteOperation) return false + + const hasUnknownFunction = containsUnknownFunction(normalizedQuery) + if (hasUnknownFunction) return false + + 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 e9bc5bdeb55..7762e28bd27 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 a4e3852e65d..bcd29442f04 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 9e162427f3b..ff9f68ca873 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/ProductMenu/ProductMenuItem.tsx b/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx index e05c15edc2f..e6d439aa03d 100644 --- a/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx +++ b/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx @@ -38,7 +38,7 @@ const ProductMenuItem = ({ > {name} {label !== undefined && ( - + {label} )} diff --git a/apps/studio/lib/constants/index.ts b/apps/studio/lib/constants/index.ts index bfca4e0614c..efffcd60df1 100644 --- a/apps/studio/lib/constants/index.ts +++ b/apps/studio/lib/constants/index.ts @@ -33,7 +33,6 @@ export const LOCAL_STORAGE_KEYS = { UI_PREVIEW_NAVIGATION_LAYOUT: 'supabase-ui-preview-nav-layout', UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel', UI_PREVIEW_CLS: 'supabase-ui-cls', - UI_PREVIEW_ASSISTANT_V2: 'supabase-ui-assistant-v2', UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown', SQL_SCRATCH_PAD_BANNER_ACKNOWLEDGED: 'supabase-sql-scratch-pad-banner-acknowledged', diff --git a/apps/studio/next.config.js b/apps/studio/next.config.js index 2b2f4e7a15b..2deaf3ead2e 100644 --- a/apps/studio/next.config.js +++ b/apps/studio/next.config.js @@ -429,6 +429,11 @@ const nextConfig = { source: '/project/:ref/settings/vault/keys', destination: '/project/:ref/integrations/vault/keys', }, + { + permanent: true, + source: '/project/:ref/integrations/cron-jobs', + destination: '/project/:ref/integrations/cron', + }, ...(process.env.NEXT_PUBLIC_BASE_PATH?.length ? [ { diff --git a/apps/studio/package.json b/apps/studio/package.json index 62e41e28347..0dd1d48a1e0 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 0e4f60b6af7..55517ba1c0b 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/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx index b6e38e3e40d..2db8fb416a5 100644 --- a/apps/studio/pages/project/[ref]/auth/policies.tsx +++ b/apps/studio/pages/project/[ref]/auth/policies.tsx @@ -4,7 +4,6 @@ import { partition } from 'lodash' import { Search } from 'lucide-react' import { useState } from 'react' -import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { AIPolicyEditorPanel } from 'components/interfaces/Auth/Policies/AIPolicyEditorPanel' import Policies from 'components/interfaces/Auth/Policies/Policies' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' @@ -20,7 +19,6 @@ import { useTablesQuery } from 'data/tables/tables-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useUrlState } from 'hooks/ui/useUrlState' import { PROTECTED_SCHEMAS } from 'lib/constants/schemas' -import { useAppStateSnapshot } from 'state/app-state' import type { NextPageWithLayout } from 'types' import { Input } from 'ui' @@ -66,8 +64,6 @@ const AuthPoliciesPage: NextPageWithLayout = () => { }>() const { schema = 'public', search: searchString = '' } = params const { project } = useProjectContext() - const isAssistantV2Enabled = useIsAssistantV2Enabled() - const { setAiAssistantPanel } = useAppStateSnapshot() const [selectedTable, setSelectedTable] = useState() const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false) diff --git a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx index 2444dfbb623..e6db1c7bde4 100644 --- a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -70,7 +70,7 @@ const PITR = () => { 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 2dd040bb59f..9fa1a73b3ea 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 @@ -121,7 +121,7 @@ const RestoreToNewProject = () => { diff --git a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx index dc5a35e608b..46f2c0d8761 100644 --- a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -42,7 +42,7 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => { diff --git a/apps/studio/pages/project/[ref]/index.tsx b/apps/studio/pages/project/[ref]/index.tsx index 99d07bda346..836b11b3f7a 100644 --- a/apps/studio/pages/project/[ref]/index.tsx +++ b/apps/studio/pages/project/[ref]/index.tsx @@ -62,7 +62,7 @@ const Home: NextPageWithLayout = () => { This project is using Postgres with OrioleDB which is currently in preview and not suitable for production workloads. View our{' '} - {/* [Joshen] Make this into a reusable component to use links inline */} + {/* [Refactor] Make this into a reusable component to use links inline */} \ 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 00000000000..b04fd1909cb --- /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 00000000000..e07df745d5b --- /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 00000000000..64ef0697bde --- /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 00000000000..b1f87ad216b --- /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 00000000000..c25c9b6f852 --- /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 00000000000..30adaf69f8c --- /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/components/svg-icons.tsx b/apps/www/components/svg-icons.tsx new file mode 100644 index 00000000000..4994593683b --- /dev/null +++ b/apps/www/components/svg-icons.tsx @@ -0,0 +1,88 @@ +export const JsIcon = (props: any) => ( + + + +) +export const FlutterIcon = (props: any) => ( + + + + + + + +) +export const SwiftIcon = (props: any) => ( + + + +) +export const PythonIcon = (props: any) => ( + + + + +) diff --git a/apps/www/data/features.tsx b/apps/www/data/features.tsx index 9b951aae35f..7aac3f8e207 100644 --- a/apps/www/data/features.tsx +++ b/apps/www/data/features.tsx @@ -1,44 +1,56 @@ +import { FunctionComponent } from 'react' import { - ChartScatter, - Database, - FileCode2, - Lock, - Cloud, - UploadCloud, - Image, - MessageCircle, - ShieldCheck, - Package, - Users, - Folders, - RectangleEllipsis, - Braces, - Globe, - UserX, - GitBranch, - DatabaseZap, - Mail, - Smartphone, - Server, Activity, BarChart, + Braces, Brain, - Puzzle, + ChartScatter, + Cloud, + Database, DatabaseBackup, + DatabaseZap, + Eye, + FileCode2, + Folders, + GitBranch, + Globe, + Image, + Lock, + Mail, + MessageCircle, + Package, + Puzzle, + RectangleEllipsis, + Server, + Shield, + ShieldCheck, ShieldPlus, + Smartphone, Terminal, + UploadCloud, + Users, + UserX, Zap, } from 'lucide-react' +import { FlutterIcon, JsIcon, PythonIcon, SwiftIcon } from '~/components/svg-icons' import { PRODUCT, PRODUCT_SHORTNAMES } from 'shared-data/products' import type { LucideIcon } from 'lucide-react' enum ADDITIONAL_PRODUCTS { - PLATFORM = 'platform management', + PLATFORM = 'platform', STUDIO = 'studio', } export type FeatureProductType = PRODUCT | ADDITIONAL_PRODUCTS +export enum PRODUCT_STAGES { + PRIVATE_ALPHA = 'Private Alpha', + PUBLIC_ALPHA = 'Public Alpha', + BETA = 'Beta', + PUBLIC_BETA = 'Public Beta', + GA = 'General Availability', +} + export type FeatureType = { /** * name of the feature @@ -59,7 +71,7 @@ export type FeatureType = { /** * icon will be displayed in the feature card */ - icon: string | LucideIcon + icon: string | LucideIcon | FunctionComponent /** * Each feature belongs to one or more products */ @@ -79,6 +91,17 @@ export type FeatureType = { * url to docs or blog page for this feature */ docsUrl?: string + /** + * feature metadata on its status + */ + status?: { + stage: PRODUCT_STAGES + availableOnSelfHosted: boolean + selfHostedTooling?: { + label: string + link: string + } + } } export const features: FeatureType[] = [ @@ -106,6 +129,10 @@ It's 100% portable, which means you can easily migrate your data to and from oth heroImageLight: '/images/features/postgres-database-light.png', docsUrl: 'https://supabase.com/docs/guides/database/overview', slug: 'postgres-database', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Vector database', @@ -127,6 +154,10 @@ Supabase provides an open source toolkit for developing AI applications using Po heroImage: 'https://www.youtube-nocookie.com/embed/ibzlEQmgPPY', docsUrl: 'https://supabase.com/docs/guides/ai', slug: 'vector-database', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Auto-generated REST API via PostgREST', @@ -149,6 +180,10 @@ Supabase automatically generates a comprehensive RESTful API from your database heroImageLight: '/images/features/auto-generated-rest-api-light.png', docsUrl: 'https://supabase.com/docs/guides/api#rest-api-overview', slug: 'auto-generated-rest-api', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Auto-generated GraphQL API via pg_graphql', @@ -178,6 +213,10 @@ Supabase offers lightning-fast GraphQL APIs through its custom-built Postgres Gr heroImageLight: '/images/features/auto-generated-graphql-api-light.png', docsUrl: 'https://supabase.com/docs/guides/graphql/api', slug: 'auto-generated-graphql-api', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Database backups', @@ -213,6 +252,14 @@ Supabase's backup solutions provide a balance of security, flexibility, and ease heroImageLight: '/images/features/backups-light.png', docsUrl: 'https://supabase.com/docs/guides/platform/backups', slug: 'database-backups', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + selfHostedTooling: { + label: 'wal-g', + link: 'https://github.com/wal-g/wal-g', + }, + }, }, { title: 'Custom domains', @@ -238,6 +285,10 @@ By using custom domains, you create a more cohesive brand experience and gain fl heroImageLight: '/images/features/custom-domains-light.png', docsUrl: 'https://supabase.com/docs/guides/platform/custom-domains', slug: 'custom-domains', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: false, + }, }, { title: 'Network restrictions', @@ -271,6 +322,10 @@ By implementing Network Restrictions, you create a more secure environment for y heroImageLight: '/images/features/network-restrictions-light.png', docsUrl: 'https://supabase.com/docs/guides/platform/network-restrictions', slug: 'network-restrictions', + status: { + stage: PRODUCT_STAGES.BETA, + availableOnSelfHosted: false, + }, }, { title: 'SSL enforcement', @@ -306,6 +361,10 @@ By enabling SSL Enforcement, you implement a fundamental best practice in data p heroImageLight: '/images/features/ssl-enforcement-light.png', docsUrl: 'https://supabase.com/docs/guides/platform/ssl-enforcement', slug: 'ssl-enforcement', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: false, + }, }, { title: 'Branching', @@ -338,6 +397,10 @@ Supabase Branching allows you to create and test changes in separate, temporary heroImageLight: '/images/features/branching-light.png', docsUrl: 'https://supabase.com/docs/guides/platform/branching', slug: 'branching', + status: { + stage: PRODUCT_STAGES.PUBLIC_ALPHA, + availableOnSelfHosted: false, + }, }, { title: 'Terraform provider', @@ -374,6 +437,10 @@ By adopting the Supabase Terraform provider, teams can implement GitOps practice heroImageLight: '/images/features/terraform-provider-light.png', docsUrl: 'https://supabase.com/docs/guides/deployment/terraform', slug: 'terraform-provider', + status: { + stage: PRODUCT_STAGES.PUBLIC_ALPHA, + availableOnSelfHosted: false, + }, }, { title: 'Read replicas', @@ -409,6 +476,10 @@ By leveraging Read Replicas, you can achieve consistent low-latency performance heroImage: 'https://www.youtube-nocookie.com/embed/PX3R1fXjJ2M', docsUrl: 'https://supabase.com/docs/guides/platform/read-replicas', slug: 'read-replicas', + status: { + stage: PRODUCT_STAGES.PRIVATE_ALPHA, + availableOnSelfHosted: false, + }, }, { title: 'Fly Postgres', @@ -443,6 +514,10 @@ By leveraging Fly Postgres, you can achieve improved global performance for your heroImage: '', docsUrl: 'https://supabase.com/docs/guides/platform/fly-postgres', slug: 'fly-postgres', + status: { + stage: PRODUCT_STAGES.PRIVATE_ALPHA, + availableOnSelfHosted: false, + }, }, { title: 'Postgres Extensions', @@ -483,6 +558,10 @@ By leveraging these extensions, you can implement complex features more easily, heroImageLight: '/images/features/postgres-extensions-light.png', docsUrl: 'https://supabase.com/docs/guides/database/extensions', slug: 'postgres-extensions', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Database Webhooks', @@ -505,6 +584,10 @@ This feature is particularly useful for developers looking to automate workflows heroImageLight: '/images/features/database-webhooks-light.png', docsUrl: 'https://supabase.com/docs/guides/database/webhooks', slug: 'database-webhooks', + status: { + stage: PRODUCT_STAGES.BETA, + availableOnSelfHosted: true, + }, }, { title: 'Vault', @@ -526,6 +609,10 @@ This feature is particularly useful for teams looking to enhance their security heroImage: 'https://supabase.com/docs/img/guides/database/vault-hello-compressed.mp4', docsUrl: 'https://supabase.com/docs/guides/database/vault', slug: 'vault', + status: { + stage: PRODUCT_STAGES.PUBLIC_ALPHA, + availableOnSelfHosted: true, + }, }, { title: 'Supavisor', @@ -547,10 +634,14 @@ This feature is particularly valuable for teams looking to optimize their databa heroImage: 'https://www.youtube-nocookie.com/embed/ogYNmJOFEpk', docsUrl: 'https://supabase.com/blog/supavisor-postgres-connection-pooler', slug: 'supavisor', + status: { + stage: PRODUCT_STAGES.PUBLIC_BETA, + availableOnSelfHosted: true, + }, }, // Realtime { - title: 'Realtime Postgres changes', + title: 'Realtime - Postgres changes', subtitle: 'Receive your database changes through websockets.', description: ` Supabase's Realtime Postgres Changes feature allows you to listen to database changes in real-time using the Realtime system. This capability enables you to build responsive, live-updating applications that reflect database changes instantly. @@ -582,9 +673,13 @@ Supabase's Realtime Postgres Changes feature provides a powerful tool for creati heroImage: 'https://www.youtube-nocookie.com/embed/2rUjcmgZDwQ', docsUrl: 'https://supabase.com/docs/guides/realtime/postgres-changes', slug: 'realtime-postgres-changes', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { - title: 'Realtime Broadcast', + title: 'Realtime - Broadcast', subtitle: 'Send messages between connected users through websockets.', description: ` Supabase's Realtime Broadcast feature enables real-time communication between connected clients using the Realtime system. This functionality allows you to build interactive, collaborative applications where users can instantly share messages or data in real-time. @@ -619,9 +714,13 @@ Supabase's Realtime Broadcast feature provides a powerful tool for creating resp heroImage: 'https://www.youtube-nocookie.com/embed/BelYEMJ2N00', docsUrl: 'https://supabase.com/docs/guides/realtime/broadcast', slug: 'realtime-broadcast', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { - title: 'Realtime Presence', + title: 'Realtime - Presence', subtitle: 'Synchronize shared state between users through websockets.', description: ` Supabase's Realtime Presence feature allows you to track and synchronize shared state between connected users in real-time. This capability enables you to build collaborative applications where users can see each other's status, actions, or any custom state information instantly. @@ -655,6 +754,59 @@ Supabase's Realtime Presence feature provides a powerful tool for creating inter heroImage: 'https://www.youtube-nocookie.com/embed/BelYEMJ2N00', docsUrl: 'https://supabase.com/docs/guides/realtime/presence', slug: 'realtime-presence', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, + }, + { + title: 'Realtime - Broadcast Authorization', + subtitle: 'Control access to broadcast channels in real-time.', + description: ` +The Realtime Broadcast Authorization feature allows you to manage access permissions for broadcast channels in your application. This functionality ensures that only authorized users can listen to specific channels, enhancing security and control over real-time data streams. + +Key benefits: +1. Secure Access Control: Implement fine-grained access control over who can receive broadcast messages in real-time. +2. Customizable Permissions: Define specific authorization rules based on user roles or attributes. +3. Enhanced User Experience: Ensure that users only receive relevant updates based on their permissions. +4. Easy Integration with Existing Systems: Seamlessly integrate broadcast authorization into your current application architecture. +5. Comprehensive Documentation Available: Access detailed guides on how to implement broadcast authorization effectively. + +This feature is particularly valuable for applications that require controlled access to live data streams, such as chat applications or collaborative tools. +`, + icon: Shield, + products: [PRODUCT_SHORTNAMES.REALTIME, PRODUCT_SHORTNAMES.AUTHENTICATION], + heroImage: 'https://www.youtube-nocookie.com/embed/IXRrU9MpA8Q', + docsUrl: 'https://supabase.com/docs/guides/realtime/authorization#broadcast', + slug: 'realtime-broadcast-authorization', + status: { + stage: PRODUCT_STAGES.PUBLIC_BETA, + availableOnSelfHosted: true, + }, + }, + { + title: 'Realtime - Presence Authorization', + subtitle: 'Manage presence information securely in real-time.', + description: ` +The Realtime Presence Authorization feature enables you to control access permissions related to presence information in your application. This allows you to manage who can see the online status of other users in real-time, enhancing privacy and security within collaborative environments. + +Key benefits: +1. Controlled Visibility of Presence Data: Ensure that only authorized users can view the online status of others, protecting user privacy. +2. Customizable Presence Rules: Define specific rules based on user roles or attributes for who can see presence information. +3. Improved User Experience: Enhance collaboration by providing relevant presence information while maintaining security protocols. +4. Seamless Integration: Easily incorporate presence authorization into existing systems without significant overhead. +5. Detailed Implementation Guides: Access comprehensive documentation on how to implement presence authorization effectively. + +This feature is particularly useful for collaborative applications where knowing the online status of team members is crucial while ensuring privacy and security are maintained.`, + icon: Eye, + products: [PRODUCT_SHORTNAMES.REALTIME, PRODUCT_SHORTNAMES.AUTHENTICATION], + heroImage: 'https://www.youtube-nocookie.com/embed/IXRrU9MpA8Q', + docsUrl: 'https://supabase.com/docs/guides/realtime/authorization#presence', + slug: 'realtime-presence-authorization', + status: { + stage: PRODUCT_STAGES.PUBLIC_BETA, + availableOnSelfHosted: true, + }, }, // Auth { @@ -691,6 +843,10 @@ Supabase's Email Login provides a robust foundation for user authentication, all heroImageLight: '/images/features/email-login-light.png', docsUrl: 'https://supabase.com/docs/guides/auth/passwords', slug: 'email-login', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Social login', @@ -726,6 +882,10 @@ Supabase's Social Login feature allows you to improve user experience and potent heroImageLight: '/images/features/social-login-light.png', docsUrl: 'https://supabase.com/docs/guides/auth/social-login', slug: 'social-login', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Phone logins', @@ -760,6 +920,10 @@ Supabase's Phone Login feature enables a seamless authentication experience, par heroImageLight: '/images/features/phone-login-light.png', docsUrl: 'https://supabase.com/docs/guides/auth/phone-login', slug: 'phone-logins', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Passwordless login via Magic Links', @@ -795,6 +959,10 @@ Supabase's Magic Links feature offers a secure and user-friendly authentication heroImage: '', docsUrl: 'https://supabase.com/docs/guides/auth/auth-email-passwordless', slug: 'passwordless-login-via-magicklink', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Multi-Factor Authentication (MFA)', @@ -830,6 +998,10 @@ Supabase's MFA feature provides a robust tool for enhancing application security heroImage: '', docsUrl: 'https://supabase.com/docs/guides/auth/auth-mfa', slug: 'multi-factor-authentication', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Authorization via Row Level Security', @@ -865,6 +1037,10 @@ Supabase's RLS feature provides a powerful tool for implementing sophisticated a heroImage: `https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/website/supabase-rls.mp4`, docsUrl: 'https://supabase.com/docs/guides/auth/row-level-security', slug: 'row-level-security', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Captcha protection', @@ -900,6 +1076,10 @@ Supabase's Captcha Protection feature provides a powerful tool for enhancing app heroImageLight: '/images/features/auth-captcha-protection-light.png', docsUrl: 'https://supabase.com/docs/guides/auth/auth-captcha', slug: 'auth-captcha-protection', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Server-side Auth', @@ -934,6 +1114,10 @@ Supabase's Server-side Auth feature allows developers to create more secure and heroImage: '', docsUrl: 'https://supabase.com/docs/guides/auth/server-side', slug: 'server-side-auth', + status: { + stage: PRODUCT_STAGES.BETA, + availableOnSelfHosted: true, + }, }, // Storage { @@ -971,6 +1155,10 @@ Supabase Storage simplifies adding robust file management to your applications, heroImageLight: '/images/features/file-storage-light.png', docsUrl: 'https://supabase.com/docs/guides/storage', slug: 'file-storage', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Content Delivery Network', @@ -1006,6 +1194,14 @@ Supabase's CDN significantly improves the performance and user experience of you heroImage: '', docsUrl: 'https://supabase.com/docs/guides/storage/cdn', slug: 'cdn', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + selfHostedTooling: { + label: 'Cloudflare', + link: 'https://www.cloudflare.com', + }, + }, }, { title: 'Smart Content Delivery Network', @@ -1041,6 +1237,14 @@ Supabase's Smart CDN optimizes both performance and content accuracy, providing heroImage: 'https://www.youtube-nocookie.com/embed/NpEl20iuOtg', docsUrl: 'https://supabase.com/docs/guides/storage/cdn/smart-cdn', slug: 'smart-cdn', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + selfHostedTooling: { + label: 'Cloudflare', + link: 'https://www.cloudflare.com', + }, + }, }, { title: 'Image transformations', @@ -1077,6 +1281,10 @@ Supabase's Image Transformations feature enables you to efficiently manage and s heroImage: 'https://www.youtube-nocookie.com/embed/dLqSmxX3r7I', docsUrl: 'https://supabase.com/docs/guides/storage/image-transformations', slug: 'image-transformations', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Resumable uploads', @@ -1113,6 +1321,10 @@ Supabase's Resumable Uploads feature significantly improves the reliability and heroImage: 'https://www.youtube-nocookie.com/embed/pT2PcZFq_M0', docsUrl: 'https://supabase.com/docs/guides/storage/uploads/resumable-uploads', slug: 'resumable-uploads', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'S3 compatibility', @@ -1150,6 +1362,10 @@ Supabase's S3 compatibility allows seamless integration with existing workflows heroImageLight: '/images/features/s3-compatibility-light.png', docsUrl: 'https://supabase.com/docs/guides/storage/s3/compatibility', slug: 's3-compatibility', + status: { + stage: PRODUCT_STAGES.PUBLIC_ALPHA, + availableOnSelfHosted: true, + }, }, // Functions { @@ -1189,6 +1405,10 @@ Supabase's Deno Edge Functions enable you to build responsive, globally distribu heroImage: 'https://www.youtube-nocookie.com/embed/5OWH9c4u68M', docsUrl: 'https://supabase.com/docs/guides/functions', slug: 'deno-edge-functions', + status: { + stage: PRODUCT_STAGES.BETA, + availableOnSelfHosted: true, + }, }, { title: 'Regional invocations', @@ -1220,6 +1440,10 @@ By leveraging Supabase's Regional Invocations, you can significantly enhance the heroImage: '', docsUrl: 'https://supabase.com/docs/guides/functions/regional-invocation', slug: 'regional-invocations', + status: { + stage: PRODUCT_STAGES.BETA, + availableOnSelfHosted: true, + }, }, { title: 'NPM compatibility', @@ -1251,6 +1475,10 @@ By leveraging NPM Compatibility in Supabase Edge Functions, you can take advanta heroImage: 'https://www.youtube-nocookie.com/embed/eCbiywoDORw', docsUrl: 'https://supabase.com/blog/edge-functions-node-npm', slug: 'npm-compatibility', + status: { + stage: PRODUCT_STAGES.BETA, + availableOnSelfHosted: true, + }, }, // Vector { @@ -1283,6 +1511,10 @@ By leveraging Supabase's AI Integrations, you can rapidly develop and deploy sop heroImage: 'https://www.youtube-nocookie.com/embed/OgnYxRkxEUw', docsUrl: 'https://supabase.com/docs/guides/ai/examples/huggingface-image-captioning', slug: 'ai-integrations', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, // Platform { @@ -1316,6 +1548,10 @@ By leveraging the Supabase CLI, you can significantly improve your development w docsUrl: 'https://supabase.com/docs/guides/local-development?queryGroups=package-manager&package-manager=pnpm', slug: 'cli', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Management API', @@ -1348,6 +1584,10 @@ By utilizing the Supabase Management API, you can create more efficient, scalabl heroImageLight: '/images/features/management-api-light.png', docsUrl: 'https://supabase.com/docs/guides/project-management/api', slug: 'management-api', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Role-Based Access Control (RBAC)', @@ -1380,6 +1620,10 @@ By leveraging Role-Based Access Control, you can create a secure, scalable, and docsUrl: 'https://supabase.com/docs/guides/database/postgres/custom-claims-and-role-based-access-control-rbac', slug: 'role-based-access-control', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: false, + }, }, // Analytics { @@ -1412,6 +1656,10 @@ By leveraging Supabase Reports, you gain a understanding of your project's perfo heroImageLight: '/images/features/reports-and-metrics-light.png', docsUrl: 'https://supabase.com/blog/supabase-reports-and-metrics', slug: 'reports-and-metrics', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'SOC 2 Compliance', @@ -1449,6 +1697,10 @@ By building on Supabase's SOC 2 compliant platform, you gain a significant advan heroImage: '', docsUrl: 'https://supabase.com/docs/guides/security/compliance', slug: 'soc-2-compliance', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: false, + }, }, // Studio { @@ -1472,6 +1724,10 @@ This feature is especially beneficial for teams looking to improve their databas docsUrl: 'https://supabase.com/blog/studio-introducing-assistant#introducing-the-supabase-assistant', slug: 'ai-query-assistance', + status: { + stage: PRODUCT_STAGES.PUBLIC_ALPHA, + availableOnSelfHosted: true, + }, }, { title: 'Logs & Analytics', @@ -1494,6 +1750,10 @@ This feature is particularly valuable for teams looking to enhance their applica heroImageLight: '/images/features/logs-analytics-light.png', docsUrl: 'https://supabase.com/docs/guides/monitoring-troubleshooting/logs', slug: 'logs-analytics', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Visual Schema Designer', @@ -1516,6 +1776,10 @@ This feature is particularly valuable for teams engaged in agile development pro heroImageLight: '/images/features/visual-schema-designer-light.png', docsUrl: 'https://supabase.com/blog/supabase-studio-3-0#schema-visualizer', slug: 'visual-schema-designer', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Policy Templates', @@ -1538,6 +1802,10 @@ This feature is essential for organizations looking to maintain robust security heroImageLight: '/images/features/policy-templates-light.png', docsUrl: '', slug: 'policy-templates', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'SQL Editor', @@ -1560,6 +1828,10 @@ This feature is particularly valuable for developers looking to streamline their heroImageLight: '/images/features/sql-editor-light.png', docsUrl: '', slug: 'sql-editor', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Security & Performance Advisor', @@ -1582,6 +1854,10 @@ This feature is essential for organizations aiming to maintain high security sta heroImageLight: '/images/features/security-and-performance-advisor-light.png', docsUrl: 'https://supabase.com/blog/security-and-performance-advisor', slug: 'security-and-performance-advisor', + status: { + stage: PRODUCT_STAGES.BETA, + availableOnSelfHosted: true, + }, }, { title: 'Postgres Roles', @@ -1604,6 +1880,10 @@ This feature is particularly valuable for teams looking to implement robust secu heroImageLight: '/images/features/postgres-roles-light.png', docsUrl: 'https://supabase.com/docs/guides/database/postgres/roles', slug: 'postgres-roles', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Foreign Key Selector', @@ -1626,6 +1906,10 @@ This feature is particularly beneficial for developers working with complex data 'https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/fk-lookup.mp4', docsUrl: 'https://supabase.com/blog/supabase-studio-2.0#foreign-key-selector', slug: 'foreign-key-selector', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, }, { title: 'Log Drains', @@ -1647,5 +1931,109 @@ This feature is particularly useful for teams seeking to enhance their observabi heroImage: 'https://www.youtube-nocookie.com/embed/A4GFmvgxS-E', docsUrl: 'https://supabase.com/blog/log-drains', slug: 'log-drains', + status: { + stage: PRODUCT_STAGES.PUBLIC_ALPHA, + availableOnSelfHosted: true, + }, + }, + { + title: 'Client Library - JavaScript', + subtitle: 'Easily integrate Supabase with your JavaScript applications.', + description: ` +The Supabase JavaScript Client Library provides a straightforward way to interact with your Supabase database and services directly from your JavaScript applications. This library simplifies the process of making API calls, managing authentication, and handling real-time updates, making it an essential tool for developers working with Supabase. + +Key benefits: +1. Simplified API Interaction: Easily connect to your Supabase backend with minimal setup. +2. Built-in Authentication: Manage user authentication seamlessly within your JavaScript applications. +3. Real-Time Capabilities: Subscribe to changes in your database and receive updates in real-time. +4. Comprehensive Documentation: Access detailed guides and examples to help you get started quickly. +5. Community Support: Join a growing community of developers using the JavaScript Client Library for various projects. + +This feature is particularly valuable for developers looking to build dynamic web applications that leverage the power of Supabase as a backend service. +`, + icon: JsIcon, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: '', + docsUrl: 'https://supabase.com/docs/reference/javascript/start', + slug: 'client-library-javascript', + status: { + stage: PRODUCT_STAGES.GA, + availableOnSelfHosted: true, + }, + }, + { + title: 'Client Library - Flutter', + subtitle: 'Integrate Supabase into your Flutter applications effortlessly.', + description: ` +The Supabase Flutter Client Library allows developers to easily integrate Supabase into their Flutter applications. This library provides a comprehensive set of tools for managing database interactions, user authentication, and real-time data updates, all tailored for the Flutter framework. + +Key benefits: +1. Seamless Integration: Connect your Flutter app to Supabase with minimal configuration. +2. User Authentication: Manage user sign-ups, logins, and sessions directly within your Flutter application. +3. Real-Time Updates: Receive live updates from your database, enhancing user experience. +4. Detailed Documentation: Access extensive resources and examples to facilitate development. +5. Active Community: Engage with other Flutter developers leveraging Supabase for their projects. + +This feature is particularly useful for Flutter developers aiming to create responsive mobile applications backed by a powerful database solution. +`, + icon: FlutterIcon, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: '', + docsUrl: 'https://supabase.com/docs/reference/dart/start', + slug: 'client-library-flutter', + status: { + stage: PRODUCT_STAGES.BETA, + availableOnSelfHosted: true, + }, + }, + { + title: 'Client Library - Swift', + subtitle: 'Effortlessly connect your Swift applications to Supabase.', + description: ` +The Supabase Swift Client Library provides an easy way for developers to integrate their iOS applications with Supabase services. This library simplifies database access, user authentication, and real-time data handling, making it an essential tool for Swift developers. + +Key benefits: +1. Easy Integration: Quickly connect your Swift app to Supabase with straightforward setup instructions. +2. Authentication Management: Handle user authentication seamlessly within your iOS applications. +3. Real-Time Data Handling: Subscribe to changes in your database and receive updates instantly. +4. Comprehensive Guides: Utilize detailed documentation and examples to streamline development. +5. Supportive Community: Join a community of Swift developers using Supabase for their app development needs. + +This feature is particularly valuable for iOS developers looking to leverage the capabilities of Supabase in their mobile applications. +`, + icon: SwiftIcon, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: '', + docsUrl: 'https://supabase.com/docs/reference/swift/start', + slug: 'client-library-swift', + status: { + stage: PRODUCT_STAGES.BETA, + availableOnSelfHosted: true, + }, + }, + { + title: 'Client Library - Python', + subtitle: 'Integrate Supabase easily into your Python applications.', + description: ` +The Supabase Python Client Library enables developers to connect their Python applications with Supabase effortlessly. This library provides tools for interacting with the database, managing user authentication, and handling real-time updates, tailored specifically for Python developers. + +Key benefits: +1. Simple Connection Setup: Easily connect your Python application to Supabase with minimal configuration. +2. User Authentication Support: Manage user accounts and sessions directly within your Python code. +3. Real-Time Data Updates: Subscribe to changes in your database and receive live updates as they occur. +4. In-Depth Documentation: Access comprehensive guides and examples to assist in development. +5. Engaged Community Support: Connect with other Python developers utilizing Supabase in their projects. + +This feature is especially beneficial for Python developers looking to build robust applications powered by a scalable backend service like Supabase. +`, + icon: PythonIcon, + products: [ADDITIONAL_PRODUCTS.PLATFORM], + heroImage: '', + docsUrl: 'https://supabase.com/docs/reference/python/start', + slug: 'client-library-python', + status: { + stage: PRODUCT_STAGES.BETA, + availableOnSelfHosted: true, + }, }, ] diff --git a/apps/www/pages/assistant.tsx b/apps/www/pages/assistant.tsx new file mode 100644 index 00000000000..9730ba19c55 --- /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/pages/features.tsx b/apps/www/pages/features.tsx index a8654f8f666..40a0f5d60c0 100644 --- a/apps/www/pages/features.tsx +++ b/apps/www/pages/features.tsx @@ -131,7 +131,7 @@ function FeaturesPage() {

Filter by tags:

{products - .sort((a, b) => (a > b ? 1 : -1)) + .sort((a, b) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1)) .map((product) => (
= ({ feature, prevFeature, nextFea flex flex-col gap-4 md:gap-8 text-foreground-light - !py-10 + !pt-10 !pb-4 " >
-
+
+ + + + + {feature.products.map((product) => ( - + {product} @@ -186,6 +199,29 @@ const FeaturePage: React.FC = ({ feature, prevFeature, nextFea +
+
+ Stage: + {feature.status?.stage} +
+
+ Available on self-hosted: + {feature.status?.selfHostedTooling ? ( + + Requires + + {feature.status?.selfHostedTooling?.label} + + + ) : ( + {feature.status?.availableOnSelfHosted ? 'Yes' : 'N/A'} + )} +
+
{feature.description} diff --git a/apps/www/public/images/ai/error.gif b/apps/www/public/images/ai/error.gif new file mode 100644 index 00000000000..37b9e76b260 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 00000000000..98cdfe1366a 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 00000000000..8693d18b501 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 bb2157e2063..5cba5c3e8da 100644 --- a/apps/www/public/rss.xml +++ b/apps/www/public/rss.xml @@ -11,7 +11,7 @@ https://supabase.com/blog/supabase-ai-assistant-v2 Supabase AI Assistant v2 https://supabase.com/blog/supabase-ai-assistant-v2 - An evolution of how we approach AI within the Supabase dashboard' + An evolution of how we approach AI within the Supabase dashboard Mon, 02 Dec 2024 00:00:00 -0700 @@ -21,6 +21,13 @@ An evolution of how we approach AI within the Supabase dashboard Mon, 02 Dec 2024 00:00:00 -0700 + + https://supabase.com/blog/orioledb-launch + OrioleDB Public Alpha + https://supabase.com/blog/orioledb-launch + Launching OrioleDB Public Alpha + Sun, 01 Dec 2024 00:00:00 -0700 + https://supabase.com/blog/supabase-dynamic-functions Executing Dynamic JavaScript Code on Supabase with Edge Functions diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 21da9cb3c59..803c5af8f34 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 a32c2d06a21..de3c5ea0f71 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 0c756ff94cd..f4868df710b 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 e69de29bb2d..00000000000 diff --git a/examples/ai/aws_bedrock_image_search/supabase/config.toml b/examples/ai/aws_bedrock_image_search/supabase/config.toml index 2af8d3e1813..2571a2f783d 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 e69de29bb2d..00000000000 diff --git a/examples/storage/protomaps/supabase/config.toml b/examples/storage/protomaps/supabase/config.toml index c38bfcfcba2..b5611e5b582 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 c103cd7b57a..78e915c30a5 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 0dfcff65469..01f31d93ddb 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/shared-data/extensions.json b/packages/shared-data/extensions.json index 9b253e941b2..527ea514f6d 100644 --- a/packages/shared-data/extensions.json +++ b/packages/shared-data/extensions.json @@ -230,8 +230,8 @@ "tags": ["Utility"], "link": "/guides/database/extensions/pg_cron", "github_url": "https://github.com/citusdata/pg_cron", - "product": "Supabase Cron Jobs", - "product_url": "/project/{ref}/integrations/cron-jobs" + "product": "Supabase Cron", + "product_url": "/project/{ref}/integrations/cron" }, { "name": "pg_freespacemap",