Merge branch 'master' into feat/tiger-d1

This commit is contained in:
Jonathan Summers-Muir committed 2024-12-02 22:22:27 +08:00
commit 399ca9ff14
82 files changed
+3015 -976

No files matched your search

@@ -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,
@@ -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.
<Image
alt="TPC-C (warehouses = 500)"
src="/docs/img/database/orioledb-tpc-c-500-warehouse.png"
className="max-w-[550px] !mx-auto border rounded-md"
zoomable
/>
<Admonition type="info">
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.
</Admonition>
## 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.
<Image
alt="Creating OrioleDB project"
src={{
light: '/docs/img/database/orioledb-creating-project--light.png',
dark: '/docs/img/database/orioledb-creating-project.png',
}}
className="max-w-[550px] !mx-auto border rounded-md"
zoomable
/>
### 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.
<Admonition type="info">
Currently, only B-tree indexes are supported, so features like pg_vector's HNSW indexes are not yet available.
</Admonition>
```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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

+2 -2
View File
@@ -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
@@ -49,7 +49,8 @@ export const TextEditor = <TRow, TSummaryRow = unknown>({
})
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<string | null>(initialValue)
const [isConfirmNextModalOpen, setIsConfirmNextModalOpen] = useState(false)
@@ -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 (
<div className="flex flex-col gap-y-2">
<p className="text-foreground-light text-sm mb-4">
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.
</p>
<Image
src={`${BASE_PATH}/img/previews/assistant-v2.png`}
width={1860}
height={970}
alt="api-docs-side-panel-preview"
className="rounded border"
/>
<p className="text-foreground-light text-sm mb-4">
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!
</p>
<p className="text-foreground-light text-sm mb-4">
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!
</p>
<div className="flex flex-col gap-y-2">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>
<Markdown
className="text-foreground-light"
content={`Add a button in the top navigation bar where you can access the AI Assistant from anywhere in the dashboard`}
/>
</li>
<li>
<Markdown
className="text-foreground-light"
content={`Add a keyboard shortcut (${os === 'macos' ? 'Cmd' : 'Ctrl'} + I) that can also open the Assistant from anywhere in the dashboard`}
/>
</li>
</ul>
</div>
</div>
)
}
@@ -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: <AssistantV2Preview />,
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]
}
@@ -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() {
@@ -72,7 +72,8 @@ const QueryError = ({
{open ? 'Hide error details' : 'Show error details'}
</Button>
</CollapsibleTrigger_Shadcn_>
{!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 && (
<Button
size="tiny"
type="default"
@@ -91,7 +92,7 @@ const QueryError = ({
>
Fix with Assistant
</Button>
)}
)} */}
</div>
<CollapsibleContent_Shadcn_ className="overflow-auto">
{formattedError.length > 0 ? (
@@ -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 = ({
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1.5" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent
side="bottom"
align="end"
className={cn(isAssistantV2Enabled ? 'w-52' : 'w-40')}
>
<DropdownMenuContent side="bottom" align="end" className="w-52">
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectEditPolicy(policy)}>
<Edit size={14} />
<p>Edit policy</p>
</DropdownMenuItem>
{isAssistantV2Enabled && (
<DropdownMenuItem
className="space-x-2"
onClick={() => {
const sql = generatePolicyCreateSQL(policy)
setAiAssistantPanel({
open: true,
sqlSnippets: [sql],
initialInput: `Update the policy with name "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
suggestions: {
title: `I can help you make a change to the policy "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table, here are a few example prompts to get you started:`,
prompts: [
'Tell me how I can improve this policy...',
'Duplicate this policy for another table...',
'Add extra conditions to this policy...',
],
},
})
}}
>
<Edit size={14} />
<p>Edit policy with Assistant</p>
</DropdownMenuItem>
)}
<DropdownMenuItem
className="space-x-2"
onClick={() => {
const sql = generatePolicyCreateSQL(policy)
setAiAssistantPanel({
open: true,
sqlSnippets: [sql],
initialInput: `Update the policy with name "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
suggestions: {
title: `I can help you make a change to the policy "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table, here are a few example prompts to get you started:`,
prompts: [
'Tell me how I can improve this policy...',
'Duplicate this policy for another table...',
'Add extra conditions to this policy...',
],
},
})
}}
>
<Edit size={14} />
<p>Edit policy with Assistant</p>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItemTooltip
className="gap-x-2"
@@ -4,7 +4,6 @@ import { Lock, Unlock } from 'lucide-react'
import { useQueryState } from 'nuqs'
import { useParams } from 'common'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
@@ -37,7 +36,6 @@ const PolicyTableRowHeader = ({
const { ref } = useParams()
const { setAiAssistantPanel } = useAppStateSnapshot()
const enableAssistantV2 = useIsAssistantV2Enabled()
const canCreatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies')
const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
@@ -117,15 +115,10 @@ const PolicyTableRowHeader = ({
type="default"
className="px-1"
onClick={() => {
if (enableAssistantV2) {
setAiAssistantPanel({
open: true,
initialInput: `Create a new policy for the ${table.schema} schema on the ${table.name} table that ...`,
})
} else {
onSelectCreatePolicy()
setEditView('conversation')
}
setAiAssistantPanel({
open: true,
initialInput: `Create a new policy for the ${table.schema} schema on the ${table.name} table that ...`,
})
}}
tooltip={{
content: {
@@ -8,8 +8,16 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
import { useDatabaseExtensionDisableMutation } from 'data/database-extensions/database-extension-disable-mutation'
import { DatabaseExtension } from 'data/database-extensions/database-extensions-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsOrioleDb } from 'hooks/misc/useSelectedProject'
import { extensions } from 'shared-data'
import { Button, cn, Switch } from 'ui'
import {
Button,
cn,
Switch,
Tooltip_Shadcn_,
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
} from 'ui'
import { Admonition } from 'ui-patterns'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import EnableExtensionModal from './EnableExtensionModal'
@@ -23,6 +31,7 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
const { project } = useProjectContext()
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || ''
const isOn = extension.installed_version !== null
const isOrioleDb = useIsOrioleDb()
const [isDisableModalOpen, setIsDisableModalOpen] = useState(false)
const [showConfirmEnableModal, setShowConfirmEnableModal] = useState(false)
@@ -31,6 +40,8 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'extensions'
)
const orioleDbCheck = isOrioleDb && extension.name === 'orioledb'
const disabled = !canUpdateExtensions || orioleDbCheck
const X_PADDING = 'px-5'
const extensionMeta = extensions.find((item: any) => item.name === extension.name)
@@ -76,13 +87,26 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
{isDisabling ? (
<Loader2 className="animate-spin" size={16} />
) : (
<Switch
disabled={!canUpdateExtensions}
checked={isOn}
onCheckedChange={() =>
isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
}
/>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_>
<Switch
disabled={disabled}
checked={isOn}
onCheckedChange={() =>
isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
}
/>
</TooltipTrigger_Shadcn_>
{disabled && (
<TooltipContent_Shadcn_ side="bottom">
{!canUpdateExtensions
? 'You need additional permissions to toggle extensions'
: orioleDbCheck
? 'Project is using OrioleDB and cannot be disabled'
: null}
</TooltipContent_Shadcn_>
)}
</Tooltip_Shadcn_>
)}
</div>
@@ -11,14 +11,12 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAppStateSnapshot } from 'state/app-state'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
interface FunctionListProps {
schema: string
@@ -38,7 +36,6 @@ const FunctionList = ({
const router = useRouter()
const { project: selectedProject } = useProjectContext()
const { setAiAssistantPanel } = useAppStateSnapshot()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const { data: functions } = useDatabaseFunctionsQuery({
projectRef: selectedProject?.ref,
@@ -111,10 +108,7 @@ const FunctionList = ({
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent
side="left"
className={cn(isAssistantV2Enabled ? 'w-52' : 'w-40')}
>
<DropdownMenuContent side="left" className="w-52">
{isApiDocumentAvailable && (
<DropdownMenuItem
className="space-x-2"
@@ -128,30 +122,28 @@ const FunctionList = ({
<Edit2 size={14} />
<p>Edit function</p>
</DropdownMenuItem>
{isAssistantV2Enabled && (
<DropdownMenuItem
className="space-x-2"
onClick={() => {
setAiAssistantPanel({
open: true,
initialInput: 'Update this function to do...',
suggestions: {
title:
'I can help you make a change to this function, here are a few example prompts to get you started:',
prompts: [
'Rename this function to ...',
'Modify this function so that it ...',
'Add a trigger for this function that calls it when ...',
],
},
sqlSnippets: [x.complete_statement],
})
}}
>
<Edit size={14} />
<p>Edit function with Assistant</p>
</DropdownMenuItem>
)}
<DropdownMenuItem
className="space-x-2"
onClick={() => {
setAiAssistantPanel({
open: true,
initialInput: 'Update this function to do...',
suggestions: {
title:
'I can help you make a change to this function, here are a few example prompts to get you started:',
prompts: [
'Rename this function to ...',
'Modify this function so that it ...',
'Add a trigger for this function that calls it when ...',
],
},
sqlSnippets: [x.complete_statement],
})
}}
>
<Edit size={14} />
<p>Edit function with Assistant</p>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="space-x-2" onClick={() => deleteFunction(x)}>
<Trash size={14} className="text-destructive" />
@@ -5,7 +5,6 @@ import { Search } from 'lucide-react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import Table from 'components/to-be-cleaned/Table'
@@ -38,7 +37,6 @@ const FunctionsList = ({
const { search } = useParams()
const { project } = useProjectContext()
const { setAiAssistantPanel } = useAppStateSnapshot()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const filterString = search ?? ''
@@ -144,28 +142,26 @@ const FunctionsList = ({
>
Create a new function
</ButtonTooltip>
{isAssistantV2Enabled && (
<ButtonTooltip
type="default"
disabled={!canCreateFunctions}
className="px-1 pointer-events-auto"
icon={<AiIconAnimation size={16} />}
onClick={() =>
setAiAssistantPanel({
open: true,
initialInput: `Create a new function for the schema ${selectedSchema} that does ...`,
})
}
tooltip={{
content: {
side: 'bottom',
text: !canCreateFunctions
? 'You need additional permissions to create functions'
: 'Create with Supabase Assistant',
},
}}
/>
)}
<ButtonTooltip
type="default"
disabled={!canCreateFunctions}
className="px-1 pointer-events-auto"
icon={<AiIconAnimation size={16} />}
onClick={() =>
setAiAssistantPanel({
open: true,
initialInput: `Create a new function for the schema ${selectedSchema} that does ...`,
})
}
tooltip={{
content: {
side: 'bottom',
text: !canCreateFunctions
? 'You need additional permissions to create functions'
: 'Create with Supabase Assistant',
},
}}
/>
</>
)}
</div>
@@ -1,6 +1,13 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { includes, sortBy } from 'lodash'
import { Check, Edit, Edit2, MoreVertical, Trash, X } from 'lucide-react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import Table from 'components/to-be-cleaned/Table'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAppStateSnapshot } from 'state/app-state'
import {
Badge,
Button,
@@ -12,15 +19,6 @@ import {
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
} from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import Table from 'components/to-be-cleaned/Table'
import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Check, X, MoreVertical, Edit3, Trash, Edit, Edit2 } from 'lucide-react'
import { useAppStateSnapshot } from 'state/app-state'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { cn } from 'ui'
import { generateTriggerCreateSQL } from './TriggerList.utils'
interface TriggerListProps {
@@ -40,7 +38,6 @@ const TriggerList = ({
}: TriggerListProps) => {
const { project } = useProjectContext()
const { setAiAssistantPanel } = useAppStateSnapshot()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const { data: triggers } = useDatabaseTriggersQuery({
projectRef: project?.ref,
@@ -141,40 +138,34 @@ const TriggerList = ({
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent
side="bottom"
align="end"
className={cn(isAssistantV2Enabled ? 'w-52' : 'w-36')}
>
<DropdownMenuContent side="bottom" align="end" className="w-52">
<DropdownMenuItem className="space-x-2" onClick={() => editTrigger(x)}>
<Edit2 size={14} />
<p>Edit trigger</p>
</DropdownMenuItem>
{isAssistantV2Enabled && (
<DropdownMenuItem
className="space-x-2"
onClick={() => {
const sql = generateTriggerCreateSQL(x)
setAiAssistantPanel({
open: true,
initialInput: `Update this trigger which exists on the ${x.schema}.${x.table} table to...`,
suggestions: {
title:
'I can help you make a change to this trigger, here are a few example prompts to get you started:',
prompts: [
'Rename this trigger to ...',
'Change the events this trigger responds to ...',
'Modify this trigger to run after instead of before ...',
],
},
sqlSnippets: [sql],
})
}}
>
<Edit size={14} />
<p>Edit with Assistant</p>
</DropdownMenuItem>
)}
<DropdownMenuItem
className="space-x-2"
onClick={() => {
const sql = generateTriggerCreateSQL(x)
setAiAssistantPanel({
open: true,
initialInput: `Update this trigger which exists on the ${x.schema}.${x.table} table to...`,
suggestions: {
title:
'I can help you make a change to this trigger, here are a few example prompts to get you started:',
prompts: [
'Rename this trigger to ...',
'Change the events this trigger responds to ...',
'Modify this trigger to run after instead of before ...',
],
},
sqlSnippets: [sql],
})
}}
>
<Edit size={14} />
<p>Edit with Assistant</p>
</DropdownMenuItem>
<DropdownMenuItem className="space-x-2" onClick={() => deleteTrigger(x)}>
<Trash stroke="red" size={14} />
<p>Delete trigger</p>
@@ -182,26 +173,18 @@ const TriggerList = ({
</DropdownMenuContent>
</DropdownMenu>
) : (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger asChild>
<Button disabled type="default" icon={<MoreVertical />} />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="left">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-alternative py-1 px-2 leading-none shadow',
'border border-background',
].join(' ')}
>
<span className="text-xs text-foreground">
You need additional permissions to update triggers
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
<ButtonTooltip
disabled
type="default"
className="px-1"
icon={<MoreVertical />}
tooltip={{
content: {
side: 'bottom',
text: 'You need additional permissions to update triggers',
},
}}
/>
)}
</div>
)}
@@ -3,7 +3,6 @@ import { noop, partition } from 'lodash'
import { Search } from 'lucide-react'
import { useState } from 'react'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlphaPreview from 'components/to-be-cleaned/AlphaPreview'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
@@ -34,6 +33,7 @@ const TriggersList = ({
deleteTrigger = noop,
}: TriggersListProps) => {
const { project } = useProjectContext()
const { setAiAssistantPanel } = useAppStateSnapshot()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const [filterString, setFilterString] = useState<string>('')
@@ -59,9 +59,6 @@ const TriggersList = ({
const canCreateTriggers = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers')
const { setAiAssistantPanel } = useAppStateSnapshot()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
if (isLoading) {
return <GenericSkeletonLoader />
}
@@ -126,37 +123,35 @@ const TriggersList = ({
>
Create a new trigger
</ButtonTooltip>
{isAssistantV2Enabled && (
<ButtonTooltip
type="default"
disabled={!canCreateTriggers}
className="px-1 pointer-events-auto"
icon={<AiIconAnimation size={16} />}
onClick={() =>
setAiAssistantPanel({
open: true,
initialInput: `Create a new trigger for the schema ${selectedSchema} that does ...`,
suggestions: {
title:
'I can help you create a new trigger, here are a few example prompts to get you started:',
prompts: [
'Create a trigger that logs changes to the users table',
'Create a trigger that updates updated_at timestamp',
'Create a trigger that validates email format before insert',
],
},
})
}
tooltip={{
content: {
side: 'bottom',
text: !canCreateTriggers
? 'You need additional permissions to create triggers'
: 'Create with Supabase Assistant',
<ButtonTooltip
type="default"
disabled={!canCreateTriggers}
className="px-1 pointer-events-auto"
icon={<AiIconAnimation size={16} />}
onClick={() =>
setAiAssistantPanel({
open: true,
initialInput: `Create a new trigger for the schema ${selectedSchema} that does ...`,
suggestions: {
title:
'I can help you create a new trigger, here are a few example prompts to get you started:',
prompts: [
'Create a trigger that logs changes to the users table',
'Create a trigger that updates updated_at timestamp',
'Create a trigger that validates email format before insert',
],
},
}}
/>
)}
})
}
tooltip={{
content: {
side: 'bottom',
text: !canCreateTriggers
? 'You need additional permissions to create triggers'
: 'Create with Supabase Assistant',
},
}}
/>
</div>
)}
</div>
@@ -69,7 +69,7 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
onCheckedChange={() => showToggleConfirmationModal(true)}
/>
<Button type="default" icon={<History />}>
<Link href={`/project/${ref}/integrations/cron-jobs/cron-jobs/${job.jobname}`}>
<Link href={`/project/${ref}/integrations/cron/cron-jobs/${job.jobname}`}>
History
</Link>
</Button>
@@ -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<HTMLDivElement>) => {
if (isLoadingCronJobRuns || !isAtBottom(event)) return
@@ -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 = ({
<div className="flex-col justify-start items-start gap-y-0.5 flex">
<div className="flex items-center gap-x-2">
<p className="text-foreground text-sm">{name}</p>
{beta && (
<Badge variant="warning" className="py-0 px-1.5">
Beta
{status && (
<Badge variant="warning" className="py-0 px-1.5 capitalize">
{status}
</Badge>
)}
</div>
@@ -26,7 +26,7 @@ const Loading = () => (
export type IntegrationDefinition = {
id: string
name: string
beta?: boolean
status?: 'alpha' | 'beta'
icon: (props?: { className?: string; style?: Record<string, any> }) => 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 } = {}) => (
<Vault className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} />
),
@@ -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 []
}
}
@@ -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) => {
<SheetHeader>
<SheetTitle>Manage queue permissions on {name}</SheetTitle>
<SheetDescription>
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{' '}
<code className="text-xs">pgmq_public</code> schema.
</>
)}
</SheetDescription>
</SheetHeader>
<SheetSection className="p-0 flex-grow">
{!isExposed ? (
<Admonition
type="default"
className="rounded-none border-x-0 border-t-0"
title="Queue permissions are only relevant if exposure through PostgREST has been enabled"
description={
<>
You may opt to manage your queues via any Supabase client libraries or PostgREST
endpoints by enabling this in the{' '}
<Link
href={`/project/${project?.ref}/integrations/queues/settings`}
className="underline transition underline-offset-2 decoration-foreground-lighter hover:decoration-foreground"
>
queues settings
</Link>
</>
}
/>
) : (
<Admonition
type="default"
className="rounded-none border-x-0 border-t-0"
title="Only relevant roles for managing queues via client libraries or PostgREST are shown here"
/>
)}
<Table>
<TableHeader className="[&_th]:h-8">
<TableRow className="py-2">
<TableHead>Role</TableHead>
{ACTIONS.map((x) => (
<TableHead key={x} className="capitalize">
{x}
</TableHead>
))}
{ACTIONS.map((x) => {
const relatedFunctions = getQueueFunctionsMapping(x)
return (
<TableHead key={x}>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ className="mx-auto flex items-center gap-x-1 capitalize text-foreground-light font-normal">
{x}
{isExposed && <HelpCircle size={14} strokeWidth={1.5} />}
</TooltipTrigger_Shadcn_>
{isExposed && (
<TooltipContent_Shadcn_
side="bottom"
className="w-64 flex flex-col gap-y-1"
>
<p>
Required for{' '}
{relatedFunctions.length === 6
? 'all'
: `the following ${relatedFunctions.length}`}{' '}
functions:
</p>
<div className="max-w-full flex flex-wrap gap-x-0.5 gap-y-1">
{relatedFunctions.map((y) => (
<code key={`${x}_${y}`}>{y}</code>
))}
</div>
</TooltipContent_Shadcn_>
)}
</Tooltip_Shadcn_>
</TableHead>
)
})}
</TableRow>
</TableHeader>
<TableBody className="[&_td]:py-2">
@@ -272,7 +349,7 @@ export const QueueSettings = ({}: QueueSettingsProps) => {
<TableRow key={role.id}>
<TableCell>{role.name}</TableCell>
{ACTIONS.map((x) => (
<TableCell key={x}>
<TableCell key={x} className="text-center">
<Switch
checked={
(privileges[role.name] as Privileges)?.[x as keyof Privileges] ??
@@ -1,3 +1,5 @@
import { useMemo } from 'react'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useFDWsQuery } from 'data/fdw/fdws-query'
@@ -13,6 +15,7 @@ import {
} from 'ui'
import { INTEGRATIONS } from '../Landing/Integrations.constants'
import WrapperRow from './WrapperRow'
import { wrapperMetaComparator } from './Wrappers.utils'
interface WrapperTableProps {
isLatest?: boolean
@@ -28,6 +31,14 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => {
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 (
<p className="text-foreground-light text-sm">
@@ -36,8 +47,6 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => {
)
}
const wrappers = (data ?? []).filter((x) => x.handler === integration.meta.handlerName) || []
return (
<Card className="max-w-5xl">
<CardContent className="p-0 pb-3">
@@ -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',
},
],
},
],
},
]
@@ -84,7 +84,7 @@ export const AdvancedConfiguration = ({ form }: AdvancedConfigurationProps) => {
<>
Postgres with OrioleDB
<Badge color="warning" className="ml-2">
Preview
Alpha
</Badge>
</>
}
@@ -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 (
<div className="border bg-studio border-border rounded-md justify-start items-center flex flex-col w-10/12 relative">
<div className="w-full px-5 py-4 items-center gap-4 inline-flex border-b rounded-t-md">
@@ -17,28 +13,13 @@ const NoChannelEmptyState = () => {
</p>
</div>
</div>
<div
className={cn(
'w-full px-5 py-4 items-center gap-4 inline-flex',
isOrioleDb ? '' : 'rounded-b-md'
)}
>
<div className={cn('w-full px-5 py-4 items-center gap-4 inline-flex')}>
<div className="grow flex-col flex">
<p className="text-foreground">Not sure what to do?</p>
<p className="text-foreground-lighter text-xs">Browse our documentation</p>
</div>
<DocsButton href="https://supabase.com/docs/guides/realtime" />
</div>
{/* [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 && (
<Admonition
type="warning"
className="mb-0 rounded-none border-0 border-t rounded-b-md [&>div]:text-xs"
title="Realtime database changes are not available"
description="This is only a temporary limitation for projects that are using Postgres with OrioleDB."
/>
)}
</div>
)
}
@@ -20,6 +20,7 @@ interface AiMessagePreProps {
className?: string
}
// [DEPRECATED] To delete
export const AiMessagePre = ({ onDiff, children, className }: AiMessagePreProps) => {
const [copied, setCopied] = useState(false)
@@ -21,6 +21,7 @@ interface MessageProps {
action?: React.ReactNode
}
// [DEPRECATED] To delete
const Message = memo(function Message({
name,
role,
@@ -51,6 +51,7 @@ interface AiAssistantPanelProps {
onClose: () => void
}
// [DEPRECATED] To delete
export const AiAssistantPanel = ({
selectedMessage,
existingSql,
@@ -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 =
@@ -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<IStandaloneCodeEditor | null>(null)
const monacoRef = useRef<Monaco | null>(null)
const diffEditorRef = useRef<IStandaloneDiffEditor | null>(null)
const aiPanelRef = useRef<ImperativePanelHandle>(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 && (
<AISchemaSuggestionPopover
onClickSettings={() => {
appSnap.setShowAiSettingsModal(true)
@@ -729,41 +695,6 @@ const SQLEditor = () => {
)}
<ResizablePanel maxSize={70}>
<div className="flex-grow overflow-y-auto border-b h-full">
{!isAssistantV2Enabled && !isAiOpen && (
<motion.button
layoutId="ask-ai-input-icon"
transition={{ duration: 0.1 }}
className="group absolute z-10 rounded-lg right-[24px] top-4 transition-all duration-200 ease-out"
onClick={() => {
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()
}
}}
>
<AiIconAnimation loading={false} allowHoverEffect />
</motion.button>
)}
{isLoading ? (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="animate-spin text-brand" />
@@ -895,28 +826,6 @@ const SQLEditor = () => {
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
{!isAssistantV2Enabled && (
<>
<ResizableHandle withHandle />
<ResizablePanel
ref={aiPanelRef}
collapsible
collapsedSize={0}
minSize={31}
maxSize={40}
onCollapse={() => setIsAiOpen(false)}
onExpand={() => setIsAiOpen(true)}
>
<AiAssistantPanel
existingSql={editorRef.current?.getValue() || ''}
includeSchemaMetadata={includeSchemaMetadata}
onDiff={updateEditorWithCheckForDiff}
onClose={() => aiPanelRef.current?.collapse()}
/>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</>
)
@@ -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 })
}}
@@ -157,9 +157,9 @@ export const Header = forwardRef<HTMLDivElement, HeaderProps>(({ scroll }, ref)
<div className="flex-col justify-start items-start flex">
<div className="text-foreground text-sm flex items-center gap-x-2">
<span>{integration.name}</span>
{integration.beta && (
<Badge variant="warning" className="py-0 px-1.5">
Beta
{integration.status && (
<Badge variant="warning" className="py-0 px-1.5 capitalize">
{integration.status}
</Badge>
)}
</div>
@@ -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: (
@@ -165,7 +165,7 @@ export function LogsSidebarMenuV2() {
items: [],
},
{
name: 'Cron Jobs',
name: 'Cron',
key: 'pg_cron',
url: `/project/${ref}/logs/pgcron-logs`,
items: [],
@@ -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: [],
},
]
@@ -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 (
<div
className={`flex h-12 max-h-12 min-h-12 items-center justify-between py-2 px-5 bg-dash-sidebar ${
className={`flex h-12 max-h-12 min-h-12 items-center bg-dash-sidebar ${
headerBorder ? 'border-b border-default' : ''
}`}
>
<div className="-ml-2 flex items-center text-sm">
{/* Organization is selected */}
{projectRef && (
<>
<OrganizationDropdown />
<div className="flex items-center justify-between py-2 px-3 flex-1">
<div className="flex items-center text-sm">
{/* Organization is selected */}
{projectRef && (
<>
<OrganizationDropdown />
{projectRef && (
{projectRef && (
<>
<span className="text-border-stronger">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M16 3.549L7.12 20.600"></path>
</svg>
</span>
<ProjectDropdown />
{exceedingLimits && (
<div className="ml-2">
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
<Badge variant="destructive">Exceeding usage limits</Badge>
</Link>
</div>
)}
</>
)}
{selectedProject && (
<>
<span className="text-border-stronger">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M16 3.549L7.12 20.600"></path>
</svg>
</span>
{isBranchingEnabled ? <BranchDropdown /> : <EnableBranchingButton />}
</>
)}
</>
)}
{/* Additional breadcrumbs are supplied */}
<BreadcrumbsView defaultValue={breadcrumbs} />
</div>
<div className="flex items-center gap-x-2">
<div className="flex items-center gap-x-2">
{customHeaderComponents && customHeaderComponents}
{IS_PLATFORM && (
<>
<span className="text-border-stronger">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M16 3.549L7.12 20.600"></path>
</svg>
</span>
<ProjectDropdown />
{exceedingLimits && (
<div className="ml-2">
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
<Badge variant="destructive">Exceeding usage limits</Badge>
</Link>
</div>
)}
<FeedbackDropdown />
<NotificationsPopoverV2 />
<HelpPopover />
</>
)}
{selectedProject && (
<>
<span className="text-border-stronger">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M16 3.549L7.12 20.600"></path>
</svg>
</span>
{isBranchingEnabled ? <BranchDropdown /> : <EnableBranchingButton />}
</>
)}
</>
)}
{/* Additional breadcrumbs are supplied */}
<BreadcrumbsView defaultValue={breadcrumbs} />
</div>
<div className="flex items-center gap-x-2">
{customHeaderComponents && customHeaderComponents}
{IS_PLATFORM && (
<>
<FeedbackDropdown />
<NotificationsPopoverV2 />
<HelpPopover />
{isAssistantV2Enabled && !!projectRef && <AssistantButton />}
</>
)}
</div>
</div>
</div>
{!!projectRef && (
<div className="border-l flex-0 h-full">
<AssistantButton />
</div>
)}
</div>
)
}
@@ -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 ? (
<NavigationIconButton
size="tiny"
onClick={() => {
setCommandMenuOpen(true)
snap.setNavigationPanelOpen(false)
}}
type="text"
icon={<Search size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />}
rightText={
<div
className={cn(
'flex items-center gap-1',
'h-6 py-1.5 px-2 leading-none',
'bg-surface-100 text-foreground-lighter',
'border border-default rounded-md',
'shadow-xs shadow-background-surface-100'
)}
>
{os === 'macos' || true ? ( // todo: issue with `os` and hydration fail
<Command size={11.5} strokeWidth={1.5} />
) : (
<p className="text-xs">CTRL</p>
)}
<p className="text-xs">K</p>
</div>
}
>
Search
</NavigationIconButton>
) : (
const CommandButton = (
<HoverCard_Shadcn_ openDelay={10}>
<HoverCardTrigger_Shadcn_ asChild>
<NavigationIconButton
@@ -1,9 +1,8 @@
import { useParams } from 'common'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect } from 'react'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useParams } from 'common'
import ProjectAPIDocs from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs'
import { AiAssistantPanel } from 'components/ui/AIAssistantPanel/AIAssistantPanel'
import AISettingsModal from 'components/ui/AISettingsModal'
@@ -96,7 +95,6 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
const { open } = aiAssistantPanel
const navLayoutV2 = useFlag('navigationLayoutV2')
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const projectName = selectedProject?.name
const organizationName = selectedOrganization?.name
@@ -120,10 +118,10 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
e.stopPropagation()
}
}
if (isAssistantV2Enabled) window.addEventListener('keydown', handler)
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAssistantV2Enabled, open])
}, [open])
return (
<AppLayout>
@@ -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)
})
})
@@ -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&nbsp;\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) => {
@@ -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(',
]
@@ -35,10 +35,11 @@ export const Message = function Message({
return (
<motion.div
layout="position"
initial={{ y: 5, opacity: 0, scale: 0.99 }}
animate={{ y: 0, opacity: 1, scale: 1 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className={cn(
'mb-5 text-foreground-light text-sm',
isUser && 'text-foreground',
variant === 'warning' && 'bg-warning-200'
)}
>
@@ -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<any[]>()
const [error, setError] = useState<QueryResponseError>()
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 (
<div className="overflow-hidden rounded border w-auto">
<div className="overflow-hidden rounded border w-auto bg-surface-100">
<div className={cn('flex items-center gap-2', showWarning ? '' : 'px-3 pr-1 py-1')}>
{showWarning ? (
{!!showWarning ? (
<Admonition type="warning" className="mb-0 rounded-none border-0">
<p>This query contains write operations. Are you sure you want to execute it?</p>
<p>
{showWarning === 'hasWriteOperation'
? 'This query contains write operations.'
: 'This query involves running a function.'}{' '}
Are you sure you want to execute it?
</p>
<div className="flex justify-stretch mt-2 gap-2">
<Button
type="outline"
size="tiny"
className="w-full flex-1"
onClick={() => setShowWarning(false)}
onClick={() => setShowWarning(undefined)}
>
Cancel
</Button>
@@ -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,
@@ -38,7 +38,7 @@ const ProductMenuItem = ({
>
<span className="truncate">{name} </span>
{label !== undefined && (
<Badge variant="warning" className="py-0 px-1.5">
<Badge variant="warning" className="py-0 px-1.5 capitalize">
{label}
</Badge>
)}
-1
View File
@@ -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',
+5
View File
@@ -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
? [
{
+1
View File
@@ -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",
+10 -5
View File
@@ -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 }),
@@ -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<string>()
const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false)
@@ -70,7 +70,7 @@ const PITR = () => {
<Admonition
type="default"
title="Database backups are not available for OrioleDB"
description="OrioleDB is currently in preview and projects created are strictly ephemeral with no database backups"
description="OrioleDB is currently in public alpha and projects created are strictly ephemeral with no database backups"
>
<DocsButton abbrev={false} className="mt-2" href="https://supabase.com/docs" />
</Admonition>
@@ -121,7 +121,7 @@ const RestoreToNewProject = () => {
<Admonition
type="default"
title="Restoring to new projects are not available for OrioleDB"
description="OrioleDB is currently in preview and projects created are strictly ephemeral with no database backups"
description="OrioleDB is currently in public alpha and projects created are strictly ephemeral with no database backups"
>
<DocsButton abbrev={false} className="mt-2" href="https://supabase.com/docs" />
</Admonition>
@@ -42,7 +42,7 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => {
<Admonition
type="default"
title="Database backups are not available for OrioleDB"
description="OrioleDB is currently in preview and projects created are strictly ephemeral with no database backups"
description="OrioleDB is currently in public alpha and projects created are strictly ephemeral with no database backups"
>
<DocsButton abbrev={false} className="mt-2" href="https://supabase.com/docs" />
</Admonition>
+1 -1
View File
@@ -62,7 +62,7 @@ const Home: NextPageWithLayout = () => {
<TooltipContent_Shadcn_ side="bottom" align="start" className="max-w-80 text-center">
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 */}
<a
target="_blank"
rel="noreferrer noopener"
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 525.8 535.73"><defs><style>.cls-2{fill:#006bff}.cls-3{fill:#0ae9ef}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Logo_assets" data-name="Logo assets"><g id="Brand_mark" data-name="Brand mark"><path d="m443.74 337.62-27.16 47.05a139.52 139.52 0 0 1-120.82 69.75h-54.33a139.52 139.52 0 0 1-120.82-69.75l-27.16-47.05a139.52 139.52 0 0 1 0-139.51l27.16-47.05A139.53 139.53 0 0 1 241.43 81.3h54.33a139.53 139.53 0 0 1 120.82 69.76l27.16 47.05a139.23 139.23 0 0 1 8.55 17.55c0 .12.09.23.13.35a102.15 102.15 0 0 0 44.33-18.24c0-.14-.08-.28-.13-.43a237.8 237.8 0 0 0-33.29-67.58 240.67 240.67 0 0 0-52-53.48 239.3 239.3 0 0 0-312.68 360.8 239.43 239.43 0 0 0 398-98.69c.05-.15.09-.29.13-.43a102.15 102.15 0 0 0-44.33-18.24c0 .12-.09.23-.13.35a139.23 139.23 0 0 1-8.58 17.55Z" style="fill:none"/><path class="cls-2" d="M360.4 347.4c-17 15.09-38.21 33.87-76.78 33.87h-23c-27.88 0-53.23-10.12-71.37-28.49-17.72-17.94-27.48-42.5-27.48-69.16v-31.51c0-26.66 9.76-51.22 27.48-69.16 18.14-18.37 43.49-28.49 71.37-28.49h23c38.57 0 59.76 18.78 76.78 33.87 17.65 15.65 32.9 29.16 73.52 29.16a116.05 116.05 0 0 0 18.5-1.48c0-.12-.08-.23-.13-.35a139.23 139.23 0 0 0-8.55-17.55l-27.16-47.05A139.53 139.53 0 0 0 295.76 81.3h-54.33a139.53 139.53 0 0 0-120.82 69.76l-27.16 47.05a139.52 139.52 0 0 0 0 139.51l27.16 47.05a139.52 139.52 0 0 0 120.82 69.75h54.33a139.52 139.52 0 0 0 120.82-69.75l27.16-47.05a139.23 139.23 0 0 0 8.55-17.55c0-.12.09-.23.13-.35a116.05 116.05 0 0 0-18.5-1.48c-40.62 0-55.87 13.51-73.52 29.16Z"/><path class="cls-2" d="M283.62 183h-23c-42.42 0-70.3 30.3-70.3 69.09v31.51c0 38.79 27.88 69.09 70.3 69.09h23c61.82 0 57-63 150.3-63a144.19 144.19 0 0 1 26.37 2.41 139.36 139.36 0 0 0 0-48.46 143.32 143.32 0 0 1-26.37 2.42c-93.33-.01-88.48-63.06-150.3-63.06Z"/><path class="cls-2" d="M513.91 315.13a130.21 130.21 0 0 0-53.62-23c0 .16-.05.32-.08.47a138.46 138.46 0 0 1-7.79 27.16A102.15 102.15 0 0 1 496.75 338c0 .14-.08.28-.13.43A237.8 237.8 0 0 1 463.33 406a240.67 240.67 0 0 1-52 53.48A239.3 239.3 0 0 1 98.65 98.65a239.43 239.43 0 0 1 398 98.69c.05.15.09.29.13.43A102.15 102.15 0 0 1 452.42 216a139.36 139.36 0 0 1 7.8 27.18c0 .15 0 .3.07.44a129.94 129.94 0 0 0 53.62-23c15.29-11.31 12.33-24.09 10-31.65C490.22 79.52 388.33 0 267.86 0 119.93 0 0 119.93 0 267.86s119.93 267.87 267.86 267.87c120.47 0 222.36-79.52 256-188.94 2.38-7.56 5.34-20.34-9.95-31.66Z"/><path class="cls-3" d="M452.42 216a116.05 116.05 0 0 1-18.5 1.48c-40.62 0-55.87-13.51-73.52-29.16-17-15.09-38.21-33.87-76.78-33.87h-23c-27.88 0-53.23 10.12-71.37 28.49-17.72 17.94-27.48 42.5-27.48 69.16v31.51c0 26.66 9.76 51.22 27.48 69.16 18.14 18.37 43.49 28.49 71.37 28.49h23c38.57 0 59.76-18.78 76.78-33.87 17.65-15.65 32.9-29.16 73.52-29.16a116.05 116.05 0 0 1 18.5 1.48 138.46 138.46 0 0 0 7.79-27.16c0-.15.06-.31.08-.47a144.19 144.19 0 0 0-26.37-2.41c-93.33 0-88.48 63-150.3 63h-23c-42.42 0-70.3-30.3-70.3-69.09v-31.47c0-38.79 27.88-69.09 70.3-69.09h23c61.82 0 57 63 150.3 63a143.32 143.32 0 0 0 26.37-2.42c0-.14 0-.29-.07-.44a139.36 139.36 0 0 0-7.8-27.16Z"/><path class="cls-3" d="M452.42 216a116.05 116.05 0 0 1-18.5 1.48c-40.62 0-55.87-13.51-73.52-29.16-17-15.09-38.21-33.87-76.78-33.87h-23c-27.88 0-53.23 10.12-71.37 28.49-17.72 17.94-27.48 42.5-27.48 69.16v31.51c0 26.66 9.76 51.22 27.48 69.16 18.14 18.37 43.49 28.49 71.37 28.49h23c38.57 0 59.76-18.78 76.78-33.87 17.65-15.65 32.9-29.16 73.52-29.16a116.05 116.05 0 0 1 18.5 1.48 138.46 138.46 0 0 0 7.79-27.16c0-.15.06-.31.08-.47a144.19 144.19 0 0 0-26.37-2.41c-93.33 0-88.48 63-150.3 63h-23c-42.42 0-70.3-30.3-70.3-69.09v-31.47c0-38.79 27.88-69.09 70.3-69.09h23c61.82 0 57 63 150.3 63a143.32 143.32 0 0 0 26.37-2.42c0-.14 0-.29-.07-.44a139.36 139.36 0 0 0-7.8-27.16Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -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.
@@ -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.
<Img
alt="orioledb-project-create"
src="/images/blog/2024-12-01-orioledb-release/orioledb-project-create.png"
captionAlign="left"
zoomable={false}
/>
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).
+70
View File
@@ -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<DotGridProps> = ({ 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 (
<div className="relative w-full h-full min-h-[600px]">
<motion.div
className="grid h-full md:w-full aspect-square justify-between items-space-between items-start"
style={{
gridTemplateColumns: `repeat(${columns}, 1px)`,
gridTemplateRows: `repeat(${rows}, 1fr)`,
rowGap: 'auto',
}}
variants={container}
initial="hidden"
animate="visible"
aria-label={`Grid of ${rows * columns} dots, ${count} highlighted`}
>
{Array.from({ length: rows * columns }).map((_, index) => {
const isHighlighted = index < count
return (
<motion.div
key={index}
variants={isHighlighted ? highlightedVariants : item}
className="w-[2px] h-[2px] dark:w-[1px] dark:h-[1px] rounded-full bg-foreground-lighter"
/>
)
})}
<div className="absolute bg-gradient-to-b lg:bg-gradient-to-r inset-0 w-full h-full from-background to-transparent to-40%" />
<div className="absolute bg-gradient-to-t lg:bg-gradient-to-l inset-0 w-full h-full from-background to-transparent to-20%" />
</motion.div>
</div>
)
}
export default DotGrid
+215
View File
@@ -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<DemoMessage[]>([])
const [isComplete, setIsComplete] = useState(false)
const [pendingMessages, setPendingMessages] = useState<DemoMessage[]>([])
const [input, setInput] = useState('')
const [showFade, setShowFade] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollContainerRef = useRef<HTMLDivElement>(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: (
<div className="rounded-md overflow-hidden relative">
<Image
src={errorGif}
alt="Try the assistant in your dashboard for real results"
className="w-full grayscale"
/>
</div>
),
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 (
<div className="w-full shadow-2xl relative h-full bg-surface-75 border border-text-light/25 rounded-md overflow-hidden">
<div className="absolute inset-0 flex flex-col">
{/* Header */}
<div className="z-30 border-b border-b-muted flex items-center gap-x-3 px-5 h-[46px]">
<AiIconAnimation loading={false} allowHoverEffect />
<div className="text-sm flex-1">Assistant</div>
</div>
{/* Messages */}
<div
ref={scrollContainerRef}
className="flex-1 overflow-y-auto p-5 flex flex-col [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"
>
{messages.map((message, index) => (
<>
<motion.div
key={index}
layout="position"
initial={{ y: 5, opacity: 0, scale: 0.99 }}
animate={{ y: 0, opacity: 1, scale: 1 }}
className={cn(
'text-foreground-light text-sm mb-6',
message.role === 'user'
? 'px-5 py-3 rounded-lg bg-background-muted/80 w-fit self-end max-w-64'
: ''
)}
>
{message.content}
{message.render && <div className="mt-4">{message.render}</div>}
</motion.div>
</>
))}
{messages.length > 0 && messages[messages.length - 1].role === 'user' && (
<motion.div className="text-foreground-lighter text-sm flex gap-1.5 items-center">
<span>Thinking</span>
<div className="flex gap-1">
<motion.span
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 1.5, repeat: Infinity, delay: 0 }}
>
.
</motion.span>
<motion.span
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 1.5, repeat: Infinity, delay: 0.3 }}
>
.
</motion.span>
<motion.span
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 1.5, repeat: Infinity, delay: 0.6 }}
>
.
</motion.span>
</div>
</motion.div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<AnimatePresence>
{showFade && (
<motion.div
initial={{ opacity: 0 }}
exit={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="pointer-events-none z-10 -mt-24"
>
<div className="h-24 w-full bg-gradient-to-t from-background muted to-transparent" />
</motion.div>
)}
</AnimatePresence>
<div className="p-5 pt-0 relative z-10">
<form onSubmit={handleSubmit} className={cn('rounded-md', styles['border-gradient'])}>
<Input
type="text"
value={input}
onChange={(e) => 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'
)}
/>
</form>
</div>
</div>
</div>
)
}
+236
View File
@@ -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<any>(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 (
<div className="px-0 py-5 border-t border-t-muted">
<ChartContainer config={{}} className="aspect-auto h-[250px] w-full">
<BarChart
data={results}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="name"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={16}
padding={{ left: 24, right: 24 }}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent className="w-[150px]" />}
/>
<Bar dataKey="value" fill="hsl(var(--brand-default))" />
</BarChart>
</ChartContainer>
</div>
)
}
return (
<>
<div className={cn(showResults ? 'h-auto max-h-64 overflow-auto border-t' : 'h-0')}>
<table className="w-full m-0 p-0">
<thead>
<tr className="bg-surface-100 border-b">
{results?.length > 0 &&
Object.keys(results[0]).map((key) => (
<th key={key} className="p-2 px-5 text-left text-xs font-mono">
{key}
</th>
))}
</tr>
</thead>
<tbody>
{results?.map((row: any, i: number) => (
<tr key={i} className="border-b">
{Object.values(row).map((value: any, j: number) => (
<td key={j} className="p-2 px-5 text-left text-xs font-mono">
{value === null
? 'NULL'
: typeof value === 'object'
? JSON.stringify(value)
: String(value)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="flex items-center justify-between bg-surface-100 py-2 px-4">
<p className="text-xs text-foreground-light">
{results.length} rows
{results.length >= 100 && ' (Limited to only 100 rows)'}
</p>
</div>
</>
)
}
return (
<div className="my-3 mt-2 border rounded bg-surface-100 overflow-hidden">
<div className="">
<div className="flex items-center gap-2 px-4 py-1">
<SQL_ICON
className={cn(
'transition-colors',
'fill-foreground-muted',
'group-aria-selected:fill-foreground',
'w-5 h-5 shrink-0',
'-ml-0.5'
)}
size={16}
strokeWidth={1.5}
/>
<h3 className="text-xs font-medium flex-1">{title}</h3>
<div className="flex">
<Button
type="text"
size="tiny"
className="w-7 h-7"
icon={<Code size={14} />}
onClick={() => setShowCode(!showCode)}
/>
<Button
type="text"
size="tiny"
className="w-7 h-7"
icon={<Play size={14} />}
loading={isExecuting}
onClick={handleExecute}
/>
</div>
</div>
{showCode && (
<div className="border-t">
<CodeBlock className="border-0 rounded-none" showLineNumbers lang="sql">
{sql}
</CodeBlock>
</div>
)}
{isExecuting ? (
<motion.div className="text-foreground-lighter text-sm flex gap-1.5 items-center p-5">
<span>Executing query</span>
<div className="flex gap-1">
<motion.span
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 1.5, repeat: Infinity, delay: 0 }}
>
.
</motion.span>
<motion.span
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 1.5, repeat: Infinity, delay: 0.3 }}
>
.
</motion.span>
<motion.span
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 1.5, repeat: Infinity, delay: 0.6 }}
>
.
</motion.span>
</div>
</motion.div>
) : showResults && results ? (
<ResultsDisplay />
) : null}
</div>
</div>
)
}
@@ -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: '<angle>';
initial-value: 0deg;
inherits: false;
}
@property --color1 {
syntax: '<color>';
initial-value: #0a5c36;
inherits: false;
}
@property --color2 {
syntax: '<color>';
initial-value: #2ecc71;
inherits: false;
}
@property --color3 {
syntax: '<color>';
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;
}
}
+88
View File
@@ -0,0 +1,88 @@
export const JsIcon = (props: any) => (
<svg
width="24"
height="24"
viewBox="0 0 55 55"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.730469 0.807739H54.2695V54.3468H0.730469V0.807739ZM41.6588 45.6917C39.1773 45.6917 37.7743 44.3974 36.6959 42.6365L32.6073 45.0118C34.0843 47.9301 37.1029 50.1566 41.7753 50.1566C46.5538 50.1566 50.1121 47.6752 50.1121 43.1456C50.1121 38.944 47.6986 37.0753 43.4239 35.2422L42.1662 34.7034C40.0076 33.7686 39.0728 33.1576 39.0728 31.6483C39.0728 30.4271 40.0068 29.4923 41.4804 29.4923C42.9251 29.4923 43.8556 30.1016 44.7182 31.6483L48.6359 29.1328C46.9788 26.2179 44.6791 25.1046 41.4804 25.1046C36.9873 25.1046 34.1124 27.977 34.1124 31.7503C34.1124 35.8464 36.5242 37.784 40.1546 39.3307L41.4124 39.8703C43.7069 40.874 45.0751 41.485 45.0751 43.2102C45.0751 44.6498 43.7435 45.6917 41.6588 45.6917ZM22.1613 45.6602C20.4327 45.6602 19.7138 44.4747 18.9234 43.0725L14.8281 45.5519C16.0145 48.0627 18.3473 50.1473 22.3754 50.1473C26.8336 50.1473 29.8879 47.7763 29.8879 42.5668V25.3919H24.8569V42.4989C24.8569 45.0135 23.8142 45.6602 22.1613 45.6602Z"
fill="currentColor"
/>
</svg>
)
export const FlutterIcon = (props: any) => (
<svg
width="59"
height="58"
viewBox="0 0 59 58"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M42.7859 8.69144L36.306 2.21152C35.4475 1.34882 33.6544 0.316956 32.1405 0.316956C30.838 0.316956 29.5608 0.57915 28.732 1.07394L13.8787 8.50055H40.9419C41.5663 8.50055 42.184 8.56542 42.7859 8.69144Z"
fill="currentColor"
/>
<path
d="M9.28906 12.8012L1.46792 29.095C1.14229 29.7843 0.710938 30.9811 0.710938 31.7465C0.710938 33.4 1.43832 35.0916 2.60551 36.2926L9.30112 42.9383C9.2931 42.7955 9.28906 42.6522 9.28906 42.5084V12.8012Z"
fill="currentColor"
/>
<path
d="M16.6841 50.367L24.1985 57.8814H48.0582V50.4088H17.5245C17.2426 50.4088 16.9622 50.3947 16.6841 50.367Z"
fill="currentColor"
/>
<path
d="M50.1358 47.6558H58.2838V24.943L45.8917 12.5508C44.5789 11.238 42.7985 10.5005 40.9419 10.5005H12.7467L50.1358 47.6558Z"
fill="currentColor"
/>
<path
d="M11.2891 11.8716V42.5084C11.2891 44.0104 11.8857 45.4509 12.9478 46.513C14.1617 47.7268 15.8079 48.4088 17.5245 48.4088H48.0561L11.2891 11.8716Z"
fill="currentColor"
/>
</svg>
)
export const SwiftIcon = (props: any) => (
<svg
width="24"
height="24"
viewBox="0 0 58 57"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M57.3407 14.0476V42.6821C57.3407 43.9642 57.2338 45.1929 57.0201 46.4217C56.8064 47.6504 56.4325 48.8257 55.8448 49.9475C55.2572 51.016 54.5627 52.031 53.6545 52.9392C52.7997 53.8474 51.7847 54.5419 50.6628 55.1295C49.541 55.7172 48.3657 56.0912 47.1369 56.3048C45.9256 56.5155 44.6623 56.5704 43.4495 56.6231L43.3974 56.6254H14.7094C13.4273 56.6254 12.1986 56.5185 10.9698 56.3048C9.74112 56.0912 8.56582 55.7172 7.44394 55.1295C6.37549 54.5419 5.36046 53.8474 4.45228 52.9392C3.54409 52.0845 2.8496 51.0694 2.26195 49.9475C1.6743 48.8257 1.30034 47.6504 1.08665 46.4217C0.875966 45.2102 0.82107 43.9468 0.768367 42.7339L0.766113 42.6821V13.9941C0.819536 12.712 0.872962 11.4833 1.08665 10.2545C1.30034 9.02583 1.6743 7.85052 2.26195 6.72865C2.8496 5.6602 3.54409 4.64516 4.45228 3.73698C4.55912 3.63013 4.67932 3.52329 4.79952 3.41645C4.91972 3.30961 5.03992 3.20276 5.14677 3.09592C5.89468 2.50827 6.6426 1.97405 7.49737 1.54667C7.63092 1.49324 7.77784 1.42646 7.92475 1.35968C8.07166 1.2929 8.21857 1.22612 8.35212 1.1727C9.20689 0.852165 10.1151 0.585055 11.0233 0.424787C11.9062 0.268965 12.8397 0.214142 13.7255 0.162122C13.7508 0.160638 13.776 0.159156 13.8012 0.157674C14.0683 0.104251 14.3889 0.104248 14.7094 0.104248H43.3974C44.6795 0.104248 45.9082 0.211097 47.1369 0.424787C48.3657 0.638478 49.541 1.01243 50.6628 1.60007C51.7313 2.18772 52.7463 2.88222 53.6545 3.79041C54.5627 4.64517 55.2572 5.6602 55.8448 6.78208C56.4325 7.90395 56.8064 9.07925 57.0201 10.308C57.2308 11.5194 57.2857 12.7827 57.3384 13.9956L57.3407 14.0476ZM45.6946 34.4016L45.5343 35.0426C50.4492 41.026 49.1136 47.4367 48.3657 46.3148C45.8014 41.3999 41.1002 42.6287 38.6962 43.8574C38.5894 43.9108 38.4825 43.9776 38.3757 44.0444C38.2688 44.1111 38.162 44.1779 38.0551 44.2313C38.0551 44.2848 38.0017 44.2848 38.0017 44.2848C33.0334 46.9559 26.3022 47.1162 19.5709 44.2313C13.9081 41.7739 9.26034 37.6069 6.26868 32.7989C7.81793 33.9208 9.47404 34.9358 11.237 35.7371C18.3422 39.1028 25.5008 38.8357 30.576 35.7371C23.3639 30.1812 17.3272 22.9691 12.7328 17.0926C11.8781 16.0776 11.0767 14.9557 10.3822 13.8339C15.9382 18.8556 24.6461 25.2129 27.798 26.9758C21.1736 19.9775 15.2971 11.323 15.5642 11.5901C26.035 22.1144 35.7045 28.0977 35.7045 28.0977C36.0785 28.258 36.3456 28.4182 36.5593 28.5785C36.773 28.0443 36.9333 27.51 37.0935 26.9758C38.7496 20.8856 36.8798 13.8873 32.606 8.11765C42.3289 13.9407 48.0452 25.0526 45.6946 34.4016Z"
fill="currentColor"
/>
</svg>
)
export const PythonIcon = (props: any) => (
<svg
width="24"
height="24"
viewBox="0 0 56 57"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.2861 1.17086C23.2052 0.83128 25.3862 0.636497 27.6701 0.625885C29.954 0.615273 32.3353 0.788831 34.6303 1.17086C38.2558 1.77512 41.3102 4.49488 41.3102 8.11546V20.8368C41.3102 24.5673 38.3448 27.6257 34.6303 27.6257H21.2861C16.7554 27.6257 12.9401 31.5154 12.9401 35.925V42.0287H8.3467C4.46398 42.0287 2.19606 39.2106 1.2464 35.2554C-0.034647 29.9419 0.0197613 26.7664 1.2464 21.6777C2.30982 17.2381 5.70965 14.9043 9.59237 14.9043H27.966V13.2071H14.6062V8.11546C14.6062 4.26017 15.6326 2.16964 21.2861 1.17086ZM22.9521 7.27463C22.9521 5.86635 21.8252 4.72101 20.4452 4.72101C19.0603 4.72101 17.9383 5.86635 17.9383 7.27463C17.9383 8.67792 19.0603 9.81268 20.4452 9.81268C21.8252 9.81268 22.9521 8.67792 22.9521 7.27463Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M42.9762 20.8371V14.9046H47.99C51.8777 14.9046 53.7103 17.8126 54.6699 21.6779C56.0053 27.0463 56.0647 31.0708 54.6699 35.2556C53.3196 39.3207 51.8727 42.029 47.99 42.029H27.9659V43.7262H41.3101V48.8178C41.3101 52.6731 37.9936 54.6329 34.6302 55.6067C29.5703 57.0749 25.515 56.8502 21.286 55.6067C17.7545 54.568 14.6061 52.4384 14.6061 48.8178V36.0965C14.6061 32.4359 17.6308 29.3076 21.286 29.3076H34.6302C39.0768 29.3076 42.9762 25.4364 42.9762 20.8371ZM37.9779 49.6587C37.9779 48.2554 36.856 47.1206 35.471 47.1206C34.0911 47.1206 32.9641 48.2554 32.9641 49.6587C32.9641 51.0669 34.0911 52.2123 35.471 52.2123C36.856 52.2123 37.9779 51.0669 37.9779 49.6587Z"
fill="currentColor"
/>
</svg>
)
+415 -27
View File
@@ -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,
},
},
]
+773
View File
@@ -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: (
<div className="flex gap-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 40 40"
className="w-6 h-6 shrink-0"
>
<g fill="none" fill-rule="evenodd">
<path
fill="#FF6154"
d="M40 20c0 11.046-8.954 20-20 20S0 31.046 0 20 8.954 0 20 0s20 8.954 20 20"
></path>
<path
fill="#FFF"
d="M22.667 20H17v-6h5.667a3 3 0 0 1 0 6m0-10H13v20h4v-6h5.667a7 7 0 1 0 0-14"
></path>
</g>
</svg>
<p>
Welcome Product Hunter! 👋 Thanks for checking out the Supabase assistant. Let me show you
what I can do!
</p>
</div>
),
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: (
<SqlSnippet
id="user-growth"
title="Monthly User Signups"
resultType="chart"
sql={`SELECT
date_trunc('month', created_at) as month,
count(*) as new_users
FROM users
GROUP BY month
ORDER BY month
LIMIT 12;`}
mockData={[
{ name: 'Jan 2023', value: 245 },
{ name: 'Feb 2023', value: 312 },
{ name: 'Mar 2023', value: 287 },
{ name: 'Apr 2023', value: 401 },
{ name: 'May 2023', value: 352 },
{ name: 'Jun 2023', value: 489 },
{ name: 'Jul 2023', value: 567 },
{ name: 'Aug 2023', value: 623 },
{ name: 'Sep 2023', value: 712 },
{ name: 'Oct 2023', value: 812 },
{ name: 'Nov 2023', value: 934 },
{ name: 'Dec 2023', value: 1023 },
]}
/>
),
},
],
},
{
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: (
<SqlSnippet
id="latest-products"
title="Latest Products"
sql={`SELECT
product_id,
name,
price,
category,
created_at,
stock_level
FROM products
ORDER BY created_at DESC
LIMIT 5;`}
mockData={{
rows: [
{
product_id: 'PROD-001',
name: 'Wireless Earbuds Pro',
price: 199.99,
category: 'Electronics',
created_at: '2024-01-15T16:30:00Z',
stock_level: 45,
},
{
product_id: 'PROD-002',
name: 'Smart Home Hub',
price: 299.99,
category: 'Smart Home',
created_at: '2024-01-15T15:45:00Z',
stock_level: 32,
},
{
product_id: 'PROD-003',
name: 'Fitness Tracker Elite',
price: 149.99,
category: 'Wearables',
created_at: '2024-01-15T14:20:00Z',
stock_level: 78,
},
],
}}
/>
),
},
],
},
{
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: (
<SqlSnippet
id="create-function"
title="Create User Points Function"
sql={`CREATE OR REPLACE FUNCTION calculate_user_points(user_id UUID)
RETURNS INTEGER AS $$
DECLARE
total_points INTEGER;
BEGIN
SELECT
COALESCE(
(SELECT COUNT(*) * 10 FROM posts WHERE author_id = user_id) + -- 10 points per post
(SELECT COUNT(*) * 5 FROM comments WHERE user_id = user_id) + -- 5 points per comment
(SELECT COUNT(*) * 2 FROM post_likes WHERE user_id = user_id), -- 2 points per like
0
) INTO total_points;
RETURN total_points;
END;
$$ LANGUAGE plpgsql;`}
/>
),
},
],
},
{
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: (
<SqlSnippet
id="rls-policy"
title="Team Members RLS Policy"
sql={`-- Enable RLS on the table
ALTER TABLE team_members ENABLE ROW LEVEL SECURITY;
-- Create policy for viewing team members
CREATE POLICY view_team_members ON team_members
FOR SELECT
USING (
team_id IN (
-- User can see members of teams they belong to
SELECT team_id
FROM team_members
WHERE user_id = auth.uid()
)
OR
-- Team admins can see all members
EXISTS (
SELECT 1
FROM team_admins
WHERE user_id = auth.uid()
AND team_id = team_members.team_id
)
);`}
/>
),
},
],
},
{
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: (
<SqlSnippet
id="projects-sql"
title="Get All Projects SQL"
sql={`SELECT
p.id,
p.name,
p.description,
p.created_at,
u.email as owner_email
FROM projects p
JOIN users u ON p.owner_id = u.id
WHERE p.is_active = true
ORDER BY p.created_at DESC;`}
mockData={{
rows: [
{
id: 1,
name: 'Project A',
description: 'First project',
created_at: '2024-01-15T10:00:00Z',
owner_email: 'user@example.com',
},
{
id: 2,
name: 'Project B',
description: 'Second project',
created_at: '2024-01-14T10:00:00Z',
owner_email: 'user2@example.com',
},
],
}}
/>
),
},
{
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: (
<SqlSnippet
id="projects-js"
title="Get All Projects with Supabase Client"
sql={`const { data: projects, error } = await supabase
.from('projects')
.select(\`
id,
name,
description,
created_at,
owner_id (
email
)
\`)
.eq('is_active', true)
.order('created_at', { ascending: false })`}
/>
),
},
],
},
{
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: (
<SqlSnippet
id="recent-orders"
title="Recent Orders"
sql={`SELECT
order_id,
customer_name,
amount,
status,
created_at
FROM orders
ORDER BY created_at DESC
LIMIT 5;`}
mockData={{
rows: [
{
order_id: 'ORD-001',
customer_name: 'John Smith',
amount: 299.99,
status: 'processing',
created_at: '2024-01-15T14:22:31Z',
},
{
order_id: 'ORD-002',
customer_name: 'Sarah Johnson',
amount: 149.5,
status: 'shipped',
created_at: '2024-01-15T13:45:12Z',
},
{
order_id: 'ORD-003',
customer_name: 'Mike Wilson',
amount: 499.99,
status: 'completed',
created_at: '2024-01-15T12:30:45Z',
},
],
}}
/>
),
},
],
},
{
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: (
<SqlSnippet
id="category-revenue"
title="Revenue by Category"
resultType="chart"
sql={`SELECT
category_name,
sum(order_amount) as revenue
FROM orders o
JOIN products p ON o.product_id = p.id
GROUP BY category_name
ORDER BY revenue DESC;`}
mockData={[
{ name: 'Electronics', value: 125000 },
{ name: 'Clothing', value: 98000 },
{ name: 'Home & Garden', value: 87000 },
{ name: 'Sports', value: 76000 },
{ name: 'Books', value: 65000 },
{ name: 'Toys', value: 54000 },
{ name: 'Beauty', value: 43000 },
{ name: 'Automotive', value: 32000 },
{ name: 'Food', value: 21000 },
{ name: 'Art', value: 15000 },
]}
/>
),
},
],
},
{
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: (
<SqlSnippet
id="active-users"
title="Active Users"
sql={`SELECT
username,
last_login,
session_count
FROM users
WHERE last_active > 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: (
<SqlSnippet
id="page-views"
title="Daily Page Views"
resultType="chart"
sql={`SELECT
date_trunc('day', timestamp) as day,
count(*) as views
FROM page_views
WHERE timestamp > 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: (
<SqlSnippet
id="error-distribution"
title="Error Distribution"
resultType="chart"
sql={`SELECT
error_type,
count(*) as error_count
FROM error_logs
GROUP BY error_type
ORDER BY error_count DESC;`}
mockData={[
{ name: '404 Not Found', value: 1234 },
{ name: '500 Server Error', value: 567 },
{ name: '403 Forbidden', value: 456 },
{ name: 'Rate Limit Exceeded', value: 345 },
{ name: 'Database Timeout', value: 234 },
{ name: 'Authentication Failed', value: 123 },
{ name: 'Invalid Input', value: 98 },
{ name: 'Network Error', value: 87 },
{ name: 'API Error', value: 76 },
{ name: 'Other', value: 45 },
]}
/>
),
},
],
},
{
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: (
<SqlSnippet
id="api-usage"
title="API Usage Trends"
resultType="chart"
sql={`SELECT
date_trunc('hour', timestamp) as hour,
count(*) as request_count
FROM api_logs
WHERE timestamp > 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<Message[]>([])
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 (
<>
<NextSeo
title={meta_title}
description={meta_description}
openGraph={{
title: meta_title,
description: meta_description,
url: `https://supabase.com/ai`,
}}
/>
<DefaultLayout className="lg:h-screen min-h-screen overflow-hidden">
<SectionContainer className="h-full lg:pt-6 relative">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 1, duration: 5 } }}
className="absolute inset-0 hidden lg:block left-1/3"
>
<DotGrid rows={30} columns={30} count={900} />
</motion.div>
<div className="h-full grid grid-cols-12 gap-8 items-center">
{/* Left Column */}
<div className="col-span-12 lg:col-span-6 relative z-10">
{/* Main content */}
<div className="mb-6 flex flex-col gap-4 max-w-lg">
<h1 className="text-4xl sm:text-5xl sm:leading-none">Chat with Postgres</h1>
<p className="p text-lg !m-0">
Generate, run and debug queries, chart your data, create functions, policies and
more. The assistant is here to help.
</p>
<div className="min-h-12">
{!isUserLoading &&
(isLoggedIn ? (
<Button type="primary" size="medium" asChild>
<Link href="/dashboard/projects">Dashboard</Link>
</Button>
) : (
<Button type="primary" size="medium" asChild>
<Link href="https://supabase.com/dashboard">Start your project</Link>
</Button>
))}
</div>
</div>
{/* Grid of secondary buttons */}
<div>
<h2 className="font-mono text-foreground-lighter text-sm mb-4">Try me</h2>
<div className="flex flex-wrap items-start gap-2 min-h-24">
<motion.div
initial="hidden"
animate="visible"
variants={{
visible: {
transition: {
staggerChildren: 0.05,
},
},
}}
className="flex flex-wrap gap-2"
>
{demoQueries.map((query, i) => (
<motion.div
key={i}
variants={{
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0 },
}}
>
<Button
className="rounded-full"
type="default"
onClick={() => handleNewMessage(query.messages)}
>
{query.label}
</Button>
</motion.div>
))}
</motion.div>
</div>
</div>
</div>
{/* Right Column */}
<motion.div className="col-span-12 relative lg:col-span-6 h-[500px] lg:h-full justify-center flex items-center">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 1, duration: 5 } }}
className="absolute w-screen bottom-0 -top-28 -inset-x-[calc(((100vw-480px)/2)+24px)] sm:-inset-x-[calc(((100vw-640px)/2)+24px)] md:-inset-x-[calc(((100vw-768px)/2)+24px)] lg:hidden"
>
<DotGrid rows={30} columns={30} count={900} />
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 1.04, filter: 'blur(5px)' }}
animate={{
opacity: 1,
scale: 1,
filter: 'blur(0px)',
transition: { duration: 1.5, ease: EASE_OUT, delay: 0.5 },
}}
className="w-full lg:max-w-[400px] h-full max-h-[700px] relative overflow-hidden"
>
<motion.div
initial={{ x: '-100%' }}
animate={{ x: '200%' }}
transition={{
duration: 0.5,
ease: 'easeInOut',
delay: 0.5,
}}
className="absolute inset-0 bg-gradient-to-r from-transparent via-background-muted/40 to-transparent transform -skew-x-12 z-10"
/>
<AIDemoPanel incomingMessages={incomingMessages} />
</motion.div>
</motion.div>
</div>
</SectionContainer>
</DefaultLayout>
</>
)
}
export default Assistant
+1 -1
View File
@@ -131,7 +131,7 @@ function FeaturesPage() {
<h2 className="text-sm text-foreground-lighter">Filter by tags:</h2>
<div className="flex flex-col gap-2.5">
{products
.sort((a, b) => (a > b ? 1 : -1))
.sort((a, b) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1))
.map((product) => (
<div
key={product}
+40 -4
View File
@@ -99,16 +99,29 @@ const FeaturePage: React.FC<FeaturePageProps> = ({ feature, prevFeature, nextFea
flex flex-col
gap-4 md:gap-8
text-foreground-light
!py-10
!pt-10 !pb-4
"
>
<div className="relative h-full flex flex-col items-start gap-2 w-full max-w-2xl mx-auto">
<div className="flex gap-1 flex-wrap mb-2">
<div className="flex flex-wrap items-center space-x-1 mb-2">
<Link href="/features" passHref>
<Badge
className="p-0 h-[22px] w-[22px] rounded-full flex items-center justify-center text-foreground-lighter hover:text-foreground hover:border-foreground-lighter"
size="small"
>
<ChevronLeft className="w-3.5 h-3.5" />
</Badge>
</Link>
{feature.products.map((product) => (
<Link href={`/features?products=${product}`} passHref>
<Link
key={`product-${product}`}
href={`/features?products=${product}`}
className="inline-flex"
passHref
>
<Badge
key={product}
className="capitalize hover:border-foreground-lighter"
className="capitalize hover:border-foreground-lighter hover:text-foreground"
size="small"
>
{product}
@@ -186,6 +199,29 @@ const FeaturePage: React.FC<FeaturePageProps> = ({ feature, prevFeature, nextFea
</SectionContainer>
</header>
<SectionContainer className="!pt-0">
<div className="prose w-full max-w-3xl mx-auto pb-4 mb-8 border-b flex flex-col md:flex-row gap-4 md:gap-8 flex-wrap justify-center text-sm text-foreground-lighter">
<div className="flex gap-2 items-center">
<span>Stage:</span>
<Badge>{feature.status?.stage}</Badge>
</div>
<div className="flex gap-2 items-center">
<span>Available on self-hosted:</span>
{feature.status?.selfHostedTooling ? (
<Badge className="flex gap-1.5">
Requires
<Link
href={feature.status?.selfHostedTooling.link}
target="_blank"
className="translate-y-px hover:cursor-pointer"
>
{feature.status?.selfHostedTooling?.label}
</Link>
</Badge>
) : (
<Badge>{feature.status?.availableOnSelfHosted ? 'Yes' : 'N/A'}</Badge>
)}
</div>
</div>
<main className="max-w-xl mx-auto flex flex-col items-start gap-8">
<div className="prose prose-docs">
<ReactMarkdown>{feature.description}</ReactMarkdown>
Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+8 -1
View File
@@ -11,7 +11,7 @@
<guid>https://supabase.com/blog/supabase-ai-assistant-v2</guid>
<title>Supabase AI Assistant v2</title>
<link>https://supabase.com/blog/supabase-ai-assistant-v2</link>
<description>An evolution of how we approach AI within the Supabase dashboard&apos;</description>
<description>An evolution of how we approach AI within the Supabase dashboard</description>
<pubDate>Mon, 02 Dec 2024 00:00:00 -0700</pubDate>
</item>
<item>
@@ -21,6 +21,13 @@
<description>An evolution of how we approach AI within the Supabase dashboard</description>
<pubDate>Mon, 02 Dec 2024 00:00:00 -0700</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/orioledb-launch</guid>
<title>OrioleDB Public Alpha</title>
<link>https://supabase.com/blog/orioledb-launch</link>
<description>Launching OrioleDB Public Alpha</description>
<pubDate>Sun, 01 Dec 2024 00:00:00 -0700</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/supabase-dynamic-functions</guid>
<title>Executing Dynamic JavaScript Code on Supabase with Edge Functions</title>
+2 -2
View File
@@ -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:
@@ -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://<database>/<schema>/<hook_name>"
# 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. <bucket_name>.s3-<region>.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"
@@ -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);
Whitespace-only changes.
@@ -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://<database>/<schema>/<hook_name>"
# 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. <bucket_name>.s3-<region>.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)"
Whitespace-only changes.
@@ -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
@@ -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"
+1
View File
@@ -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",
+2 -2
View File
@@ -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",