diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx
index f8fbd00d211..e46c387c320 100644
--- a/apps/design-system/__registry__/index.tsx
+++ b/apps/design-system/__registry__/index.tsx
@@ -1831,6 +1831,28 @@ export const Index: Record = {
subcategory: "undefined",
chunks: []
},
+ "form-patterns-pagelayout": {
+ name: "form-patterns-pagelayout",
+ type: "components:example",
+ registryDependencies: ["form","card","input","button","select","switch","checkbox","textarea","radio-group","calendar","popover","multi-select"],
+ component: React.lazy(() => import("@/registry/default/example/form-patterns-pagelayout")),
+ source: "",
+ files: ["registry/default/example/form-patterns-pagelayout.tsx"],
+ category: "undefined",
+ subcategory: "undefined",
+ chunks: []
+ },
+ "form-patterns-sidepanel": {
+ name: "form-patterns-sidepanel",
+ type: "components:example",
+ registryDependencies: ["form","sheet","input","button","select","switch","checkbox","textarea","radio-group","calendar","popover","multi-select"],
+ component: React.lazy(() => import("@/registry/default/example/form-patterns-sidepanel")),
+ source: "",
+ files: ["registry/default/example/form-patterns-sidepanel.tsx"],
+ category: "undefined",
+ subcategory: "undefined",
+ chunks: []
+ },
"form-item-layout-with-select": {
name: "form-item-layout-with-select",
type: "components:example",
diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts
index 098d67bb4cb..4c921fd1170 100644
--- a/apps/design-system/config/docs.ts
+++ b/apps/design-system/config/docs.ts
@@ -65,6 +65,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/ui-patterns/empty-states',
items: [],
},
+ {
+ title: 'Forms',
+ href: '/docs/ui-patterns/forms',
+ items: [],
+ },
],
},
{
diff --git a/apps/design-system/content/docs/components/table.mdx b/apps/design-system/content/docs/components/table.mdx
index ab2cc6bf5dc..6dcafe2b601 100644
--- a/apps/design-system/content/docs/components/table.mdx
+++ b/apps/design-system/content/docs/components/table.mdx
@@ -48,6 +48,24 @@ import {
```
+## Sortable Columns
+
+Use `TableHeadSort` inside `TableHead` to enable column sorting.
+
+| Prop | Type | Description |
+| -------------- | -------------------------- | ------------------------------------------------------------------ |
+| `column` | `string` | Unique identifier for the column |
+| `currentSort` | `string` | Current sort state in format `"column:order"` (e.g., `"name:asc"`) |
+| `onSortChange` | `(column: string) => void` | Callback fired when column header is clicked |
+
+```tsx
+
+
+ Name
+
+
+```
+
## Data Table
You can use the `` component to build more complex data tables. Combine it with [@tanstack/react-table](https://tanstack.com/table/v8) to create tables with sorting, filtering and pagination.
diff --git a/apps/design-system/content/docs/ui-patterns/forms.mdx b/apps/design-system/content/docs/ui-patterns/forms.mdx
new file mode 100644
index 00000000000..eb71580c536
--- /dev/null
+++ b/apps/design-system/content/docs/ui-patterns/forms.mdx
@@ -0,0 +1,50 @@
+---
+title: Forms
+description: Common form patterns used in Studio settings pages and side panels.
+---
+
+Forms in Supabase Studio should follow consistent patterns to ensure a cohesive user experience across settings pages and side panels. This guide covers the most common form patterns and field types.
+
+## Page Layout
+
+Forms in page layouts typically use `PageSection` components with `Card` containers. Fields use `FormItemLayout` with `layout="flex-row-reverse"` for horizontal alignment.
+
+
+
+## Side Panel
+
+Forms in side panels (Sheets) use `FormItemLayout` with `layout="horizontal"` on wider panels and `layout="vertical"` on panels with a size of `sm` or below. The form is typically wrapped in a `Sheet` component.
+
+
+
+## Best Practices
+
+1. **Always use FormItemLayout**: Use `FormItemLayout` instead of manually composing `FormItem`, `FormLabel`, `FormMessage`, and `FormDescription`.
+
+2. **Layout selection**:
+
+ - Use `layout="flex-row-reverse"` for page layouts (horizontal alignment)
+ - Use `layout="horizontal"` for side panels with more width
+ - Use `layout="vertical"` for side panels with limited width
+
+3. **Wrap inputs in FormControl*Shadcn***: Always wrap form inputs with `FormControl_Shadcn_` to ensure proper form integration.
+
+4. **Use Cards for grouping**: Wrap form sections in `Card` components with `CardContent` and `CardFooter` for actions.
+
+5. **Handle dirty state**: Show cancel buttons and disable save buttons based on `form.formState.isDirty`.
+
+6. **Error handling**: Always use mutations with `onSuccess` and `onError` callbacks that show toast notifications.
+
+7. **Loading states**: Show loading states on submit buttons using the `loading` prop.
+
+8. **Form IDs**: When submit buttons are outside the form, use a form ID and reference it with the `form` prop on the button.
diff --git a/apps/design-system/registry/default/example/dialog-demo.tsx b/apps/design-system/registry/default/example/dialog-demo.tsx
index 68e81be28d5..3316817aea9 100644
--- a/apps/design-system/registry/default/example/dialog-demo.tsx
+++ b/apps/design-system/registry/default/example/dialog-demo.tsx
@@ -21,7 +21,7 @@ export default function DialogDemo() {
Edit profile
- Make changes to your profile here. Click save when you're done.
+ Make changes to your profile here. Click save when you're done.
diff --git a/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx b/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx
new file mode 100644
index 00000000000..71cf2850274
--- /dev/null
+++ b/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx
@@ -0,0 +1,748 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import { format } from 'date-fns'
+import { CalendarIcon, ExternalLink, Plus, Trash2, Upload } from 'lucide-react'
+import { useRef, useState } from 'react'
+import { useFieldArray, useForm } from 'react-hook-form'
+import * as z from 'zod'
+
+import {
+ Button,
+ Calendar,
+ Card,
+ CardContent,
+ CardFooter,
+ Checkbox_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Form_Shadcn_,
+ Input_Shadcn_,
+ PrePostTab,
+ Popover_Shadcn_,
+ PopoverContent_Shadcn_,
+ PopoverTrigger_Shadcn_,
+ RadioGroupStacked,
+ RadioGroupStackedItem,
+ Select_Shadcn_,
+ SelectContent_Shadcn_,
+ SelectItem_Shadcn_,
+ SelectTrigger_Shadcn_,
+ SelectValue_Shadcn_,
+ Separator,
+ Switch,
+ Textarea,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import { Input } from 'ui-patterns/DataInputs/Input'
+import {
+ MultiSelector,
+ MultiSelectorContent,
+ MultiSelectorItem,
+ MultiSelectorList,
+ MultiSelectorTrigger,
+} from 'ui-patterns/multi-select'
+import {
+ PageSection,
+ PageSectionContent,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns/PageSection'
+
+const formSchema = z.object({
+ name: z.string().min(1, 'Name is required'),
+ description: z.string().optional(),
+ maxConnections: z.number().min(1).max(1000),
+ enableFeature: z.boolean(),
+ enableRls: z.boolean(),
+ enableNotifications: z.boolean(),
+ enableAnalytics: z.boolean(),
+ region: z.string().min(1, 'Region is required'),
+ schemas: z.array(z.string()).min(1, 'At least one schema is required'),
+ queueType: z.enum(['basic', 'partitioned']),
+ expiryDate: z.date().optional(),
+ password: z.string().min(8, 'Password must be at least 8 characters'),
+ duration: z.number().min(5).max(30),
+ redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })),
+ apiKey: z.string().optional(),
+})
+
+const fakeApiKey = 'sk_live_51H3x4mpl3_4nd_53cur3_k3y_1234567890'
+
+export default function FormPatternsPageLayout() {
+ const uploadButtonRef = useRef(null)
+ const fileUploadRef = useRef(null)
+ const [logoFile, setLogoFile] = useState()
+ const [logoUrl, setLogoUrl] = useState()
+ const [uploadedFiles, setUploadedFiles] = useState([])
+ const [isDragging, setIsDragging] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: '',
+ description: '',
+ maxConnections: 10,
+ enableFeature: false,
+ enableRls: true,
+ enableNotifications: false,
+ enableAnalytics: true,
+ region: '',
+ schemas: ['public'],
+ queueType: 'basic',
+ expiryDate: undefined,
+ password: '',
+ duration: 10,
+ redirectUris: [{ value: '' }],
+ apiKey: fakeApiKey,
+ },
+ })
+
+ const { fields, append, remove } = useFieldArray({
+ control: form.control,
+ name: 'redirectUris',
+ })
+
+ function onSubmit(values: z.infer) {
+ console.log(values)
+ }
+
+ return (
+
+
+
+
+ Form Settings
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx b/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx
new file mode 100644
index 00000000000..c1bc84f8790
--- /dev/null
+++ b/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx
@@ -0,0 +1,770 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import { format } from 'date-fns'
+import { CalendarIcon, ExternalLink, Plus, Trash2, Upload } from 'lucide-react'
+import { useRef, useState } from 'react'
+import { useFieldArray, useForm } from 'react-hook-form'
+import * as z from 'zod'
+
+import {
+ Button,
+ Calendar,
+ Checkbox_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Form_Shadcn_,
+ Input_Shadcn_,
+ Popover_Shadcn_,
+ PopoverContent_Shadcn_,
+ PopoverTrigger_Shadcn_,
+ RadioGroupStacked,
+ RadioGroupStackedItem,
+ Select_Shadcn_,
+ SelectContent_Shadcn_,
+ SelectItem_Shadcn_,
+ SelectTrigger_Shadcn_,
+ SelectValue_Shadcn_,
+ Separator,
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetSection,
+ SheetTitle,
+ Switch,
+ Textarea,
+ PrePostTab,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import { Input } from 'ui-patterns/DataInputs/Input'
+import {
+ MultiSelector,
+ MultiSelectorContent,
+ MultiSelectorItem,
+ MultiSelectorList,
+ MultiSelectorTrigger,
+} from 'ui-patterns/multi-select'
+
+const formSchema = z.object({
+ name: z.string().min(1, 'Name is required'),
+ description: z.string().optional(),
+ maxConnections: z.number().min(1).max(1000),
+ enableFeature: z.boolean(),
+ enableRls: z.boolean(),
+ enableNotifications: z.boolean(),
+ enableAnalytics: z.boolean(),
+ region: z.string().min(1, 'Region is required'),
+ schemas: z.array(z.string()).min(1, 'At least one schema is required'),
+ queueType: z.enum(['basic', 'partitioned']),
+ expiryDate: z.date().optional(),
+ password: z.string().min(8, 'Password must be at least 8 characters'),
+ duration: z.number().min(5).max(30),
+ redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })),
+ apiKey: z.string().optional(),
+})
+
+const fakeApiKey = 'sk_live_51H3x4mpl3_4nd_53cur3_k3y_1234567890'
+
+export default function FormPatternsSidePanel() {
+ const [open, setOpen] = useState(false)
+ const uploadButtonRef = useRef(null)
+ const fileUploadRef = useRef(null)
+ const [logoFile, setLogoFile] = useState()
+ const [logoUrl, setLogoUrl] = useState()
+ const [uploadedFiles, setUploadedFiles] = useState([])
+ const [isDragging, setIsDragging] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: '',
+ description: '',
+ maxConnections: 10,
+ enableFeature: false,
+ enableRls: true,
+ enableNotifications: false,
+ enableAnalytics: true,
+ region: '',
+ schemas: ['public'],
+ queueType: 'basic',
+ expiryDate: undefined,
+ password: '',
+ duration: 10,
+ redirectUris: [{ value: '' }],
+ apiKey: fakeApiKey,
+ },
+ })
+
+ const { fields, append, remove } = useFieldArray({
+ control: form.control,
+ name: 'redirectUris',
+ })
+
+ function onSubmit(values: z.infer) {
+ console.log(values)
+ setOpen(false)
+ }
+
+ const formId = 'sidepanel-form'
+
+ return (
+ <>
+ setOpen(true)}>
+ Open form panel
+
+
+
+
+ Create Configuration
+
+
+
+ {/* Text Input */}
+
+ (
+
+
+
+
+
+ )}
+ />
+
+
+
+
+ {/* Password Input */}
+
+ (
+
+
+
+
+
+ )}
+ />
+
+
+
+
+ {/* Copyable Input */}
+
+ (
+
+
+ {}}
+ onCopy={() => console.log('Copied to clipboard')}
+ />
+
+
+ )}
+ />
+
+
+
+
+ {/* Number Input */}
+
+ (
+
+
+ field.onChange(Number(e.target.value))}
+ />
+
+
+ )}
+ />
+
+
+
+
+ {/* Input with Units */}
+
+ (
+
+
+
+
+
+
+
+ )}
+ />
+
+
+
+
+ {/* Textarea */}
+
+ (
+
+
+
+
+
+ )}
+ />
+
+
+
+
+ {/* Icon Upload */}
+
+ (
+
+
+
+
uploadButtonRef.current?.click()}
+ className="flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong"
+ style={{
+ backgroundImage: logoUrl ? `url("${logoUrl}")` : 'none',
+ }}
+ >
+ {!logoUrl && }
+
+
+ }
+ onClick={() => uploadButtonRef.current?.click()}
+ >
+ Upload
+
+ {logoUrl && (
+ }
+ onClick={() => {
+ setLogoFile(undefined)
+ setLogoUrl(undefined)
+ }}
+ />
+ )}
+
+
{
+ const files = e.target.files
+ if (files && files.length > 0) {
+ const file = files[0]
+ setLogoFile(file)
+ setLogoUrl(URL.createObjectURL(file))
+ e.target.value = ''
+ }
+ }}
+ />
+
+
+
+ )}
+ />
+
+
+
+
+ {/* File Upload */}
+
+ (
+
+
+ {
+ e.preventDefault()
+ setIsDragging(true)
+ }}
+ onDragLeave={() => setIsDragging(false)}
+ onDrop={(e) => {
+ e.preventDefault()
+ setIsDragging(false)
+ const files = Array.from(e.dataTransfer.files)
+ setUploadedFiles((prev) => [...prev, ...files])
+ }}
+ >
+
{
+ const files = e.target.files
+ if (files) {
+ setUploadedFiles((prev) => [...prev, ...Array.from(files)])
+ }
+ e.target.value = ''
+ }}
+ />
+
+
+
+ {uploadedFiles.length > 0
+ ? `${uploadedFiles.length} file${uploadedFiles.length > 1 ? 's' : ''} selected`
+ : 'Upload files'}
+
+
+ Drag and drop or{' '}
+ fileUploadRef.current?.click()}
+ className="underline cursor-pointer hover:text-foreground-light"
+ >
+ select files
+ {' '}
+ to upload
+
+ {uploadedFiles.length > 0 && (
+
+ {uploadedFiles.map((file, idx) => (
+
+
+ {file.name}
+
+ }
+ onClick={() => {
+ setUploadedFiles((prev) => prev.filter((_, i) => i !== idx))
+ }}
+ />
+
+ ))}
+
+ )}
+
+
+
+
+ )}
+ />
+
+
+
+
+ {/* Switch */}
+
+ (
+
+
+
+
+
+ )}
+ />
+
+
+ {/* Checkbox */}
+
+
+
+
(
+
+
+
+
+
+ Enable Row Level Security
+
+
+ )}
+ />
+ (
+
+
+
+
+
+ Enable email notifications
+
+
+ )}
+ />
+ (
+
+
+
+
+
+ Enable analytics tracking
+
+
+ )}
+ />
+
+
+
+
+
+
+ {/* Select */}
+
+ (
+
+
+
+
+
+
+
+
+ US East (N. Virginia)
+
+
+ US West (Oregon)
+
+
+ EU West (Ireland)
+
+
+
+
+
+ )}
+ />
+
+
+
+
+ {/* Multi-Select */}
+
+ (
+
+
+
+
+
+
+ public
+ auth
+ storage
+
+
+
+
+
+ )}
+ />
+
+
+
+
+ {/* Radio Group */}
+
+ (
+
+
+
+
+
+
+
+
+ )}
+ />
+
+
+
+
+ {/* Date Picker */}
+
+ (
+
+
+
+
+ }
+ >
+ {field.value ? format(field.value, 'PPP') : 'Pick a date'}
+
+
+
+
+
+
+
+
+ )}
+ />
+
+
+
+
+ {/* Field Array */}
+
+ (
+
+
+ {fields.map((field, index) => (
+
(
+
+
+
+
+ {fields.length > 1 && (
+ }
+ onClick={() => remove(index)}
+ />
+ )}
+
+ )}
+ />
+ ))}
+ }
+ onClick={() => append({ value: '' })}
+ >
+ Add redirect URI
+
+
+
+ )}
+ />
+
+
+
+
+ {/* Action Field */}
+
+
+
+ }
+ onClick={() => console.log('Action performed')}
+ >
+ View documentation
+
+ console.log('Reset action')}>
+ Reset API key
+
+
+
+
+
+
+
+ {
+ form.reset()
+ setOpen(false)
+ }}
+ >
+ Cancel
+
+
+ Create
+
+
+
+
+ >
+ )
+}
diff --git a/apps/design-system/registry/default/example/page-layout-detail.tsx b/apps/design-system/registry/default/example/page-layout-detail.tsx
index 6553522b8ed..a5036cca81c 100644
--- a/apps/design-system/registry/default/example/page-layout-detail.tsx
+++ b/apps/design-system/registry/default/example/page-layout-detail.tsx
@@ -24,7 +24,7 @@ export default function PageLayoutDetail() {
Billing
- Manage your organization's billing and subscription settings.
+ Manage your organization's billing and subscription settings.
@@ -72,7 +72,7 @@ export default function PageLayoutDetail() {
Cost control
- Set spending limits and alerts to manage your organization's costs.
+ Set spending limits and alerts to manage your organization's costs.
diff --git a/apps/design-system/registry/default/example/toc-demo.tsx b/apps/design-system/registry/default/example/toc-demo.tsx
index 2de1b7fdfeb..86c2d700664 100644
--- a/apps/design-system/registry/default/example/toc-demo.tsx
+++ b/apps/design-system/registry/default/example/toc-demo.tsx
@@ -39,7 +39,7 @@ export default function MultiSelectDemo() {
- Before diving deep into cloud services, it's important to understand the basic
+ Before diving deep into cloud services, it's important to understand the basic
building blocks that make cloud computing possible.
diff --git a/apps/design-system/registry/default/example/toc-single-demo.tsx b/apps/design-system/registry/default/example/toc-single-demo.tsx
index d7fe367d0ce..d5f24f153b0 100644
--- a/apps/design-system/registry/default/example/toc-single-demo.tsx
+++ b/apps/design-system/registry/default/example/toc-single-demo.tsx
@@ -39,7 +39,7 @@ export default function MultiSelectDemo() {
- Before diving deep into cloud services, it's important to understand the basic
+ Before diving deep into cloud services, it's important to understand the basic
building blocks that make cloud computing possible.
diff --git a/apps/design-system/registry/default/example/typography-blockquote.tsx b/apps/design-system/registry/default/example/typography-blockquote.tsx
index 15c95473fd2..4d0ae0e529e 100644
--- a/apps/design-system/registry/default/example/typography-blockquote.tsx
+++ b/apps/design-system/registry/default/example/typography-blockquote.tsx
@@ -1,8 +1,8 @@
export default function TypographyBlockquote() {
return (
- "After all," he said, "everyone enjoys a good joke, so it's only fair that they should pay for
- the privilege."
+ "After all," he said, "everyone enjoys a good joke, so it's only fair that
+ they should pay for the privilege."
)
}
diff --git a/apps/design-system/registry/default/example/typography-demo.tsx b/apps/design-system/registry/default/example/typography-demo.tsx
index 60ec59526bc..3ea54c3a470 100644
--- a/apps/design-system/registry/default/example/typography-demo.tsx
+++ b/apps/design-system/registry/default/example/typography-demo.tsx
@@ -10,7 +10,7 @@ export default function TypographyDemo() {
of money.
- The King's Plan
+ The King's Plan
The king thought long and hard, and finally came up with{' '}
@@ -20,12 +20,13 @@ export default function TypographyDemo() {
: he would tax the jokes in the kingdom.
- "After all," he said, "everyone enjoys a good joke, so it's only fair that they should pay
- for the privilege."
+ "After all," he said, "everyone enjoys a good joke, so it's only fair
+ that they should pay for the privilege."
The Joke Tax
- The king's subjects were not amused. They grumbled and complained, but the king was firm:
+ The king's subjects were not amused. They grumbled and complained, but the king was
+ firm:
1st level of puns: 5 gold coins
@@ -34,22 +35,24 @@ export default function TypographyDemo() {
As a result, people stopped telling jokes, and the kingdom fell into a gloom. But there was
- one person who refused to let the king's foolishness get him down: a court jester named
+ one person who refused to let the king's foolishness get him down: a court jester named
Jokester.
- Jokester's Revolt
+
+ Jokester's Revolt
+
Jokester began sneaking into the castle in the middle of the night and leaving jokes all
- over the place: under the king's pillow, in his soup, even in the royal toilet. The king was
- furious, but he couldn't seem to stop Jokester.
+ over the place: under the king's pillow, in his soup, even in the royal toilet. The
+ king was furious, but he couldn't seem to stop Jokester.
And then, one day, the people of the kingdom discovered that the jokes left by Jokester were
- so funny that they couldn't help but laugh. And once they started laughing, they couldn't
- stop.
+ so funny that they couldn't help but laugh. And once they started laughing, they
+ couldn't stop.
- The People's Rebellion
+ The People's Rebellion
The people of the kingdom, feeling uplifted by the laughter, started to tell jokes and puns
@@ -60,10 +63,10 @@ export default function TypographyDemo() {
- King's Treasury
+ King's Treasury
- People's happiness
+ People's happiness
diff --git a/apps/design-system/registry/default/example/typography-table.tsx b/apps/design-system/registry/default/example/typography-table.tsx
index 0698c475194..7a91dfda844 100644
--- a/apps/design-system/registry/default/example/typography-table.tsx
+++ b/apps/design-system/registry/default/example/typography-table.tsx
@@ -5,10 +5,10 @@ export default function TypographyTable() {
- King's Treasury
+ King's Treasury
- People's happiness
+ People's happiness
diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts
index 2e3181ddf77..59495e9004f 100644
--- a/apps/design-system/registry/examples.ts
+++ b/apps/design-system/registry/examples.ts
@@ -1032,6 +1032,46 @@ export const examples: Registry = [
type: 'components:example',
files: ['example/form-item-layout-demo.tsx'],
},
+ {
+ name: 'form-patterns-pagelayout',
+ type: 'components:example',
+ registryDependencies: [
+ 'form',
+ 'card',
+ 'input',
+ 'button',
+ 'select',
+ 'switch',
+ 'checkbox',
+ 'textarea',
+ 'radio-group',
+ 'calendar',
+ 'popover',
+ 'multi-select',
+ ],
+ files: ['example/form-patterns-pagelayout.tsx'],
+ dependencies: ['date-fns'],
+ },
+ {
+ name: 'form-patterns-sidepanel',
+ type: 'components:example',
+ registryDependencies: [
+ 'form',
+ 'sheet',
+ 'input',
+ 'button',
+ 'select',
+ 'switch',
+ 'checkbox',
+ 'textarea',
+ 'radio-group',
+ 'calendar',
+ 'popover',
+ 'multi-select',
+ ],
+ files: ['example/form-patterns-sidepanel.tsx'],
+ dependencies: ['date-fns'],
+ },
{
name: 'form-item-layout-with-select',
type: 'components:example',
diff --git a/apps/docs/instrumentation.ts b/apps/docs/instrumentation.ts
index 3063091e693..b1d3d20b222 100644
--- a/apps/docs/instrumentation.ts
+++ b/apps/docs/instrumentation.ts
@@ -1,10 +1,12 @@
import * as Sentry from '@sentry/nextjs'
export async function register() {
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config')
}
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config')
}
diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt
index ae02a7e2c30..da8bea31fb4 100644
--- a/apps/docs/public/humans.txt
+++ b/apps/docs/public/humans.txt
@@ -16,6 +16,7 @@ Amy Q
Andrew Valleteau
Andrey A
Angelico de los Reyes
+Anna Weihl
Ant Wilson
Ariuna K
Artur Zakirov
@@ -126,6 +127,7 @@ Monica Khoury
Mykhailo Mischa Lieibenson
Nick B
Nick Littman
+Nick Nowlan
Nyannyacha
Oli R
Pamela Chia
@@ -165,6 +167,7 @@ Taryn King
Terry Sutton
Thomas E
Thor Schaeff
+Tim Frietas
Timothy Lim
Tom Ashley
Tom G
diff --git a/apps/studio/.github/eslint-rule-baselines.json b/apps/studio/.github/eslint-rule-baselines.json
index a2729485e45..016422c7cf9 100644
--- a/apps/studio/.github/eslint-rule-baselines.json
+++ b/apps/studio/.github/eslint-rule-baselines.json
@@ -1,9 +1,10 @@
{
"rules": {
- "react-hooks/exhaustive-deps": 230,
+ "react-hooks/exhaustive-deps": 226,
"import/no-anonymous-default-export": 62,
- "@tanstack/query/exhaustive-deps": 19,
- "@tanstack/query/no-deprecated-options": 0
+ "@tanstack/query/exhaustive-deps": 18,
+ "@tanstack/query/no-deprecated-options": 0,
+ "@typescript-eslint/no-explicit-any": 1452
},
"ruleFiles": {
"react-hooks/exhaustive-deps": {
@@ -24,7 +25,6 @@
"components/interfaces/Auth/BasicAuthSettingsForm.tsx": 1,
"components/interfaces/Auth/Hooks/CreateHookSheet.tsx": 1,
"components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx": 1,
- "components/interfaces/Auth/OAuthApps/CreateOAuthAppSheet.tsx": 1,
"components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx": 1,
"components/interfaces/Auth/Policies/PolicyEditor/PolicyDefinition.tsx": 1,
"components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyDetailsV2.tsx": 1,
@@ -101,14 +101,12 @@
"components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CopyEnvButton.tsx": 1,
"components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx": 1,
"components/interfaces/Storage/StorageSettings/StorageSettings.tsx": 1,
- "components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx": 1,
"components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/index.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx": 2,
- "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/QuickstartAIWidget.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/useAITableGeneration.ts": 2,
"components/interfaces/TableGridEditor/TableDefinition.tsx": 1,
@@ -168,14 +166,13 @@
"pages/project/[ref]/editor/index.tsx": 1,
"pages/project/[ref]/integrations/[id]/[pageId]/index.tsx": 1,
"pages/project/[ref]/logs/explorer/index.tsx": 1,
- "pages/project/[ref]/settings/jwt/index.tsx": 1,
+ "pages/project/[ref]/settings/jwt/legacy.tsx": 1,
"pages/project/[ref]/sql/quickstarts.tsx": 1,
"pages/project/[ref]/sql/templates.tsx": 1,
"pages/sign-in-fly-tos.tsx": 1,
"pages/sign-in-mfa.tsx": 1,
"state/role-impersonation-state.tsx": 1,
"state/sidebar-manager-state.tsx": 1,
- "state/storage-explorer.tsx": 1,
"state/table-editor-table.tsx": 1
},
"import/no-anonymous-default-export": {
@@ -245,7 +242,7 @@
"@tanstack/query/exhaustive-deps": {
"components/interfaces/Reports/SharedAPIReport/SharedAPIReport.constants.ts": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx": 1,
- "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 3,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 2,
"data/branches/branch-diff-query.ts": 1,
"data/database/foreign-key-constraints-query.ts": 1,
"data/database/schemas-query.ts": 1,
@@ -258,6 +255,574 @@
"hooks/analytics/useProjectUsageStats.tsx": 1,
"hooks/analytics/useSingleLog.tsx": 1
},
- "@tanstack/query/no-deprecated-options": {}
+ "@tanstack/query/no-deprecated-options": {},
+ "@typescript-eslint/no-explicit-any": {
+ "components/grid/SupabaseGrid.tsx": 1,
+ "components/grid/SupabaseGrid.utils.ts": 4,
+ "components/grid/components/common/Hooks.tsx": 2,
+ "components/grid/components/common/MonacoEditor.tsx": 3,
+ "components/grid/components/editor/BooleanEditor.tsx": 1,
+ "components/grid/components/editor/JsonEditor.tsx": 2,
+ "components/grid/components/editor/SelectEditor.tsx": 1,
+ "components/grid/components/editor/TextEditor.tsx": 2,
+ "components/grid/components/formatter/ReferenceRecordPeek.tsx": 4,
+ "components/grid/components/grid/AddColumn.tsx": 2,
+ "components/grid/components/grid/Grid.tsx": 10,
+ "components/grid/components/grid/Grid.utils.tsx": 2,
+ "components/grid/components/grid/GridError.tsx": 4,
+ "components/grid/components/grid/SelectColumn.tsx": 3,
+ "components/grid/components/header/Header.utils.ts": 1,
+ "components/grid/components/header/sort/SortRow.tsx": 1,
+ "components/grid/components/menu/ColumnMenu.tsx": 1,
+ "components/grid/types/base.ts": 2,
+ "components/grid/types/table.ts": 1,
+ "components/grid/utils/column.ts": 2,
+ "components/grid/utils/common.ts": 1,
+ "components/grid/utils/gridColumns.tsx": 10,
+ "components/interfaces/Account/AccessTokens/NewAccessTokenButton.tsx": 1,
+ "components/interfaces/Account/AccessTokens/NewAccessTokenDialog.tsx": 1,
+ "components/interfaces/Auth/AdvancedAuthSettingsForm.tsx": 2,
+ "components/interfaces/Auth/AuditLogsForm.tsx": 1,
+ "components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.types.ts": 1,
+ "components/interfaces/Auth/AuthProvidersForm/FormField.tsx": 4,
+ "components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx": 6,
+ "components/interfaces/Auth/AuthProvidersForm/index.tsx": 5,
+ "components/interfaces/Auth/AuthProvidersFormValidation.tsx": 2,
+ "components/interfaces/Auth/BasicAuthSettingsForm.tsx": 1,
+ "components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx": 1,
+ "components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx": 1,
+ "components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx": 3,
+ "components/interfaces/Auth/Overview/OverviewTable.tsx": 1,
+ "components/interfaces/Auth/Overview/OverviewUsage.constants.ts": 1,
+ "components/interfaces/Auth/Policies/Policies.tsx": 1,
+ "components/interfaces/Auth/Policies/Policies.utils.ts": 1,
+ "components/interfaces/Auth/Policies/PolicyEditor/PolicyRoles.tsx": 1,
+ "components/interfaces/Auth/Policies/PolicyEditor/index.tsx": 2,
+ "components/interfaces/Auth/Policies/PolicyEditorModal/index.tsx": 2,
+ "components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx": 1,
+ "components/interfaces/Auth/Policies/PolicyEditorPanel/RLSCodeEditor.tsx": 2,
+ "components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx": 2,
+ "components/interfaces/Auth/ProtectionAuthSettingsForm/ProtectionAuthSettingsForm.tsx": 1,
+ "components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx": 1,
+ "components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx": 2,
+ "components/interfaces/Auth/SiteUrl/SiteUrl.tsx": 1,
+ "components/interfaces/Auth/SmtpForm/SmtpForm.tsx": 4,
+ "components/interfaces/Auth/Users/CreateUserModal.tsx": 1,
+ "components/interfaces/Auth/Users/InviteUserModal.tsx": 3,
+ "components/interfaces/Auth/Users/UserLogs.tsx": 1,
+ "components/interfaces/Auth/Users/UserPanel.tsx": 2,
+ "components/interfaces/Auth/Users/Users.utils.tsx": 5,
+ "components/interfaces/Auth/Users/UsersGridComponents.tsx": 2,
+ "components/interfaces/Auth/Users/UsersV2.tsx": 6,
+ "components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx": 3,
+ "components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx": 3,
+ "components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx": 3,
+ "components/interfaces/BranchManagement/Branch.Commands.tsx": 1,
+ "components/interfaces/BranchManagement/DatabaseDiffPanel.tsx": 1,
+ "components/interfaces/BranchManagement/EmptyStates.tsx": 1,
+ "components/interfaces/BranchManagement/ReviewWithAI.tsx": 1,
+ "components/interfaces/Connect/ConnectTabs.tsx": 1,
+ "components/interfaces/Database/Backups/PITR/PITR.utils.ts": 1,
+ "components/interfaces/Database/Backups/RestoreToNewProject/BackupsList.tsx": 1,
+ "components/interfaces/Database/EnumeratedTypes/CreateEnumeratedTypeSidePanel.tsx": 2,
+ "components/interfaces/Database/EnumeratedTypes/DeleteEnumeratedTypeModal.tsx": 1,
+ "components/interfaces/Database/EnumeratedTypes/EditEnumeratedTypeSidePanel.tsx": 2,
+ "components/interfaces/Database/EnumeratedTypes/EnumeratedTypeValueRow.tsx": 1,
+ "components/interfaces/Database/Extensions/EnableExtensionModal.tsx": 4,
+ "components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx": 1,
+ "components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx": 3,
+ "components/interfaces/Database/Hooks/EditHookPanel.tsx": 9,
+ "components/interfaces/Database/Hooks/FormContents.tsx": 5,
+ "components/interfaces/Database/Hooks/HTTPRequestFields.tsx": 6,
+ "components/interfaces/Database/Hooks/HooksList/HooksList.tsx": 2,
+ "components/interfaces/Database/Indexes/Indexes.tsx": 1,
+ "components/interfaces/Database/Migrations/Migrations.tsx": 2,
+ "components/interfaces/Database/Privileges/PrivilegesTable.tsx": 1,
+ "components/interfaces/Database/Publications/PublicationsList.tsx": 5,
+ "components/interfaces/Database/Publications/PublicationsTableItem.tsx": 5,
+ "components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx": 8,
+ "components/interfaces/Database/Replication/PublicationsComboBox.tsx": 1,
+ "components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx": 1,
+ "components/interfaces/Database/Replication/RetryOptionsDropdown.tsx": 1,
+ "components/interfaces/Database/Replication/RowMenu.tsx": 2,
+ "components/interfaces/Database/Replication/UpdateVersionModal.tsx": 2,
+ "components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx": 1,
+ "components/interfaces/Database/Roles/RoleRow.tsx": 4,
+ "components/interfaces/Database/Roles/RolesList.tsx": 1,
+ "components/interfaces/Database/Schemas/SchemaGraph.tsx": 1,
+ "components/interfaces/Database/Tables/ColumnList.tsx": 2,
+ "components/interfaces/DiskManagement/DiskManagement.utils.ts": 3,
+ "components/interfaces/Docs/Description.tsx": 2,
+ "components/interfaces/Docs/GeneratingTypes.tsx": 1,
+ "components/interfaces/Docs/Param.tsx": 1,
+ "components/interfaces/Docs/ResourceContent.tsx": 1,
+ "components/interfaces/Docs/RpcContent.tsx": 4,
+ "components/interfaces/Docs/Snippets.ts": 1,
+ "components/interfaces/Functions/CommandRender.tsx": 4,
+ "components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx": 1,
+ "components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx": 3,
+ "components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx": 1,
+ "components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx": 1,
+ "components/interfaces/Functions/EdgeFunctionsListItem.tsx": 2,
+ "components/interfaces/Functions/FunctionsNav.tsx": 1,
+ "components/interfaces/Home/Home.tsx": 6,
+ "components/interfaces/Home/NewProjectPanel/APIKeys.tsx": 1,
+ "components/interfaces/Home/ProjectList/ProjectList.tsx": 1,
+ "components/interfaces/Home/ProjectUsage.tsx": 1,
+ "components/interfaces/Home/ServiceStatus.tsx": 1,
+ "components/interfaces/HomeNew/ProjectUsageSection.tsx": 1,
+ "components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx": 1,
+ "components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx": 2,
+ "components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx": 1,
+ "components/interfaces/Integrations/CronJobs/CronJobsTab.tsx": 1,
+ "components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx": 2,
+ "components/interfaces/Integrations/GraphQL/GraphiQLTab.tsx": 1,
+ "components/interfaces/Integrations/Landing/Integrations.constants.tsx": 1,
+ "components/interfaces/Integrations/Queues/QueuesSettings.tsx": 1,
+ "components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx": 1,
+ "components/interfaces/Integrations/Queues/SingleQueue/QueueFilters.tsx": 1,
+ "components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx": 2,
+ "components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx": 6,
+ "components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx": 1,
+ "components/interfaces/Integrations/Wrappers/CreateIcebergWrapperSheet.tsx": 3,
+ "components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx": 6,
+ "components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx": 7,
+ "components/interfaces/Integrations/Wrappers/InputField.tsx": 1,
+ "components/interfaces/Integrations/Wrappers/WrapperDynamicColumns.tsx": 1,
+ "components/interfaces/Integrations/Wrappers/WrapperTableEditor.tsx": 7,
+ "components/interfaces/Integrations/Wrappers/Wrappers.utils.ts": 2,
+ "components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx": 2,
+ "components/interfaces/JwtSecrets/jwt-settings.tsx": 5,
+ "components/interfaces/Linter/LinterDataGrid.tsx": 4,
+ "components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx": 2,
+ "components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx": 2,
+ "components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils.ts": 2,
+ "components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx": 2,
+ "components/interfaces/Organization/BillingSettings/CreditTopUp.tsx": 2,
+ "components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx": 1,
+ "components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx": 2,
+ "components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx": 1,
+ "components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx": 6,
+ "components/interfaces/Organization/BillingSettings/Subscription/UpgradeModal.tsx": 3,
+ "components/interfaces/Organization/Documents/SOC2.tsx": 1,
+ "components/interfaces/Organization/Documents/SecurityQuestionnaire.tsx": 1,
+ "components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx": 3,
+ "components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx": 1,
+ "components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx": 1,
+ "components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx": 1,
+ "components/interfaces/Organization/OAuthApps/PublishAppSidePanel/index.tsx": 9,
+ "components/interfaces/Organization/ProjectClaim/confirm.tsx": 1,
+ "components/interfaces/Organization/TeamSettings/MemberRow.tsx": 2,
+ "components/interfaces/Organization/TeamSettings/TeamSettings.tsx": 1,
+ "components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesConfirmationModal.tsx": 2,
+ "components/interfaces/Organization/Usage/Usage.tsx": 1,
+ "components/interfaces/Organization/Usage/Usage.utils.ts": 1,
+ "components/interfaces/Organization/Usage/UsageChartTooltips.tsx": 7,
+ "components/interfaces/ProjectAPIDocs/Content/Entities.tsx": 1,
+ "components/interfaces/ProjectAPIDocs/Content/Entity.tsx": 1,
+ "components/interfaces/ProjectAPIDocs/Content/RPC.tsx": 1,
+ "components/interfaces/ProjectAPIDocs/ProjectAPIDocs.constants.ts": 2,
+ "components/interfaces/ProjectAPIDocs/ResourceContent.tsx": 1,
+ "components/interfaces/ProjectAPIDocs/SecondLevelNav.tsx": 1,
+ "components/interfaces/ProjectCreation/DatabasePasswordInput.tsx": 1,
+ "components/interfaces/ProjectCreation/PostgresVersionSelector.tsx": 2,
+ "components/interfaces/ProjectCreation/SchemaGenerator.tsx": 1,
+ "components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton.tsx": 1,
+ "components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorDisabledState.tsx": 1,
+ "components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils.ts": 2,
+ "components/interfaces/QueryPerformance/QueryDetail.tsx": 1,
+ "components/interfaces/QueryPerformance/QueryIndexes.tsx": 1,
+ "components/interfaces/QueryPerformance/QueryPerformance.tsx": 1,
+ "components/interfaces/QueryPerformance/QueryPerformanceChart.tsx": 3,
+ "components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx": 3,
+ "components/interfaces/QueryPerformance/WithMonitor/WithMonitor.utils.ts": 2,
+ "components/interfaces/QueryPerformance/WithStatements/WithStatements.tsx": 2,
+ "components/interfaces/QueryPerformance/WithStatements/WithStatements.utils.ts": 1,
+ "components/interfaces/Realtime/Inspector/Messages.types.ts": 1,
+ "components/interfaces/Realtime/Inspector/RealtimeFilterPopover/TableSelector.tsx": 1,
+ "components/interfaces/Realtime/Inspector/useRealtimeMessages.ts": 10,
+ "components/interfaces/Realtime/RealtimeSettings.tsx": 2,
+ "components/interfaces/Reports/GridResize.tsx": 2,
+ "components/interfaces/Reports/MetricOptions.tsx": 1,
+ "components/interfaces/Reports/ReportBlock/ChartBlock.tsx": 1,
+ "components/interfaces/Reports/ReportWidget.tsx": 2,
+ "components/interfaces/Reports/Reports.tsx": 3,
+ "components/interfaces/Reports/Reports.types.ts": 1,
+ "components/interfaces/Reports/Reports.utils.tsx": 10,
+ "components/interfaces/Reports/SharedAPIReport/SharedAPIReport.tsx": 3,
+ "components/interfaces/Reports/v2/ReportChartUpsell.tsx": 1,
+ "components/interfaces/Reports/v2/ReportChartV2.test.tsx": 4,
+ "components/interfaces/Reports/v2/ReportChartV2.tsx": 8,
+ "components/interfaces/RoleImpersonationSelector/UserImpersonationSelector.tsx": 1,
+ "components/interfaces/SQLEditor/MonacoEditor.tsx": 2,
+ "components/interfaces/SQLEditor/MoveQueryModal.tsx": 2,
+ "components/interfaces/SQLEditor/RenameQueryModal.tsx": 5,
+ "components/interfaces/SQLEditor/SQLEditor.tsx": 4,
+ "components/interfaces/SQLEditor/SQLEditor.utils.ts": 1,
+ "components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts.tsx": 1,
+ "components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx": 1,
+ "components/interfaces/SQLEditor/UtilityPanel/CellDetailPanel.tsx": 1,
+ "components/interfaces/SQLEditor/UtilityPanel/ChartConfig.tsx": 2,
+ "components/interfaces/SQLEditor/UtilityPanel/Results.tsx": 7,
+ "components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx": 1,
+ "components/interfaces/SQLEditor/hooks.ts": 1,
+ "components/interfaces/SQLEditor/useAddDefinitions.ts": 1,
+ "components/interfaces/SchemaVisualizer/index.tsx": 1,
+ "components/interfaces/Settings/Addons/Addons.tsx": 1,
+ "components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx": 1,
+ "components/interfaces/Settings/Addons/IPv4SidePanel.tsx": 1,
+ "components/interfaces/Settings/Addons/PITRSidePanel.tsx": 1,
+ "components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx": 2,
+ "components/interfaces/Settings/Database/DiskSizeConfigurationModal.tsx": 1,
+ "components/interfaces/Settings/Database/NetworkRestrictions/AddRestrictionModal.tsx": 5,
+ "components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.utils.ts": 2,
+ "components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx": 1,
+ "components/interfaces/Settings/General/General.tsx": 3,
+ "components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx": 1,
+ "components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx": 1,
+ "components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx": 1,
+ "components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx": 1,
+ "components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/RestartReplicaConfirmationModal.tsx": 1,
+ "components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx": 1,
+ "components/interfaces/Settings/Logs/LogTable.tsx": 3,
+ "components/interfaces/Settings/Logs/Logs.constants.ts": 1,
+ "components/interfaces/Settings/Logs/Logs.types.ts": 1,
+ "components/interfaces/Settings/Logs/Logs.utils.ts": 11,
+ "components/interfaces/Settings/Logs/LogsQueryPanel.tsx": 2,
+ "components/interfaces/Settings/Logs/PreviewFilterPanel.tsx": 1,
+ "components/interfaces/Settings/Logs/PreviewFilterPanelWithUniversal.tsx": 3,
+ "components/interfaces/Settings/Logs/SidebarV2/SidebarItem.tsx": 1,
+ "components/interfaces/Sidebar.tsx": 3,
+ "components/interfaces/SignIn/SignInForm.tsx": 1,
+ "components/interfaces/SignIn/SignInWithCustom.tsx": 1,
+ "components/interfaces/SignIn/SignInWithGitHub.tsx": 1,
+ "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/ConnectTablesDialog.tsx": 2,
+ "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/InitializeForeignSchemaDialog.tsx": 1,
+ "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx": 4,
+ "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/index.tsx": 1,
+ "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/UpdateForeignSchemaDialog.tsx": 1,
+ "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx": 1,
+ "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/useAnalyticsBucketAssociatedEntities.tsx": 4,
+ "components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.tsx": 1,
+ "components/interfaces/Storage/CreateBucketModal.tsx": 1,
+ "components/interfaces/Storage/DeleteBucketModal.tsx": 1,
+ "components/interfaces/Storage/ImportForeignSchemaDialog.tsx": 1,
+ "components/interfaces/Storage/Storage.utils.ts": 7,
+ "components/interfaces/Storage/StorageBucketsError.tsx": 1,
+ "components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx": 5,
+ "components/interfaces/Storage/StorageExplorer/FileExplorer.tsx": 2,
+ "components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx": 6,
+ "components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx": 6,
+ "components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx": 1,
+ "components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx": 2,
+ "components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx": 1,
+ "components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx": 1,
+ "components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx": 1,
+ "components/interfaces/Storage/StorageExplorer/StorageExplorer.utils.tsx": 2,
+ "components/interfaces/Storage/StoragePolicies/StoragePolicies.tsx": 8,
+ "components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx": 11,
+ "components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx": 2,
+ "components/interfaces/Storage/StoragePolicies/StoragePoliciesReview.tsx": 3,
+ "components/interfaces/Storage/StorageSettings/StorageSettings.tsx": 2,
+ "components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx": 2,
+ "components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx": 3,
+ "components/interfaces/Support/AttachmentUpload.tsx": 3,
+ "components/interfaces/Support/LinkSupportTicketForm.tsx": 6,
+ "components/interfaces/Support/SupportFormV2.tsx": 1,
+ "components/interfaces/Support/__tests__/SupportFormPage.test.tsx": 19,
+ "components/interfaces/TableGridEditor/SidePanelEditor/ActionBar.tsx": 1,
+ "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnDefaultValue.tsx": 2,
+ "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx": 5,
+ "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts": 2,
+ "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx": 1,
+ "components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx": 2,
+ "components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx": 9,
+ "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx": 1,
+ "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/InputField.tsx": 9,
+ "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/DrilldownViewer/DrilldownPane.tsx": 1,
+ "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/DrilldownViewer/DrilldownViewer.tsx": 3,
+ "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/index.tsx": 3,
+ "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx": 9,
+ "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types.ts": 1,
+ "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.test.ts": 8,
+ "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts": 9,
+ "components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx": 2,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SchemaEditor.tsx": 2,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx": 14,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.types.ts": 1,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.test.ts": 6,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 14,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetFileUpload.tsx": 3,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetTextInput.tsx": 1,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx": 4,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.types.ts": 1,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils.tsx": 18,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx": 2,
+ "components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetPreviewGrid.tsx": 2,
+ "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 3,
+ "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx": 3,
+ "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeyRow.tsx": 1,
+ "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx": 2,
+ "components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts": 1,
+ "components/interfaces/TableGridEditor/TableDefinition.tsx": 3,
+ "components/interfaces/TableGridEditor/TableEntity.utils.ts": 1,
+ "components/interfaces/UnifiedLogs/ServiceFlow/components/ServiceFlowHeader.tsx": 1,
+ "components/interfaces/UnifiedLogs/ServiceFlow/components/blocks/ResponseCompletedBlock.tsx": 2,
+ "components/interfaces/UnifiedLogs/ServiceFlow/components/shared/Block.tsx": 1,
+ "components/interfaces/UnifiedLogs/ServiceFlow/components/shared/CollapsibleSection.tsx": 4,
+ "components/interfaces/UnifiedLogs/ServiceFlow/components/shared/FieldWithSeeMore.tsx": 4,
+ "components/interfaces/UnifiedLogs/ServiceFlow/components/shared/TimelineStep.tsx": 1,
+ "components/interfaces/UnifiedLogs/ServiceFlow/config/fieldHelpers.ts": 3,
+ "components/interfaces/UnifiedLogs/ServiceFlow/types.ts": 10,
+ "components/interfaces/UnifiedLogs/ServiceFlow/utils/storageUtils.ts": 3,
+ "components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts": 5,
+ "components/interfaces/UnifiedLogs/UnifiedLogs.tsx": 8,
+ "components/interfaces/UnifiedLogs/UnifiedLogs.utils.ts": 1,
+ "components/interfaces/UnifiedLogs/components/DownloadLogsButton.tsx": 1,
+ "components/interfaces/UnifiedLogs/components/LogsListPanel.tsx": 1,
+ "components/layouts/AccountLayout/WithSidebar.tsx": 3,
+ "components/layouts/DocsLayout/DocsLayout.tsx": 2,
+ "components/layouts/IntegrationsLayout/Integrations.utils.ts": 2,
+ "components/layouts/LogsLayout/LogsSidebarMenuV2.tsx": 1,
+ "components/layouts/ObservabilityLayout/ObservabilityMenu.tsx": 1,
+ "components/layouts/ProjectLayout/BuildingState.tsx": 1,
+ "components/layouts/ProjectLayout/LayoutHeader/BreadcrumbsView.tsx": 2,
+ "components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx": 3,
+ "components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx": 1,
+ "components/layouts/ProjectLayout/NavigationBar/NavigationIconLink.tsx": 1,
+ "components/layouts/ReportsLayout/ReportsMenu.tsx": 1,
+ "components/layouts/SQLEditorLayout/SQLEditorMenu.tsx": 2,
+ "components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx": 3,
+ "components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.utils.ts": 2,
+ "components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx": 2,
+ "components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx": 1,
+ "components/layouts/TableEditorLayout/EntityListItem.tsx": 3,
+ "components/layouts/TableEditorLayout/TableEditorMenu.tsx": 2,
+ "components/layouts/Tabs/NewTab.tsx": 1,
+ "components/to-be-cleaned/KeyMap.tsx": 3,
+ "components/ui/AIAssistantPanel/AIAssistant.utils.ts": 1,
+ "components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx": 1,
+ "components/ui/AIAssistantPanel/Message.Parts.tsx": 1,
+ "components/ui/AIAssistantPanel/MessageMarkdown.tsx": 1,
+ "components/ui/AIEditor/index.tsx": 2,
+ "components/ui/CardButton.tsx": 2,
+ "components/ui/Charts/AreaChart.tsx": 1,
+ "components/ui/Charts/BarChart.tsx": 2,
+ "components/ui/Charts/ChartHandler.tsx": 4,
+ "components/ui/Charts/ChartHeader.tsx": 5,
+ "components/ui/Charts/Charts.constants.ts": 1,
+ "components/ui/Charts/Charts.utils.tsx": 3,
+ "components/ui/Charts/ComposedChart.tsx": 10,
+ "components/ui/Charts/ComposedChart.utils.tsx": 13,
+ "components/ui/Charts/ComposedChartHandler.tsx": 9,
+ "components/ui/Charts/StackedBarChart.tsx": 3,
+ "components/ui/Charts/useChartHighlight.tsx": 9,
+ "components/ui/Charts/useChartHoverState.test.tsx": 1,
+ "components/ui/CodeEditor/CodeEditor.tsx": 8,
+ "components/ui/CodeEditor/CodeEditor.utils.ts": 2,
+ "components/ui/CodeEditor/Providers/BackwardIterator.ts": 1,
+ "components/ui/CodeEditor/Providers/PgSQLCompletionProvider.ts": 21,
+ "components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider.ts": 4,
+ "components/ui/DataTable/DataTable.utils.ts": 8,
+ "components/ui/DataTable/DataTableFilters/DataTableFilterCommand.tsx": 1,
+ "components/ui/DataTable/DataTableFilters/DataTableFilters.utils.ts": 1,
+ "components/ui/DataTable/DataTableInfinite.tsx": 6,
+ "components/ui/DataTable/DataTableViewOptions.tsx": 1,
+ "components/ui/DataTable/LiveButton.tsx": 1,
+ "components/ui/DataTable/providers/DataTableProvider.tsx": 3,
+ "components/ui/DatePicker/DatePicker.types.ts": 2,
+ "components/ui/DatePicker/index.tsx": 2,
+ "components/ui/DateRangePicker.tsx": 1,
+ "components/ui/DebouncedComponent.tsx": 1,
+ "components/ui/DownloadResultsButton.tsx": 2,
+ "components/ui/Error.tsx": 1,
+ "components/ui/ErrorBoundary/ErrorBoundary.tsx": 2,
+ "components/ui/ErrorBoundary/GlobalErrorBoundaryState.tsx": 1,
+ "components/ui/FilterPopover.tsx": 1,
+ "components/ui/Forms/Form.constants.ts": 1,
+ "components/ui/InfiniteList.tsx": 1,
+ "components/ui/Logs/LogsExplorerHeader.tsx": 2,
+ "components/ui/PasswordStrengthBar.tsx": 4,
+ "components/ui/ProductMenu/ProductMenu.types.ts": 1,
+ "components/ui/ProjectSettings/DisplayApiSettings.tsx": 1,
+ "components/ui/QueryBlock/QueryBlock.tsx": 4,
+ "components/ui/QueryBlock/QueryBlock.utils.ts": 1,
+ "components/ui/SchemaComboBox.tsx": 1,
+ "components/ui/SchemaSelector.tsx": 1,
+ "components/ui/SqlEditor.tsx": 6,
+ "components/ui/TwoOptionToggle.tsx": 4,
+ "components/ui/ui.types.ts": 1,
+ "data/ai/check-api-key-query.ts": 1,
+ "data/ai/rate-message-mutation.ts": 1,
+ "data/ai/sql-cron-mutation.ts": 1,
+ "data/ai/sql-title-mutation.ts": 1,
+ "data/analytics/functions-combined-stats-query.ts": 1,
+ "data/analytics/functions-req-stats-query.ts": 1,
+ "data/analytics/functions-resource-usage-query.ts": 1,
+ "data/analytics/infra-monitoring-queries.ts": 1,
+ "data/analytics/infra-monitoring-query.ts": 1,
+ "data/analytics/project-daily-stats-queries.ts": 1,
+ "data/analytics/project-metrics-query.ts": 3,
+ "data/api-settings/create-and-expose-api-schema-mutation.ts": 1,
+ "data/auth/auth-overview-query.ts": 1,
+ "data/auth/session-access-token-query.ts": 1,
+ "data/config/project-disk-resize-mutation.ts": 1,
+ "data/config/project-storage-config-query.ts": 2,
+ "data/content/content-insert-mutation.ts": 1,
+ "data/custom-domains/custom-domains-query.ts": 1,
+ "data/database-columns/database-column-create-mutation.ts": 1,
+ "data/database-queues/database-queues-metrics-query.ts": 1,
+ "data/database-triggers/database-triggers-query.ts": 2,
+ "data/database/table-columns-query.ts": 1,
+ "data/docs/project-json-schema-query.ts": 1,
+ "data/documents/dpa-request-mutation.ts": 1,
+ "data/edge-functions/edge-function-test-mutation.ts": 1,
+ "data/edge-functions/edge-functions-deploy-mutation.ts": 2,
+ "data/fdw/fdw-create-mutation.ts": 1,
+ "data/fdw/fdw-update-mutation.ts": 1,
+ "data/fetchers.ts": 12,
+ "data/foreign-tables/foreign-tables-query.ts": 1,
+ "data/graphql/fragment-masking.ts": 29,
+ "data/graphql/gql.ts": 1,
+ "data/graphql/graphql.ts": 2,
+ "data/integrations/integrations.types.ts": 1,
+ "data/invoices/invoices-count-query.ts": 1,
+ "data/jwt-signing-keys/jwt-signing-key-create-mutation.ts": 1,
+ "data/log-drains/create-log-drain-mutation.ts": 1,
+ "data/log-drains/update-log-drain-mutation.ts": 1,
+ "data/logs/get-unified-logs.ts": 1,
+ "data/logs/unified-log-inspection-query.ts": 2,
+ "data/logs/unified-logs-chart-query.ts": 1,
+ "data/logs/unified-logs-count-query.ts": 1,
+ "data/logs/unified-logs-infinite-query.ts": 2,
+ "data/materialized-views/materialized-views-query.ts": 1,
+ "data/network-restrictions/network-restrictions-query.ts": 3,
+ "data/notifications/keys.ts": 1,
+ "data/notifications/notifications-v2-query.ts": 2,
+ "data/oauth-secrets/client-secret-create-mutation.ts": 2,
+ "data/open-api/api-spec-query.ts": 8,
+ "data/organization-members/organization-roles-query.ts": 1,
+ "data/organizations/organization-create-mutation.ts": 2,
+ "data/organizations/organization-customer-profile-update-mutation.ts": 2,
+ "data/organizations/organization-payment-method-default-mutation.ts": 2,
+ "data/organizations/organization-update-mutation.ts": 1,
+ "data/platform/platform-status-query.ts": 1,
+ "data/profile/profile-identities-query.ts": 1,
+ "data/profile/profile-unlink-identity-mutation.ts": 2,
+ "data/projects/project-create-mutation.ts": 1,
+ "data/projects/project-detail-query.ts": 2,
+ "data/replication/restart-pipeline-helper.ts": 2,
+ "data/replication/rollback-table-mutation.ts": 1,
+ "data/replication/use-table-reset.ts": 1,
+ "data/reports/database-charts.ts": 5,
+ "data/reports/v2/auth.config.ts": 16,
+ "data/reports/v2/edge-functions.config.ts": 9,
+ "data/reports/v2/edge-functions.test.tsx": 2,
+ "data/reports/v2/reports.types.ts": 2,
+ "data/sql/execute-sql-query.ts": 5,
+ "data/ssl-enforcement/ssl-enforcement-query.ts": 2,
+ "data/ssl-enforcement/ssl-enforcement-update-mutation.ts": 1,
+ "data/sso/sso-config-query.ts": 2,
+ "data/storage/analytics-bucket-delete-mutation.ts": 1,
+ "data/storage/bucket-delete-mutation.ts": 1,
+ "data/storage/bucket-update-mutation.ts": 3,
+ "data/subscriptions/org-subscription-confirm-pending-create.ts": 1,
+ "data/subscriptions/org-subscription-update-mutation.ts": 2,
+ "data/support/generate-attachment-urls-mutation.ts": 1,
+ "data/table-rows/get-cell-value-mutation.ts": 1,
+ "data/table-rows/keys.ts": 2,
+ "data/table-rows/table-row-create-mutation.ts": 1,
+ "data/table-rows/table-row-update-mutation.ts": 2,
+ "data/table-rows/table-rows-query.ts": 4,
+ "data/tables/tables-query.ts": 1,
+ "data/vault/vault-secret-decrypted-value-query.ts": 2,
+ "data/views/views-query.ts": 1,
+ "hooks/analytics/useDbQuery.tsx": 1,
+ "hooks/analytics/useFillTimeseriesSorted.ts": 3,
+ "hooks/analytics/useLogsPreview.tsx": 1,
+ "hooks/analytics/useLogsQuery.tsx": 1,
+ "hooks/analytics/useLogsUrlState.ts": 1,
+ "hooks/analytics/useSingleLog.tsx": 1,
+ "hooks/analytics/useTimeseriesUnixToIso.ts": 1,
+ "hooks/branches/useBranchMergeDiff.ts": 3,
+ "hooks/misc/withAuth.tsx": 1,
+ "hooks/storage/useStoragePolicyCounts.ts": 2,
+ "hooks/ui/useClickedOutside.ts": 2,
+ "hooks/ui/useFlag.ts": 1,
+ "hooks/ui/useHotKey.ts": 1,
+ "instrumentation-client.ts": 5,
+ "lib/ai/model.ts": 2,
+ "lib/ai/model.utils.ts": 4,
+ "lib/ai/test-fixtures.ts": 1,
+ "lib/ai/tool-filter.test.ts": 5,
+ "lib/ai/tool-filter.ts": 4,
+ "lib/api/apiAuthenticate.test.ts": 2,
+ "lib/api/apiHelpers.test.ts": 1,
+ "lib/api/apiHelpers.ts": 2,
+ "lib/api/apiWrappers.test.ts": 2,
+ "lib/api/generate-v4.test.ts": 2,
+ "lib/api/rate.test.ts": 2,
+ "lib/api/self-hosted/logs.ts": 2,
+ "lib/gotrue.ts": 1,
+ "lib/helpers.test.ts": 10,
+ "lib/helpers.ts": 12,
+ "lib/integration-utils.test.ts": 1,
+ "lib/integration-utils.ts": 2,
+ "lib/password-strength.ts": 1,
+ "lib/pg-format.ts": 6,
+ "lib/pingPostgrest.test.ts": 1,
+ "lib/posthog.test.ts": 4,
+ "lib/profile.tsx": 1,
+ "lib/role-impersonation.test.ts": 5,
+ "lib/role-impersonation.ts": 1,
+ "lib/sanitize.test.ts": 10,
+ "lib/semver.test.ts": 3,
+ "lib/telemetry/track.ts": 1,
+ "lib/upload.test.ts": 1,
+ "pages/_app.tsx": 1,
+ "pages/account/tokens.tsx": 1,
+ "pages/api/ai/feedback/rate.ts": 2,
+ "pages/api/ai/sql/generate-v4.ts": 3,
+ "pages/api/check-cname.ts": 1,
+ "pages/api/edge-functions/test.ts": 1,
+ "pages/api/platform/auth/[ref]/users/[id]/factors.ts": 1,
+ "pages/api/platform/database/[ref]/pooling.ts": 1,
+ "pages/api/platform/integrations/github/authorization.ts": 1,
+ "pages/api/platform/projects/[ref]/analytics/log-drains.ts": 1,
+ "pages/api/platform/projects/[ref]/content/index.ts": 1,
+ "pages/api/platform/projects/[ref]/content/item/[id].ts": 1,
+ "pages/api/platform/projects/[ref]/databases.ts": 1,
+ "pages/cli/login.tsx": 1,
+ "pages/integrations/vercel/[slug]/deploy-button/new-project.tsx": 1,
+ "pages/integrations/vercel/[slug]/marketplace/choose-project.tsx": 1,
+ "pages/new/index.tsx": 1,
+ "pages/project/[ref]/api/index.tsx": 3,
+ "pages/project/[ref]/auth/templates/[templateId].tsx": 1,
+ "pages/project/[ref]/functions/[functionSlug]/index.tsx": 3,
+ "pages/project/[ref]/logs/explorer/index.tsx": 3,
+ "pages/project/[ref]/merge.tsx": 1,
+ "pages/project/[ref]/observability/database.tsx": 2,
+ "pages/project/[ref]/settings/log-drains.tsx": 1,
+ "pages/project/[ref]/storage/files/buckets/[bucketId].tsx": 1,
+ "scripts/__tests__/ratchet-eslint-rules.test.ts": 1,
+ "state/ai-assistant-state.tsx": 6,
+ "state/editor-panel-state.tsx": 4,
+ "state/role-impersonation-state.tsx": 1,
+ "state/sql-editor-v2.ts": 5,
+ "state/storage-explorer.tsx": 16,
+ "state/table-editor-table.tsx": 9,
+ "state/table-editor.tsx": 5,
+ "state/tabs.tsx": 1,
+ "tests/components/Editor/SpreadsheetImport.utils.test.ts": 1,
+ "tests/components/ui/Charts/Charts.utils.test.ts": 1,
+ "tests/features/logs/LogsPreviewer.test.tsx": 2,
+ "tests/features/logs/logs-query.test.tsx": 1,
+ "tests/lib/custom-render.tsx": 1,
+ "tests/setup/polyfills.ts": 3,
+ "tests/vitestSetup.ts": 1,
+ "types/form.ts": 1,
+ "types/next.ts": 2,
+ "types/ui.ts": 2
+ }
}
}
diff --git a/apps/studio/README.md b/apps/studio/README.md
index 155c40ee873..93fe45fda3a 100644
--- a/apps/studio/README.md
+++ b/apps/studio/README.md
@@ -34,7 +34,7 @@ Project settings are managed outside of the Dashboard. If you use docker compose
### Developer Quickstart
> [!NOTE]
-> **Supabase internal use:** To develop on Studio locally with the backend services, see the instructions in the [internal `infrastructure` repo](https://github.com/supabase/infrastructure/blob/develop/docs/contributing.md).
+> **Supabase internal use:** To develop on Studio locally with the backend services, see the instructions in the [internal `infrastructure` repo](https://github.com/supabase/platform/blob/develop/docs/contributing.md).
```bash
# You'll need to be on Node v20
@@ -45,8 +45,8 @@ pnpm install # install dependencies
pnpm run dev # start dev server
## For internal contributors
-## First clone the private supabase/infrastructure repo and follow instructions for setting up mise
-mise studio # Run from supabase/infrastructure alongside `mise infra`
+## First clone the private supabase/platform repo and follow instructions for setting up mise
+mise studio # Run from supabase/platform alongside `mise infra`
## For all
pnpm run test # run tests
diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx
index 6862463414f..20e0fa3a291 100644
--- a/apps/studio/components/grid/SupabaseGrid.tsx
+++ b/apps/studio/components/grid/SupabaseGrid.tsx
@@ -44,7 +44,7 @@ export const SupabaseGrid = ({
const gridRef = useRef(null)
const [mounted, setMounted] = useState(false)
- const { filters, onApplyFilters } = useTableFilter()
+ const { filters } = useTableFilter()
const { sorts, onApplySorts } = useTableSort()
const roleImpersonationState = useRoleImpersonationStateSnapshot()
@@ -104,8 +104,6 @@ export const SupabaseGrid = ({
isLoading={isLoading}
isSuccess={isSuccess}
isError={isError}
- filters={filters}
- onApplyFilters={onApplyFilters}
/>
diff --git a/apps/studio/components/grid/components/footer/Footer.tsx b/apps/studio/components/grid/components/footer/Footer.tsx
index 38bafa91679..44259704994 100644
--- a/apps/studio/components/grid/components/footer/Footer.tsx
+++ b/apps/studio/components/grid/components/footer/Footer.tsx
@@ -1,10 +1,11 @@
+import { parseAsString, useQueryState } from 'nuqs'
+
import { useParams } from 'common'
import { GridFooter } from 'components/ui/GridFooter'
import TwoOptionToggle from 'components/ui/TwoOptionToggle'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { isTableLike, isViewLike } from 'data/table-editor/table-editor-types'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
-import { useUrlState } from 'hooks/ui/useUrlState'
import { Pagination } from './pagination/Pagination'
type FooterProps = {
@@ -15,23 +16,13 @@ export const Footer: React.FC = ({ enableForeignRowsQuery = true }:
const { id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const { data: project } = useSelectedProjectQuery()
+ const [selectedView, setSelectedView] = useQueryState('view', parseAsString.withDefault('data'))
const { data: entity } = useTableEditorQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
id,
})
-
- const [{ view: selectedView = 'data' }, setUrlState] = useUrlState()
-
- const setSelectedView = (view: string) => {
- if (view === 'data') {
- setUrlState({ view: undefined })
- } else {
- setUrlState({ view })
- }
- }
-
const isViewSelected = isViewLike(entity)
const isTableSelected = isTableLike(entity)
diff --git a/apps/studio/components/grid/components/grid/Grid.tsx b/apps/studio/components/grid/components/grid/Grid.tsx
index 09905154e25..31eab0cdcb5 100644
--- a/apps/studio/components/grid/components/grid/Grid.tsx
+++ b/apps/studio/components/grid/components/grid/Grid.tsx
@@ -2,6 +2,7 @@ import { forwardRef, memo, Ref, useRef } from 'react'
import DataGrid, { CalculatedColumn, DataGridHandle } from 'react-data-grid'
import { ref as valtioRef } from 'valtio'
+import { useTableFilter } from 'components/grid/hooks/useTableFilter'
import { handleCopyCell } from 'components/grid/SupabaseGrid.utils'
import { formatForeignKeys } from 'components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.utils'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
@@ -14,7 +15,7 @@ import { useTableEditorStateSnapshot } from 'state/table-editor'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import { Button, cn } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
-import type { Filter, GridProps, SupaRow } from '../../types'
+import type { GridProps, SupaRow } from '../../types'
import { useOnRowsChange } from './Grid.utils'
import { GridError } from './GridError'
import RowRenderer from './RowRenderer'
@@ -30,8 +31,6 @@ interface IGrid extends GridProps {
isLoading: boolean
isSuccess: boolean
isError: boolean
- filters: Filter[]
- onApplyFilters: (appliedFilters: Filter[]) => void
}
// [Joshen] Just for visibility this is causing some hook errors in the browser
@@ -50,13 +49,12 @@ export const Grid = memo(
isLoading,
isSuccess,
isError,
- filters,
- onApplyFilters,
},
ref: Ref | undefined
) => {
const tableEditorSnap = useTableEditorStateSnapshot()
const snap = useTableEditorTableStateSnapshot()
+ const { filters, onApplyFilters } = useTableFilter()
const { data: org } = useSelectedOrganizationQuery()
const { data: project } = useSelectedProjectQuery()
diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx
index 2cc06c56aef..95982e4d22f 100644
--- a/apps/studio/components/grid/components/header/Header.tsx
+++ b/apps/studio/components/grid/components/header/Header.tsx
@@ -73,7 +73,7 @@ export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true
) : snap.selectedRows.size > 0 ? (
) : (
-
+
)}
@@ -81,7 +81,9 @@ export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true
)
}
-const DefaultHeader = () => {
+const DefaultHeader = ({
+ tableQueriesEnabled = true,
+}: Pick) => {
const { ref: projectRef } = useParams()
const { data: org } = useSelectedOrganizationQuery()
@@ -104,7 +106,7 @@ const DefaultHeader = () => {
-
+
{canAddNew && (
<>
@@ -231,7 +233,6 @@ type RowHeaderProps = {
}
const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
- debugger
const { data: project } = useSelectedProjectQuery()
const tableEditorSnap = useTableEditorStateSnapshot()
const snap = useTableEditorTableStateSnapshot()
diff --git a/apps/studio/components/grid/components/header/sort/SortPopover.tsx b/apps/studio/components/grid/components/header/sort/SortPopover.tsx
index f7cc7e20400..70afe576c34 100644
--- a/apps/studio/components/grid/components/header/sort/SortPopover.tsx
+++ b/apps/studio/components/grid/components/header/sort/SortPopover.tsx
@@ -7,17 +7,26 @@ import { SortPopoverPrimitive } from './SortPopoverPrimitive'
export interface SortPopoverProps {
portal?: boolean
+ tableQueriesEnabled?: boolean
}
-export const SortPopover = ({ portal = true }: SortPopoverProps) => {
+export const SortPopover = ({ portal = true, tableQueriesEnabled }: SortPopoverProps) => {
const { urlSorts, onApplySorts } = useTableSort()
- const tableState = useTableEditorTableStateSnapshot()
- const tableName = tableState?.table?.name || ''
+
+ const snap = useTableEditorTableStateSnapshot()
+ const tableName = snap.table?.name || ''
// Convert string[] to Sort[]
const sorts = useMemo(() => {
return tableName && urlSorts ? formatSortURLParams(tableName, urlSorts) : []
}, [tableName, urlSorts])
- return
+ return (
+
+ )
}
diff --git a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx
index 3a5f1d9510c..5c6e124a130 100644
--- a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx
+++ b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx
@@ -1,8 +1,18 @@
+import { THRESHOLD_COUNT } from '@supabase/pg-meta/src/query/table-row-query'
import { isEqual } from 'lodash'
import { ChevronDown, List } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { useParams } from 'common'
+import { useTableFilter } from 'components/grid/hooks/useTableFilter'
import type { Sort } from 'components/grid/types'
+import { InlineLink } from 'components/ui/InlineLink'
+import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
+import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
+import {
+ type RoleImpersonationState,
+ useRoleImpersonationStateSnapshot,
+} from 'state/role-impersonation-state'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import {
Button,
@@ -11,6 +21,7 @@ import {
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
+import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { DropdownControl } from '../../common/DropdownControl'
import SortRow from './SortRow'
@@ -19,6 +30,8 @@ export interface SortPopoverPrimitiveProps {
sorts: Sort[]
onApplySorts: (sorts: Sort[]) => void
portal?: boolean
+ defaultOpen?: boolean
+ tableQueriesEnabled?: boolean
}
/**
@@ -35,10 +48,20 @@ export const SortPopoverPrimitive = ({
sorts,
onApplySorts,
portal = true,
+ defaultOpen = false,
+ tableQueriesEnabled = true,
}: SortPopoverPrimitiveProps) => {
- const [open, setOpen] = useState(false)
- const snap = useTableEditorTableStateSnapshot()
+ const { ref } = useParams()
+ const { filters } = useTableFilter()
+ const { data: project } = useSelectedProjectQuery()
+ const roleImpersonationState = useRoleImpersonationStateSnapshot()
+ const snap = useTableEditorTableStateSnapshot()
+ const tableName = snap.table?.name || ''
+ const tableSchema = snap.table.schema || ''
+
+ const [open, setOpen] = useState(defaultOpen)
+ const [showWarning, setShowWarning] = useState(false)
// Local state for draft sorts
const [localSorts, setLocalSorts] = useState
(sorts)
@@ -47,21 +70,18 @@ export const SortPopoverPrimitive = ({
// Track if we're in the middle of applying our own changes
const isApplyingRef = useRef(false)
- // Sync with props when they change, but in a smarter way
- useEffect(() => {
- // If we're in the middle of applying changes, don't sync from props
- if (isApplyingRef.current) {
- isApplyingRef.current = false
- return
- }
-
- // If the props changed unexpectedly (not due to our own actions)
- // then we should update our local state
- if (!isEqual(sorts, lastSortsRef.current)) {
- setLocalSorts(sorts)
- lastSortsRef.current = sorts
- }
- }, [sorts])
+ const { data: countData } = useTableRowsCountQuery(
+ {
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ tableId: snap.table.id,
+ filters,
+ enforceExactCount: snap.enforceExactCount,
+ roleImpersonationState: roleImpersonationState as RoleImpersonationState,
+ },
+ { keepPreviousData: true, enabled: tableQueriesEnabled }
+ )
+ const isLargeTable = (countData?.count ?? 0) > THRESHOLD_COUNT
// Fix: Use localSorts for button text, not sorts
const displayButtonText =
@@ -180,67 +200,128 @@ export const SortPopoverPrimitive = ({
return `sort-${sort.table}-${sort.column}-${index}`
}
- const content = (
-
- {localSorts.map((sort, index) => (
-
- ))}
- {localSorts.length === 0 && (
-
-
No sorts applied to this view
-
Add a column below to sort the view
-
- )}
+ // Sync with props when they change, but in a smarter way
+ useEffect(() => {
+ // If we're in the middle of applying changes, don't sync from props
+ if (isApplyingRef.current) {
+ isApplyingRef.current = false
+ return
+ }
-
-
- {dropdownOptions && dropdownOptions.length > 0 ? (
-
- }
- className="sb-grid-dropdown__item-trigger"
- data-testid="table-editor-pick-column-to-sort-button"
- >
- Pick {localSorts.length > 1 ? 'another' : 'a'} column to sort by
-
-
- ) : (
-
All columns have been added
- )}
-
-
- Apply sorting
-
-
-
-
- )
+ // If the props changed unexpectedly (not due to our own actions)
+ // then we should update our local state
+ if (!isEqual(sorts, lastSortsRef.current)) {
+ setLocalSorts(sorts)
+ lastSortsRef.current = sorts
+ }
+ }, [sorts])
return (
-
-
- 0 ? 'link' : 'text'} icon={
}>
- {displayButtonText}
-
-
-
- {content}
-
-
+ <>
+
+
+ 0 ? 'link' : 'text'} icon={
}>
+ {displayButtonText}
+
+
+
+
+ {localSorts.map((sort, index) => (
+
+ ))}
+ {localSorts.length === 0 && (
+
+
No sorts applied to this view
+
+ Add a column below to sort the view
+
+
+ )}
+
+
+
+ {dropdownOptions && dropdownOptions.length > 0 ? (
+
+ }
+ className="sb-grid-dropdown__item-trigger"
+ data-testid="table-editor-pick-column-to-sort-button"
+ >
+ Pick {localSorts.length > 1 ? 'another' : 'a'} column to sort by
+
+
+ ) : (
+
All columns have been added
+ )}
+
+ {
+ if (isLargeTable && localSorts.length > 0) {
+ // [Joshen] Note we're only checking PKs - unable to check indexes properly
+ // as we are not able to deterministically figure out which columns the indexes are applied to
+ const hasSortNotPK = localSorts.some(
+ (x) => !snap.table.columns.find((y) => x.column === y.name)?.isPrimaryKey
+ )
+ if (hasSortNotPK) setShowWarning(true)
+ } else onSelectApplySorts()
+ }}
+ >
+ Apply sorting
+
+
+
+
+
+
+
+ {
+ onSelectApplySorts()
+ setShowWarning(false)
+ }}
+ onCancel={() => {
+ setLocalSorts(sorts)
+ setShowWarning(false)
+ }}
+ alert={{
+ base: { variant: 'warning' },
+ title: 'Be careful with sorting on unindexed columns',
+ description:
+ 'This may adversely impact your database, in particular if your table has a large number of rows - use with caution.',
+ }}
+ >
+
+ We highly recommend only sorting on columns which are{' '}
+
+ indexed
+
+ , such as your primary key columns.
+
+
+ >
)
}
diff --git a/apps/studio/components/interfaces/Account/AuditLogs.tsx b/apps/studio/components/interfaces/Account/AuditLogs.tsx
index 35a1ed4e3a6..9b77f7a6460 100644
--- a/apps/studio/components/interfaces/Account/AuditLogs.tsx
+++ b/apps/studio/components/interfaces/Account/AuditLogs.tsx
@@ -69,7 +69,9 @@ export const AuditLogs = () => {
)
?.filter((log) => {
if (filters.projects.length > 0) {
- return filters.projects.includes(log.target.metadata.project_ref || '')
+ return filters.projects.includes(
+ log.target.metadata.project_ref || log.target.metadata.ref || ''
+ )
} else {
return log
}
@@ -220,12 +222,12 @@ export const AuditLogs = () => {
]}
body={
sortedLogs?.map((log) => {
- const project = projects?.find(
- (project) => project.ref === log.target.metadata.project_ref
- )
- const organization = organizations?.find(
- (org) => org.slug === log.target.metadata.org_slug
- )
+ const logProjectRef =
+ log.target.metadata.project_ref || log.target.metadata.ref
+ const logOrgSlug = log.target.metadata.org_slug || log.target.metadata.slug
+
+ const project = projects?.find((project) => project.ref === logProjectRef)
+ const organization = organizations?.find((org) => org.slug === logOrgSlug)
const hasStatusCode = log.action.metadata[0]?.status !== undefined
@@ -261,16 +263,10 @@ export const AuditLogs = () => {
- {log.target.metadata.project_ref
- ? 'Ref: '
- : log.target.metadata.org_slug
- ? 'Slug: '
- : null}
- {log.target.metadata.project_ref ?? log.target.metadata.org_slug}
+ {logProjectRef ? 'Ref: ' : logOrgSlug ? 'Slug: ' : null}
+ {logProjectRef ?? logOrgSlug}
diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx
index 1b192abf3b4..0d408e1c83f 100644
--- a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx
+++ b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx
@@ -47,7 +47,7 @@ export function useApiKeysCommands() {
badge: () => (
Project: {project?.name}
- {publishableKey.type}
+ {publishableKey.type}
),
icon: () => ,
@@ -63,7 +63,7 @@ export function useApiKeysCommands() {
badge: () => (
Project: {project?.name}
- {key.type}
+ {key.type}
),
icon: () => ,
@@ -81,7 +81,7 @@ export function useApiKeysCommands() {
Project: {project?.name}
Public
- {anonKey.type}
+ {anonKey.type}
),
icon: () => ,
@@ -98,7 +98,7 @@ export function useApiKeysCommands() {
Project: {project?.name}
Secret
- {serviceKey.type}
+ {serviceKey.type}
),
icon: () => ,
diff --git a/apps/studio/components/interfaces/Auth/Hooks/AddHookDropdown.tsx b/apps/studio/components/interfaces/Auth/Hooks/AddHookDropdown.tsx
index eed802a8f73..a92e555dc76 100644
--- a/apps/studio/components/interfaces/Auth/Hooks/AddHookDropdown.tsx
+++ b/apps/studio/components/interfaces/Auth/Hooks/AddHookDropdown.tsx
@@ -52,10 +52,8 @@ export const AddHookDropdown = ({
const nonEnterpriseHookOptions = hooks.filter((h) => !isValidHook(h) && !h.enterprise)
const enterpriseHookOptions = hooks.filter((h) => !isValidHook(h) && h.enterprise)
- const hasAccessToAdvancedHooks =
- organization?.plan.id === 'team' ||
- organization?.plan.id === 'enterprise' ||
- organization?.plan.id === 'platform'
+ const isTeamsOrEnterprisePlan =
+ organization?.plan.id === 'team' || organization?.plan.id === 'enterprise'
if (!canUpdateAuthHook) {
return (
@@ -89,7 +87,7 @@ export const AddHookDropdown = ({
- {!hasAccessToAdvancedHooks && (
+ {!isTeamsOrEnterprisePlan && (
Team or Enterprise Plan required
@@ -99,10 +97,10 @@ export const AddHookDropdown = ({
)}
{enterpriseHookOptions.map((h) =>
- hasAccessToAdvancedHooks ? (
+ isTeamsOrEnterprisePlan ? (
onSelectHook(h.title)}
>
@@ -111,7 +109,7 @@ export const AddHookDropdown = ({
) : (
onSelectHook(h.title)}
>
{h.title}
diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx
index 8e77b34ab23..9db1788627d 100644
--- a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx
+++ b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx
@@ -1,8 +1,8 @@
import type { OAuthClient } from '@supabase/supabase-js'
import { Edit, MoreVertical, Plus, RotateCw, Search, Trash, X } from 'lucide-react'
import Link from 'next/link'
-import { parseAsBoolean, useQueryState } from 'nuqs'
-import { useRef, useState } from 'react'
+import { parseAsBoolean, parseAsStringLiteral, useQueryState } from 'nuqs'
+import { useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
@@ -30,6 +30,7 @@ import {
TableCell,
TableHead,
TableHeader,
+ TableHeadSort,
TableRow,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
@@ -44,6 +45,21 @@ import {
OAUTH_APP_REGISTRATION_TYPE_OPTIONS,
} from './oauthApps.utils'
+const OAUTH_APPS_SORT_VALUES = [
+ 'name:asc',
+ 'name:desc',
+ 'client_type:asc',
+ 'client_type:desc',
+ 'registration_type:asc',
+ 'registration_type:desc',
+ 'created_at:asc',
+ 'created_at:desc',
+] as const
+
+type OAuthAppsSort = (typeof OAUTH_APPS_SORT_VALUES)[number]
+type OAuthAppsSortColumn = OAuthAppsSort extends `${infer Column}:${string}` ? Column : unknown
+type OAuthAppsSortOrder = OAuthAppsSort extends `${string}:${infer Order}` ? Order : unknown
+
export const OAuthAppsList = () => {
const { ref: projectRef } = useParams()
const { data: authConfig, isLoading: isAuthConfigLoading } = useAuthConfigQuery({ projectRef })
@@ -108,12 +124,40 @@ export const OAuthAppsList = () => {
const [filterString, setFilterString] = useState('')
- const filteredOAuthApps = filterOAuthApps({
- apps: oAuthApps,
- searchString: filterString,
- registrationTypes: filteredRegistrationTypes,
- clientTypes: filteredClientTypes,
- })
+ const [sort, setSort] = useQueryState(
+ 'sort',
+ parseAsStringLiteral(OAUTH_APPS_SORT_VALUES).withDefault('name:asc')
+ )
+
+ const filteredAndSortedOAuthApps = useMemo(() => {
+ const filtered = filterOAuthApps({
+ apps: oAuthApps,
+ searchString: filterString,
+ registrationTypes: filteredRegistrationTypes,
+ clientTypes: filteredClientTypes,
+ })
+
+ const [sortCol, sortOrder] = sort.split(':') as [OAuthAppsSortColumn, OAuthAppsSortOrder]
+ const orderMultiplier = sortOrder === 'asc' ? 1 : -1
+
+ return filtered.sort((a, b) => {
+ if (sortCol === 'name') {
+ return a.client_name.localeCompare(b.client_name) * orderMultiplier
+ }
+ if (sortCol === 'client_type') {
+ return a.client_type.localeCompare(b.client_type) * orderMultiplier
+ }
+ if (sortCol === 'registration_type') {
+ return a.registration_type.localeCompare(b.registration_type) * orderMultiplier
+ }
+ if (sortCol === 'created_at') {
+ return (
+ (new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) * orderMultiplier
+ )
+ }
+ return 0
+ })
+ }, [oAuthApps, filterString, filteredRegistrationTypes, filteredClientTypes, sort])
const hasActiveFilters =
filterString.length > 0 ||
@@ -126,6 +170,22 @@ export const OAuthAppsList = () => {
setFilteredClientTypes([])
}
+ const handleSortChange = (column: OAuthAppsSortColumn) => {
+ const [currentCol, currentOrder] = sort.split(':') as [OAuthAppsSortColumn, OAuthAppsSortOrder]
+ if (currentCol === column) {
+ // Cycle through: asc -> desc -> no sort (default)
+ if (currentOrder === 'asc') {
+ setSort(`${column}:desc` as OAuthAppsSort)
+ } else {
+ // Reset to default sort (name:asc)
+ setSort('name:asc')
+ }
+ } else {
+ // New column, start with asc
+ setSort(`${column}:asc` as OAuthAppsSort)
+ }
+ }
+
if (isAuthConfigLoading || (isOAuthServerEnabled && isLoading)) {
return
}
@@ -223,26 +283,54 @@ export const OAuthAppsList = () => {
- Name
+
+
+ Name
+
+
Client ID
- Client Type
- Registration Type
- Created
+
+
+ Client Type
+
+
+
+
+ Registration Type
+
+
+
+
+ Created
+
+
- {filteredOAuthApps.length === 0 && (
+ {filteredAndSortedOAuthApps.length === 0 && (
No OAuth apps found
)}
- {filteredOAuthApps.length > 0 &&
- filteredOAuthApps.map((app) => (
+ {filteredAndSortedOAuthApps.length > 0 &&
+ filteredAndSortedOAuthApps.map((app) => (
0 ||
organization?.plan === undefined ||
organization.plan.id === 'free' ||
- organization.plan.id === 'enterprise' ||
- organization.plan.id === 'platform'
+ organization.plan.id === 'enterprise'
)
return null
diff --git a/apps/studio/components/interfaces/Billing/Subscription/Subscription.utils.ts b/apps/studio/components/interfaces/Billing/Subscription/Subscription.utils.ts
index 7d8ab4ec3e4..ece737507d6 100644
--- a/apps/studio/components/interfaces/Billing/Subscription/Subscription.utils.ts
+++ b/apps/studio/components/interfaces/Billing/Subscription/Subscription.utils.ts
@@ -45,35 +45,24 @@ export const getPlanChangeType = (
pro: 'upgrade',
team: 'upgrade',
enterprise: 'upgrade',
- platform: 'upgrade',
},
pro: {
free: 'downgrade',
pro: 'none',
team: 'upgrade',
enterprise: 'upgrade',
- platform: 'upgrade',
},
team: {
free: 'downgrade',
pro: 'downgrade',
team: 'none',
enterprise: 'upgrade',
- platform: 'upgrade',
},
enterprise: {
free: 'downgrade',
pro: 'downgrade',
team: 'downgrade',
enterprise: 'none',
- platform: 'upgrade',
- },
- platform: {
- free: 'downgrade',
- pro: 'downgrade',
- team: 'downgrade',
- enterprise: 'downgrade',
- platform: 'none',
},
}
diff --git a/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx b/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx
index d8662ed7686..e5ec6962633 100644
--- a/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx
+++ b/apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx
@@ -26,6 +26,11 @@ import { Admonition } from 'ui-patterns'
const orioleExtCallOuts = ['vector', 'postgis']
+// Extensions that have recommended schemas (rather than required schemas)
+const extensionsWithRecommendedSchemas: Record = {
+ wrappers: 'extensions',
+}
+
interface EnableExtensionModalProps {
visible: boolean
extension: PostgresExtension
@@ -89,6 +94,20 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
}
}, [visible, extension.name])
+ const getSchemaDescriptionText = (extensionName: string, schema: string | null | undefined) => {
+ // Prioritize defaultSchema (required/forced) over recommended schema
+ if (schema) {
+ return `Extension must be installed in the “${schema}” schema.`
+ }
+
+ const recommendedSchema = extensionsWithRecommendedSchemas[extensionName]
+ if (recommendedSchema) {
+ return `Use the “${recommendedSchema}” schema for full compatibility with related features.`
+ }
+
+ return undefined
+ }
+
const validate = (values: any) => {
const errors: any = {}
if (values.schema === 'custom' && !values.name) errors.name = 'Required field'
@@ -165,13 +184,14 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
name="schema"
value={defaultSchema}
label="Select a schema to enable the extension for"
- descriptionText={`Extension must be installed in ${defaultSchema}.`}
+ descriptionText={getSchemaDescriptionText(extension.name, defaultSchema)}
/>
) : (
{
const { data: project } = useSelectedProjectQuery()
const { schema: urlSchema, table } = useParams()
- const [search, setSearch] = useState('')
+ const [search, setSearch] = useQueryState('search', parseAsString.withDefault(''))
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const deletingIndexNameRef = useRef(null)
@@ -184,7 +184,7 @@ const Indexes = () => {
- {sortedIndexes.length === 0 && search.length === 0 && (
+ {indexes.length === 0 && search.length === 0 && (
No indexes created yet
@@ -194,7 +194,7 @@ const Indexes = () => {
)}
- {sortedIndexes.length === 0 && search.length > 0 && (
+ {indexes.length === 0 && search.length > 0 && (
No results found
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx
index 6c5d9346cca..cee10b749d2 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx
@@ -7,7 +7,6 @@ import * as z from 'zod'
import { useFlag, useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
-import { getCatalogURI } from 'components/interfaces/Storage/StorageSettings/StorageSettings.utils'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCheckPrimaryKeysExists } from 'data/database/primary-keys-exists-query'
@@ -221,19 +220,10 @@ export const DestinationPanel = ({
// Helper function to handle namespace creation if needed
const resolveNamespace = async (data: z.infer) => {
if (data.namespace === CREATE_NEW_NAMESPACE) {
- if (!data.newNamespaceName) {
- throw new Error('New namespace name is required')
- }
-
- // Construct catalog URI for namespace creation
- const protocol = projectSettings?.app_config?.protocol ?? 'https'
- const endpoint =
- projectSettings?.app_config?.storage_endpoint || projectSettings?.app_config?.endpoint
- const catalogUri = getCatalogURI(project?.ref ?? '', protocol, endpoint)
+ if (!data.newNamespaceName) throw new Error('New namespace name is required')
await createNamespace({
projectRef,
- catalogUri,
warehouse: data.warehouseName!,
namespace: data.newNamespaceName,
})
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx
index 992311dd4ca..99aaf046f8b 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanelFields.tsx
@@ -1,10 +1,9 @@
import { Eye, EyeOff, Loader2 } from 'lucide-react'
-import { useMemo, useState } from 'react'
+import { useState } from 'react'
import type { UseFormReturn } from 'react-hook-form'
import { useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
-import { getCatalogURI } from 'components/interfaces/Storage/StorageSettings/StorageSettings.utils'
import { InlineLink } from 'components/ui/InlineLink'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
@@ -154,30 +153,19 @@ export const AnalyticsBucketFields = ({
isError: isErrorBuckets,
} = useAnalyticsBucketsQuery({ projectRef })
- // Construct catalog URI for iceberg namespaces query
- const catalogUri = useMemo(() => {
- if (!project?.ref || !projectSettings) return ''
- const protocol = projectSettings.app_config?.protocol ?? 'https'
- const endpoint =
- projectSettings.app_config?.storage_endpoint || projectSettings.app_config?.endpoint
- return getCatalogURI(project.ref, protocol, endpoint)
- }, [project?.ref, projectSettings])
-
const canSelectNamespace = !!warehouseName && !!serviceApiKey
const {
data: namespaces = [],
isLoading: isLoadingNamespaces,
isError: isErrorNamespaces,
- refetch: refetchNamespaces,
} = useIcebergNamespacesQuery(
{
projectRef,
- catalogUri,
- warehouse: warehouseName || '',
+ warehouse: warehouseName,
},
{
- enabled: type === 'Analytics Bucket' && !!catalogUri && !!warehouseName && !!serviceApiKey,
+ enabled: type === 'Analytics Bucket' && !!serviceApiKey,
}
)
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.constants.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.constants.ts
index 0882b1bfecf..aa06149d371 100644
--- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.constants.ts
+++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.constants.ts
@@ -1,5 +1,5 @@
// [Joshen] These should eventually be in shared-types so that it can be used between infra and dashboard
-// https://github.com/supabase/infrastructure/blob/a466d85ece3179cf7b6da1b15c11f2ed2be49bd6/shared/src/volumes.ts#L5-L24
+// https://github.com/supabase/platform/blob/a466d85ece3179cf7b6da1b15c11f2ed2be49bd6/shared/src/volumes.ts#L5-L24
// https://docs.aws.amazon.com/ebs/latest/userguide/provisioned-iops.html#io2-bx-considerations
export const IO2_AVAILABLE_REGIONS = [
'ap-east-1',
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts
index 789a42f1dbc..a93062840e8 100644
--- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts
+++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts
@@ -50,8 +50,9 @@ export const CreateDiskStorageSchema = ({
}) => {
const isFlyProject = cloudProvider === 'FLY'
const isAwsNimbusProject = cloudProvider === 'AWS_NIMBUS'
+ const isAwsK8sProject = cloudProvider === 'AWS_K8S'
- const validateDiskConfiguration = !isFlyProject && !isAwsNimbusProject
+ const validateDiskConfiguration = !isFlyProject && !isAwsNimbusProject && !isAwsK8sProject
const schema = baseSchema.superRefine((data, ctx) => {
const { storageType, totalSize, provisionedIOPS, throughput, maxSizeGb } = data
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx
index 4de34f0e855..6a3b5a34bd2 100644
--- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx
+++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx
@@ -72,6 +72,9 @@ export function DiskManagementForm() {
const { data: org } = useSelectedOrganizationQuery()
const { setProjectStatus } = useSetProjectStatus()
+ const isSpendCapEnabled =
+ org?.plan.id !== 'free' && !org?.usage_billing_enabled && project?.cloud_provider !== 'FLY'
+
const { data: resourceWarnings } = useResourceWarningsQuery({ ref: projectRef })
// [Joshen Cleanup] JFYI this client side filtering can be cleaned up once BE changes are live which will only return the warnings based on the provided ref
const projectResourceWarnings = (resourceWarnings ?? [])?.find(
@@ -208,7 +211,7 @@ export function DiskManagementForm() {
const usedPercentage = (usedSize / totalSize) * 100
const disableIopsThroughputConfig =
- RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(form.watch('computeSize')) && !hasAccess
+ RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)
const isBranch = project?.parent_project_ref !== undefined
@@ -216,6 +219,7 @@ export function DiskManagementForm() {
isRequestingChanges ||
isPlanUpgradeRequired ||
isWithinCooldownWindow ||
+ isSpendCapEnabled ||
!canUpdateDiskConfiguration ||
!isAws
@@ -256,6 +260,7 @@ export function DiskManagementForm() {
// [Joshen] Skip disk configuration related stuff for AWS Nimbus
try {
if (
+ !isAwsK8s &&
!isAwsNimbus &&
(payload.storageType !== form.formState.defaultValues?.storageType ||
payload.provisionedIOPS !== form.formState.defaultValues?.provisionedIOPS ||
@@ -274,6 +279,7 @@ export function DiskManagementForm() {
}
if (
+ !isAwsK8s &&
!isAwsNimbus &&
(payload.growthPercent !== form.formState.defaultValues?.growthPercent ||
payload.minIncrementGb !== form.formState.defaultValues?.minIncrementGb ||
@@ -323,154 +329,160 @@ export function DiskManagementForm() {
}
description="You will need to upgrade to at least the Pro Plan to configure compute and disk"
/>
- {isProjectResizing || isProjectRequestingDiskChanges || noPermissions ? (
-
-
-
-
-
- ) : null}
+ {isProjectResizing ||
+ isProjectRequestingDiskChanges ||
+ (noPermissions && (
+
+
+
+
+
+ ))}
+
{!(isAws || isAwsNimbus) && }
-
+
+
-
- {isAws && (
- <>
-
-
- {!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
-
-
-
- )}
- {isReadOnlyMode && (
-
-
-
- )}
-
-
-
-
setAdvancedSettingsOpenState((prev) => !prev)}
- >
-
-
- Advanced disk settings
-
- Specify additional settings for your disk, including autoscaling
- configuration, IOPS, throughput, and disk type.
-
-
-
-
-
+
+ {isAws && (
+ <>
+
+
+ {!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
+
+
+
)}
+ {isReadOnlyMode && (
+
+
+
+ )}
+
+
+
+ setAdvancedSettingsOpenState((prev) => !prev)}
>
-
-
-
-
{
- form.setValue('computeSize', 'ci_large')
- }}
- >
- Change to LARGE Compute
-
- }
+
+
+ Advanced disk settings
+
+ Specify additional settings for your disk, including autoscaling
+ configuration, IOPS, throughput, and disk type.
+
+
+
-
-
-
-
-
-
- >
- )}
+
+
+
+
+
+ {
+ form.setValue('computeSize', 'ci_large')
+ }}
+ >
+ Change to LARGE Compute
+
+ }
+ />
+
+
+
+
+
+
+ >
+ )}
+
{isDirty ? (
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx
index 5845431ffb2..161057c7995 100644
--- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx
+++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx
@@ -152,6 +152,8 @@ export const DiskManagementReviewAndSubmitDialog = ({
const { data: org } = useSelectedOrganizationQuery()
const isAwsNimbus = useIsAwsNimbusCloudProvider()
+ const isAwsK8sProject = project?.cloud_provider === 'AWS_K8S'
+
const { formState, getValues } = form
const { can: canUpdateDiskConfiguration } = useAsyncCheckPermissions(
@@ -214,22 +216,34 @@ export const DiskManagementReviewAndSubmitDialog = ({
const hasComputeChanges =
form.formState.defaultValues?.computeSize !== form.getValues('computeSize')
const hasTotalSizeChanges =
- !isAwsNimbus && form.formState.defaultValues?.totalSize !== form.getValues('totalSize')
+ !isAwsK8sProject &&
+ !isAwsNimbus &&
+ form.formState.defaultValues?.totalSize !== form.getValues('totalSize')
const hasStorageTypeChanges =
- !isAwsNimbus && form.formState.defaultValues?.storageType !== form.getValues('storageType')
+ !isAwsK8sProject &&
+ !isAwsNimbus &&
+ form.formState.defaultValues?.storageType !== form.getValues('storageType')
const hasThroughputChanges =
- !isAwsNimbus && form.formState.defaultValues?.throughput !== form.getValues('throughput')
+ !isAwsK8sProject &&
+ !isAwsNimbus &&
+ form.formState.defaultValues?.throughput !== form.getValues('throughput')
const hasIOPSChanges =
+ !isAwsK8sProject &&
!isAwsNimbus &&
form.formState.defaultValues?.provisionedIOPS !== form.getValues('provisionedIOPS')
const hasGrowthPercentChanges =
- !isAwsNimbus && form.formState.defaultValues?.growthPercent !== form.getValues('growthPercent')
+ !isAwsK8sProject &&
+ !isAwsNimbus &&
+ form.formState.defaultValues?.growthPercent !== form.getValues('growthPercent')
const hasMinIncrementChanges =
+ !isAwsK8sProject &&
!isAwsNimbus &&
form.formState.defaultValues?.minIncrementGb !== form.getValues('minIncrementGb')
const hasMaxSizeChanges =
- !isAwsNimbus && form.formState.defaultValues?.maxSizeGb !== form.getValues('maxSizeGb')
+ !isAwsK8sProject &&
+ !isAwsNimbus &&
+ form.formState.defaultValues?.maxSizeGb !== form.getValues('maxSizeGb')
const hasDiskConfigChanges =
hasIOPSChanges ||
diff --git a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx
index 6cd51999223..92372df1baf 100644
--- a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx
+++ b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx
@@ -8,7 +8,7 @@ export enum DiskType {
IO2 = 'io2',
}
-// [Joshen] As per https://github.com/supabase/infrastructure/pull/20478
+// [Joshen] As per https://github.com/supabase/platform/pull/20478
export const DISK_AUTOSCALE_CONFIG_DEFAULTS = {
growthPercent: 50,
minIncrementSize: 4,
@@ -57,7 +57,6 @@ export const PLAN_DETAILS: Record = {
pro: { includedDiskGB: { gp3: 8, io2: 0 } },
team: { includedDiskGB: { gp3: 8, io2: 0 } },
enterprise: { includedDiskGB: { gp3: 8, io2: 0 } },
- platform: { includedDiskGB: { gp3: 8, io2: 0 } },
}
export const COMPUTE_BASELINE_IOPS = {
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx
index 576f19937ff..4fbdd2e7a2e 100644
--- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx
@@ -25,7 +25,7 @@ interface EdgeFunctionSecretProps {
const EdgeFunctionSecret = ({ secret, onSelectEdit, onSelectDelete }: EdgeFunctionSecretProps) => {
const { can: canUpdateSecrets } = useAsyncCheckPermissions(PermissionAction.SECRETS_WRITE, '*')
// [Joshen] Following API's validation:
- // https://github.com/supabase/infrastructure/blob/develop/api/src/routes/v1/projects/ref/secrets/secrets.controller.ts#L106
+ // https://github.com/supabase/platform/blob/develop/api/src/routes/v1/projects/ref/secrets/secrets.controller.ts#L106
const isReservedSecret = !!secret.name.match(/^(SUPABASE_).*/)
return (
diff --git a/apps/studio/components/interfaces/Home/ProjectUsage.tsx b/apps/studio/components/interfaces/Home/ProjectUsage.tsx
index 0a2ea44a51e..1afdaada2f1 100644
--- a/apps/studio/components/interfaces/Home/ProjectUsage.tsx
+++ b/apps/studio/components/interfaces/Home/ProjectUsage.tsx
@@ -35,7 +35,7 @@ import {
type ChartIntervalKey = ProjectLogStatsVariables['interval']
-const LOG_RETENTION = { free: 1, pro: 7, team: 28, enterprise: 90, platform: 1 }
+const LOG_RETENTION = { free: 1, pro: 7, team: 28, enterprise: 90 }
const CHART_INTERVALS: ChartIntervals[] = [
{
@@ -44,7 +44,7 @@ const CHART_INTERVALS: ChartIntervals[] = [
startValue: 1,
startUnit: 'hour',
format: 'MMM D, h:mma',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
key: '1day',
@@ -52,7 +52,7 @@ const CHART_INTERVALS: ChartIntervals[] = [
startValue: 24,
startUnit: 'hour',
format: 'MMM D, ha',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
key: '7day',
diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx
index 62c1d19864e..1b65332cb5f 100644
--- a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx
+++ b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx
@@ -1,10 +1,10 @@
+import { BASE_PATH } from 'lib/constants'
import { Check, ChevronLeft, ChevronRight } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { useEffect, useState } from 'react'
-import { cn, Button, Card, CardContent, Badge } from 'ui'
-import { GettingStartedStep, GettingStartedAction } from './GettingStarted.types'
-import { BASE_PATH } from 'lib/constants'
+import { Badge, Button, Card, CardContent, cn } from 'ui'
+import { GettingStartedAction, GettingStartedStep } from './GettingStarted.types'
// Determine action type for tracking
const getActionType = (action: GettingStartedAction): 'primary' | 'ai_assist' | 'external_link' => {
@@ -186,7 +186,7 @@ export function GettingStarted({ steps, onStepClick }: GettingStartedProps) {
{activeStep.title}
{activeStep.status}
diff --git a/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx b/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx
index f3a1e6da98a..7fb2e02baf8 100644
--- a/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx
+++ b/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx
@@ -43,7 +43,7 @@ import {
formatDelta,
} from './ProjectUsage.metrics'
-const LOG_RETENTION = { free: 1, pro: 7, team: 28, enterprise: 90, platform: 1 }
+const LOG_RETENTION = { free: 1, pro: 7, team: 28, enterprise: 90 }
const CHART_INTERVALS: ChartIntervals[] = [
{
@@ -52,7 +52,7 @@ const CHART_INTERVALS: ChartIntervals[] = [
startValue: 1,
startUnit: 'hour',
format: 'MMM D, h:mma',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
key: '1day',
@@ -60,7 +60,7 @@ const CHART_INTERVALS: ChartIntervals[] = [
startValue: 24,
startUnit: 'hour',
format: 'MMM D, ha',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
key: '7day',
diff --git a/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx b/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx
index 4e28f74b065..061b5a5528f 100644
--- a/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx
+++ b/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx
@@ -105,7 +105,7 @@ export const IntegrationInstallation = forwardRef
- {integration.metadata?.account.type}
+ {integration.metadata?.account.type}
diff --git a/apps/studio/components/interfaces/JwtSecrets/algorithm-hover-card.tsx b/apps/studio/components/interfaces/JwtSecrets/algorithm-hover-card.tsx
index 1d69c0051a0..0a2d40f6a10 100644
--- a/apps/studio/components/interfaces/JwtSecrets/algorithm-hover-card.tsx
+++ b/apps/studio/components/interfaces/JwtSecrets/algorithm-hover-card.tsx
@@ -1,6 +1,6 @@
-import { LockKeyholeOpen, RectangleEllipsis } from 'lucide-react'
+import { ExternalLink, HelpCircle, LockKeyholeOpen, RectangleEllipsis } from 'lucide-react'
-import { InfoPill } from 'components/ui/InfoPill'
+import { HoverCard, HoverCardContent, HoverCardTrigger } from 'ui'
import { AlgorithmDetail, algorithmDetails } from './algorithm-details'
interface AlgorithmHoverCardProps {
@@ -12,39 +12,57 @@ export const AlgorithmHoverCard = ({ algorithm, legacy }: AlgorithmHoverCardProp
const details: AlgorithmDetail = algorithmDetails[algorithm]
return (
- {legacy ? `Legacy ${details.label}` : details.label} }
- icon={
- algorithm === 'HS256' ? (
-
- ) : (
-
- )
- }
- title={details.name}
- description={
-
-
{details.description}
-
- Pros:
-
)
}
diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx
index 786f48eea0e..44615179bdd 100644
--- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx
+++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx
@@ -236,6 +236,7 @@ export const JWTSecretKeysTable = () => {
Type
+
Actions
diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/key-details-dialog.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/key-details-dialog.tsx
index 3a12789a6c9..3f99b86acc8 100644
--- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/key-details-dialog.tsx
+++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/key-details-dialog.tsx
@@ -1,19 +1,18 @@
-import { useMemo } from 'react'
import { FileKey } from 'lucide-react'
+import { useMemo } from 'react'
+
+import { JWTSigningKey } from 'data/jwt-signing-keys/jwt-signing-keys-query'
import {
- DialogContent,
- DialogDescription,
+ Button,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
Input,
- Textarea,
Label_Shadcn_,
- Button,
+ Textarea,
} from 'ui'
-import { JWTSigningKey } from 'data/jwt-signing-keys/jwt-signing-keys-query'
export function KeyDetailsDialog({
selectedKey,
diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/rotate-key-dialog.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/rotate-key-dialog.tsx
index 40ae299dc8f..68002a01837 100644
--- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/rotate-key-dialog.tsx
+++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/rotate-key-dialog.tsx
@@ -1,10 +1,11 @@
+import { ArrowRight, ExternalLink, Info, Key, Timer } from 'lucide-react'
+import { useState } from 'react'
+import { toast } from 'sonner'
+
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { useJWTSigningKeyUpdateMutation } from 'data/jwt-signing-keys/jwt-signing-key-update-mutation'
import { JWTSigningKey } from 'data/jwt-signing-keys/jwt-signing-keys-query'
-import { ArrowRight, ExternalLink, Info, Key, Timer } from 'lucide-react'
-import { useState } from 'react'
-import { toast } from 'sonner'
import {
Badge,
Button,
@@ -71,7 +72,7 @@ export function RotateKeyDialog({
'px-4 py-1 gap-2 flex flex-row items-center uppercase'
)}
>
-
+
Standby key
@@ -84,7 +85,7 @@ export function RotateKeyDialog({
'px-4 py-1 gap-2 flex flex-row items-center uppercase'
)}
>
-
+
Current key
@@ -102,7 +103,7 @@ export function RotateKeyDialog({
'px-4 py-1 gap-2 flex flex-row items-center uppercase'
)}
>
-
+
Current key
@@ -114,7 +115,7 @@ export function RotateKeyDialog({
'px-4 py-1 gap-2 flex flex-row items-center uppercase'
)}
>
-
+
Previous key
diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx
index cd0ded6d0dd..838a20af78b 100644
--- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx
+++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx
@@ -1,4 +1,3 @@
-import dayjs from 'dayjs'
import { motion } from 'framer-motion'
import {
CircleArrowDown,
@@ -14,6 +13,7 @@ import {
import { components } from 'api-types'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
import { JWTSigningKey } from 'data/jwt-signing-keys/jwt-signing-keys-query'
+import dayjs from 'dayjs'
import {
Badge,
Button,
@@ -25,6 +25,7 @@ import {
TableCell,
TableRow,
} from 'ui'
+import { TimestampInfo } from 'ui-patterns'
import { AlgorithmHoverCard } from '../algorithm-hover-card'
import { statusColors, statusLabels } from '../jwt.constants'
@@ -77,15 +78,15 @@ export const SigningKeyRow = ({
)}
>
{signingKey.status === 'standby' ? (
-
+
) : (
-
+
)}
-
{statusLabels[signingKey.status]}
+
{statusLabels[signingKey.status]}
-
+
-
+
{signingKey.id}
-
+
- {(signingKey.status === 'previously_used' || signingKey.status === 'revoked') && (
-
- {dayjs(signingKey.updated_at).fromNow()}
+ {signingKey.status === 'previously_used' || signingKey.status === 'revoked' ? (
+
+
+ ) : (
+
)}
{(signingKey.status !== 'in_use' || signingKey.algorithm !== 'HS256') && (
diff --git a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx
index 69b50dc194c..56d2ed914a0 100644
--- a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx
+++ b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx
@@ -24,6 +24,7 @@ import {
import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useOrgProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query'
+import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import {
AlertDescription_Shadcn_,
@@ -32,7 +33,6 @@ import {
Button,
WarningIcon,
} from 'ui'
-import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
const logsUpgradeError = 'upgrade to Team or Enterprise Plan to access audit logs.'
@@ -348,12 +348,12 @@ export const AuditLogs = () => {
(member) => member.gotrue_id === log.actor.id
)
const role = roles.find((role) => user?.role_ids?.[0] === role.id)
- const project = projects?.find(
- (project) => project.ref === log.target.metadata.project_ref
- )
- const organization = organizations?.find(
- (org) => org.slug === log.target.metadata.org_slug
- )
+ const logProjectRef =
+ log.target.metadata.project_ref ?? log.target.metadata.ref
+ const logOrgSlug = log.target.metadata.org_slug ?? log.target.metadata.slug
+
+ const project = projects?.find((project) => project.ref === logProjectRef)
+ const organization = organizations?.find((org) => logOrgSlug)
const hasStatusCode = log.action.metadata[0]?.status !== undefined
const userIcon =
@@ -420,16 +420,10 @@ export const AuditLogs = () => {
- {log.target.metadata.project_ref
- ? 'Ref: '
- : log.target.metadata.org_slug
- ? 'Slug: '
- : null}
- {log.target.metadata.project_ref ?? log.target.metadata.org_slug}
+ {logProjectRef ? 'Ref: ' : logOrgSlug ? 'Slug: ' : null}
+ {logProjectRef ?? logOrgSlug}
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx
index 4b7feab3e57..b11a717a703 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx
@@ -57,7 +57,6 @@ const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => {
isSuccess,
} = useOrgUpcomingInvoiceQuery({ orgSlug: slug })
- // For non-platform customers, compute is broken down per project and contains a breakdown array
const computeItems =
upcomingInvoice?.lines?.filter(
(item) =>
@@ -82,8 +81,7 @@ const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => {
const otherItems =
upcomingInvoice?.lines?.filter(
(item) =>
- // In case we have no per-project breakdown for compute, we treat it as a regular item for display purposes
- (!item.usage_metric?.toString()?.startsWith('COMPUTE_HOURS') || !item.breakdown?.length) &&
+ !item.description?.toLowerCase().includes('compute') &&
!item.description?.toLowerCase().includes('plan')
) || []
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx
index 3d8c77b845d..edf665e233e 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx
@@ -51,7 +51,7 @@ const CostControl = ({}: CostControlProps) => {
const isUsageBillingEnabled = subscription?.usage_billing_enabled ?? false
const canChangeTier =
- !projectUpdateDisabled && !['team', 'enterprise', 'platform'].includes(currentPlan?.id || '')
+ !projectUpdateDisabled && !['team', 'enterprise'].includes(currentPlan?.id || '')
const costControlDisabled = selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE
@@ -122,7 +122,7 @@ const CostControl = ({}: CostControlProps) => {
{isSuccess && !costControlDisabled && (
- {['team', 'enterprise', 'platform'].includes(currentPlan?.id || '') ? (
+ {['team', 'enterprise'].includes(currentPlan?.id || '') ? (
{
type={isDowngradeOption ? 'default' : 'primary'}
disabled={
subscription?.plan?.id === 'enterprise' ||
- subscription?.plan?.id === 'platform' ||
// Downgrades to free are still allowed through the dashboard given we have much better control about showing customers the impact + any possible issues with downgrading to free
(selectedOrganization?.managed_by !== MANAGED_BY.SUPABASE &&
plan.id !== 'tier_free') ||
@@ -263,9 +262,8 @@ export const PlanUpdateSidePanel = () => {
side: 'bottom',
className: hasOrioleProjects ? 'w-96 text-center' : '',
text:
- subscription?.plan?.id === 'enterprise' ||
- subscription?.plan?.id === 'platform'
- ? 'Reach out to us via support to update your plan'
+ subscription?.plan?.id === 'enterprise'
+ ? 'Reach out to us via support to update your plan from Enterprise'
: hasOrioleProjects
? 'Your organization has projects that are using the OrioleDB extension which is only available on the Free plan. Remove all OrioleDB projects before changing your plan.'
: !canUpdateSubscription
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx
index 71382697cc8..2e9a052a10e 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx
@@ -41,8 +41,7 @@ const Subscription = () => {
const currentPlan = subscription?.plan
const planName = currentPlan?.name ?? 'Unknown'
- const canChangeTier =
- !projectUpdateDisabled && !['enterprise', 'platform'].includes(currentPlan?.id ?? '')
+ const canChangeTier = !projectUpdateDisabled && !['enterprise'].includes(currentPlan?.id ?? '')
return (
<>
diff --git a/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx b/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx
index 08b0072ac39..373db6d7bea 100644
--- a/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx
+++ b/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx
@@ -69,7 +69,7 @@ export const SSOConfig = () => {
const { data: organization } = useSelectedOrganizationQuery()
const plan = organization?.plan.id
- const canSetupSSOConfig = ['team', 'enterprise', 'platform'].includes(plan ?? '')
+ const canSetupSSOConfig = ['team', 'enterprise'].includes(plan ?? '')
const {
data: ssoConfig,
diff --git a/apps/studio/components/interfaces/Organization/Usage/Activity.tsx b/apps/studio/components/interfaces/Organization/Usage/Activity.tsx
index 394694da3a6..4ee8fc90366 100644
--- a/apps/studio/components/interfaces/Organization/Usage/Activity.tsx
+++ b/apps/studio/components/interfaces/Organization/Usage/Activity.tsx
@@ -19,8 +19,6 @@ const Activity = ({
orgSlug,
projectRef,
subscription,
- startDate,
- endDate,
currentBillingCycleSelected,
orgDailyStats,
isLoadingOrgDailyStats,
diff --git a/apps/studio/components/interfaces/Organization/Usage/Egress.tsx b/apps/studio/components/interfaces/Organization/Usage/Egress.tsx
index d9988db578c..8a20726ccc1 100644
--- a/apps/studio/components/interfaces/Organization/Usage/Egress.tsx
+++ b/apps/studio/components/interfaces/Organization/Usage/Egress.tsx
@@ -11,6 +11,8 @@ export interface EgressProps {
currentBillingCycleSelected: boolean
orgDailyStats: OrgDailyUsageResponse | undefined
isLoadingOrgDailyStats: boolean
+ startDate: string | undefined
+ endDate: string | undefined
}
const Egress = ({
@@ -20,6 +22,8 @@ const Egress = ({
currentBillingCycleSelected,
orgDailyStats,
isLoadingOrgDailyStats,
+ startDate,
+ endDate,
}: EgressProps) => {
const chartMeta: {
[key: string]: { data: DataPoint[]; margin: number; isLoading: boolean }
@@ -47,6 +51,8 @@ const Egress = ({
chartMeta={chartMeta}
subscription={subscription}
currentBillingCycleSelected={currentBillingCycleSelected}
+ startDate={startDate}
+ endDate={endDate}
/>
)
}
diff --git a/apps/studio/components/interfaces/Organization/Usage/SizeAndCounts.tsx b/apps/studio/components/interfaces/Organization/Usage/SizeAndCounts.tsx
index ebaa53b1837..0887860f75a 100644
--- a/apps/studio/components/interfaces/Organization/Usage/SizeAndCounts.tsx
+++ b/apps/studio/components/interfaces/Organization/Usage/SizeAndCounts.tsx
@@ -15,6 +15,8 @@ export interface SizeAndCountsProps {
currentBillingCycleSelected: boolean
orgDailyStats: OrgDailyUsageResponse | undefined
isLoadingOrgDailyStats: boolean
+ startDate: string | undefined
+ endDate: string | undefined
}
const SizeAndCounts = ({
@@ -24,6 +26,8 @@ const SizeAndCounts = ({
currentBillingCycleSelected,
orgDailyStats,
isLoadingOrgDailyStats,
+ startDate,
+ endDate,
}: SizeAndCountsProps) => {
const chartMeta: {
[key: string]: { data: DataPoint[]; margin: number; isLoading: boolean }
@@ -46,6 +50,8 @@ const SizeAndCounts = ({
chartMeta={chartMeta}
subscription={subscription}
currentBillingCycleSelected={currentBillingCycleSelected}
+ startDate={startDate}
+ endDate={endDate}
/>
)
}
diff --git a/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx
index 7b480f0f2a7..70cdf7bae94 100644
--- a/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx
+++ b/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx
@@ -63,8 +63,6 @@ export const TotalUsage = ({
// When the user filters by project ref or selects a custom timeframe, we only display usage+project breakdown, but no costs/limits
const showRelationToSubscription = currentBillingCycleSelected && !projectRef
- const isOnHigherPlan = ['team', 'enterprise', 'platform'].includes(subscription?.plan.id ?? '')
-
const hasExceededAnyLimits =
showRelationToSubscription &&
Boolean(
@@ -150,7 +148,7 @@ export const TotalUsage = ({
{isSuccessUsage && subscription && (
- {showRelationToSubscription && !isOnHigherPlan && (
+ {showRelationToSubscription && (
{!hasExceededAnyLimits ? (
diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx
index 9e5bf136e9a..ec443ebbdd7 100644
--- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx
+++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx
@@ -32,7 +32,7 @@ import SizeAndCounts from './SizeAndCounts'
import { TotalUsage } from './TotalUsage'
export const Usage = () => {
- const { slug, projectRef } = useParams()
+ const { slug } = useParams()
const [dateRange, setDateRange] = useState()
@@ -106,7 +106,7 @@ export const Usage = () => {
isError: isErrorOrgDailyStats,
} = useOrgDailyStatsQuery({
orgSlug: slug,
- projectRef,
+ projectRef: selectedProjectRef ?? undefined,
startDate,
endDate,
})
@@ -299,6 +299,8 @@ export const Usage = () => {
currentBillingCycleSelected={currentBillingCycleSelected}
orgDailyStats={orgDailyStats}
isLoadingOrgDailyStats={isLoadingOrgDailyStats}
+ startDate={startDate}
+ endDate={endDate}
/>
{
currentBillingCycleSelected={currentBillingCycleSelected}
orgDailyStats={orgDailyStats}
isLoadingOrgDailyStats={isLoadingOrgDailyStats}
+ startDate={startDate}
+ endDate={endDate}
/>
`
+ const message = `Organization Slug: ${slug}\nRequested plan: `
return `/support/new?orgSlug=${slug}&projectRef=no-project&category=Plan_upgrade&subject=${subject}&message=${encodeURIComponent(message)}`
}
diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx
index 04d98109a65..a77b9bc2697 100644
--- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx
+++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx
@@ -20,6 +20,8 @@ export interface UsageSectionProps {
subscription?: OrgSubscription
chartMeta: ChartMeta
currentBillingCycleSelected: boolean
+ startDate?: string
+ endDate?: string
}
const UsageSection = ({
@@ -29,6 +31,8 @@ const UsageSection = ({
chartMeta,
subscription,
currentBillingCycleSelected,
+ startDate,
+ endDate,
}: UsageSectionProps) => {
const {
data: usage,
@@ -36,7 +40,12 @@ const UsageSection = ({
isLoading: isLoadingUsage,
isError: isErrorUsage,
isSuccess: isSuccessUsage,
- } = useOrgUsageQuery({ orgSlug })
+ } = useOrgUsageQuery({
+ orgSlug,
+ projectRef,
+ start: !currentBillingCycleSelected && startDate ? new Date(startDate) : undefined,
+ end: !currentBillingCycleSelected && endDate ? new Date(endDate) : undefined,
+ })
const categoryMeta = USAGE_CATEGORIES(subscription).find(
(category) => category.key === categoryKey
diff --git a/apps/studio/components/interfaces/Reports/Reports.constants.ts b/apps/studio/components/interfaces/Reports/Reports.constants.ts
index e5f4794d1f6..7fa34e6f29d 100644
--- a/apps/studio/components/interfaces/Reports/Reports.constants.ts
+++ b/apps/studio/components/interfaces/Reports/Reports.constants.ts
@@ -26,32 +26,32 @@ export const REPORTS_DATEPICKER_HELPERS: ReportsDatetimeHelper[] = [
text: REPORT_DATERANGE_HELPER_LABELS.LAST_10_MINUTES,
calcFrom: () => dayjs().subtract(10, 'minute').toISOString(),
calcTo: () => dayjs().toISOString(),
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
text: REPORT_DATERANGE_HELPER_LABELS.LAST_30_MINUTES,
calcFrom: () => dayjs().subtract(30, 'minute').toISOString(),
calcTo: () => dayjs().toISOString(),
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
text: REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES,
calcFrom: () => dayjs().subtract(1, 'hour').toISOString(),
calcTo: () => dayjs().toISOString(),
default: true,
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
text: REPORT_DATERANGE_HELPER_LABELS.LAST_3_HOURS,
calcFrom: () => dayjs().subtract(3, 'hour').toISOString(),
calcTo: () => dayjs().toISOString(),
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
text: REPORT_DATERANGE_HELPER_LABELS.LAST_24_HOURS,
calcFrom: () => dayjs().subtract(1, 'day').toISOString(),
calcTo: () => dayjs().toISOString(),
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
},
{
text: REPORT_DATERANGE_HELPER_LABELS.LAST_7_DAYS,
diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.constants.ts b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.constants.ts
index e2e49a2b487..0c055fab81f 100644
--- a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.constants.ts
+++ b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.constants.ts
@@ -1,5 +1,5 @@
// https://supabase.com/docs/guides/platform/performance#optimizing-the-number-of-connections
-// https://github.com/supabase/infrastructure/blob/develop/worker/src/lib/constants.ts#L544-L596
+// https://github.com/supabase/platform/blob/develop/worker/src/lib/constants.ts#L544-L596
// https://github.com/supabase/supabase-admin-api/blob/master/optimizations/pgbouncer.go
// [Joshen] This matches for both Supavisor and PgBouncer
diff --git a/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.utils.ts b/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.utils.ts
index 7a58ea07e25..6a46647ac4a 100644
--- a/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.utils.ts
+++ b/apps/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.utils.ts
@@ -57,7 +57,7 @@ export const getAddressEndRange = (type: 'IPv4' | 'IPv6', address: string) => {
}
// [Joshen] Using same logic as worker
-// https://github.com/supabase/infrastructure/blob/840c9596e8bf9e9090ec94de1756bd511e67393a/worker/src/tasks/db/add_as_pooler_tenant.ts#L176C15-L176C15
+// https://github.com/supabase/platform/blob/840c9596e8bf9e9090ec94de1756bd511e67393a/worker/src/tasks/db/add_as_pooler_tenant.ts#L176C15-L176C15
export const normalize = (address: string) => {
const [isIpV4] = Validator.isValidIPv4String(address.split('/')[0])
if (isIpV4) {
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx
index 0488c89862c..4f66e7e0abc 100644
--- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx
+++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx
@@ -76,17 +76,17 @@ const DeployNewReplicaPanel = ({
const { data: addons, isSuccess } = useProjectAddonsQuery({ projectRef })
const { data: diskConfiguration } = useDiskAttributesQuery({ projectRef })
- const isNotOnHigherPlan = useMemo(
- () => !['team', 'enterprise', 'platform'].includes(org?.plan.id ?? ''),
+ const isNotOnTeamOrEnterprisePlan = useMemo(
+ () => !['team', 'enterprise'].includes(org?.plan.id ?? ''),
[org]
)
const { data: allOverdueInvoices } = useOverdueInvoicesQuery({
- enabled: isNotOnHigherPlan,
+ enabled: isNotOnTeamOrEnterprisePlan,
})
const overdueInvoices = (allOverdueInvoices ?? []).filter(
(x) => x.organization_id === project?.organization_id
)
- const hasOverdueInvoices = overdueInvoices.length > 0 && isNotOnHigherPlan
+ const hasOverdueInvoices = overdueInvoices.length > 0 && isNotOnTeamOrEnterprisePlan
const isAwsK8s = useIsAwsK8sCloudProvider()
// Opting for useState temporarily as Listbox doesn't seem to work with react-hook-form yet
diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts
index 77c56ce6d98..0f48acb42ff 100644
--- a/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts
+++ b/apps/studio/components/interfaces/Settings/Logs/Logs.constants.ts
@@ -777,7 +777,6 @@ export const TIER_QUERY_LIMITS: {
PAYG: { text: '7 days', value: 7, unit: 'day', promptUpgrade: true },
TEAM: { text: '28 days', value: 28, unit: 'day', promptUpgrade: true },
ENTERPRISE: { text: '90 days', value: 90, unit: 'day', promptUpgrade: false },
- PLATFORM: { text: '1 day', value: 1, unit: 'day', promptUpgrade: false },
}
export const LOG_ROUTES_WITH_REPLICA_SUPPORT = [
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketHeader.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketHeader.tsx
index d2802ff3212..033fd0c409d 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketHeader.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketHeader.tsx
@@ -8,7 +8,7 @@ import {
} from 'components/layouts/Scaffold'
import { HIDE_REPLICATION_USER_FLOW } from './AnalyticsBucketDetails.constants'
import { ConnectTablesDialog } from './ConnectTablesDialog'
-import { CreateTableInstructionsDialog } from './CreateTableInstructions/CreateTableInstructionsDialog'
+import { CreateTableInstructionsDialog } from './CreateTable/CreateTableInstructionsDialog'
interface BucketHeaderProps {
showActions?: boolean
@@ -35,14 +35,12 @@ export const BucketHeader = ({
{showActions && (
- {namespaces.length > 0 && (
- <>
- {HIDE_REPLICATION_USER_FLOW ? (
-
- ) : (
-
- )}
- >
+ {HIDE_REPLICATION_USER_FLOW ? (
+
+ ) : (
+ namespaces.length > 0 && (
+
+ )
)}
)}
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/CreateTableInstructions.constants.ts b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.constants.ts
similarity index 100%
rename from apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/CreateTableInstructions.constants.ts
rename to apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.constants.ts
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/index.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.tsx
similarity index 100%
rename from apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/index.tsx
rename to apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.tsx
index d09baff9804..67d0494b2cc 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/index.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.tsx
@@ -1,3 +1,4 @@
+import { Eye, EyeOff } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useParams } from 'common'
@@ -13,7 +14,6 @@ import {
} from 'data/vault/vault-secret-decrypted-value-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
-import { Eye, EyeOff } from 'lucide-react'
import {
Accordion_Shadcn_,
AccordionContent_Shadcn_,
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx
new file mode 100644
index 00000000000..9f5d0d1b61f
--- /dev/null
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx
@@ -0,0 +1,71 @@
+import { useFlag } from 'common'
+import { ChevronDown, Plus } from 'lucide-react'
+import { useState } from 'react'
+import {
+ Button,
+ cn,
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from 'ui'
+import { CreateTableInstructions } from './CreateTableInstructions'
+import { CreateTableSheet } from './CreateTableSheet'
+
+export const CreateTableInstructionsDialog = () => {
+ const enableCreationOfTablesFromDashboard = useFlag('analyticsBucketsTableCreation')
+
+ const [showModal, setShowModal] = useState(false)
+ const [showSheet, setShowSheet] = useState(false)
+
+ return (
+ <>
+
+ }
+ className={cn(enableCreationOfTablesFromDashboard && 'rounded-r-none hover:z-10')}
+ onClick={() => {
+ if (enableCreationOfTablesFromDashboard) setShowSheet(true)
+ else setShowModal(true)
+ }}
+ >
+ Create table
+
+ {enableCreationOfTablesFromDashboard && (
+
+
+ }
+ />
+
+
+ setShowModal(true)}>Via Pyiceberg
+
+
+ )}
+
+
+
+
+
+ Adding tables to your Analytics Bucket
+
+ Tables can be created or added to your bucket via Pyiceberg
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.constants.ts b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.constants.ts
new file mode 100644
index 00000000000..63389dbc20d
--- /dev/null
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.constants.ts
@@ -0,0 +1,26 @@
+export const NEW_NAMESPACE_MARKER = 'new-namespace'
+
+export const COLUMN_TYPES = [
+ 'boolean',
+ 'int',
+ 'long',
+ 'float',
+ 'double',
+ 'string',
+ 'timestamp',
+ 'date',
+ 'time',
+ 'timestamptz',
+ 'uuid',
+ 'binary',
+ 'decimal',
+ 'fixed',
+]
+
+export const COLUMN_TYPE_FIELDS = {
+ decimal: [
+ { name: 'precision', type: 'number' },
+ { name: 'scale', type: 'number' },
+ ],
+ fixed: [{ name: 'length', type: 'number' }],
+}
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.schema.ts b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.schema.ts
new file mode 100644
index 00000000000..d9aac58c843
--- /dev/null
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.schema.ts
@@ -0,0 +1,82 @@
+import { z } from 'zod'
+import { NEW_NAMESPACE_MARKER } from './CreateTableSheet.constants'
+
+const getValidRegex = (type: 'namespace' | 'table') =>
+ type === 'namespace'
+ ? /^(?!aws)[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/
+ : /^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/
+const getErrorRegex = (type: 'namespace' | 'table') =>
+ type === 'namespace'
+ ? /^(?:(?aws.*)|(?[^a-z0-9].*)|(?.*[^a-z0-9_].*)|(?.*[^a-z0-9]))$/
+ : /^(?:(?[^a-z0-9].*)|(?.*[^a-z0-9_].*)|(?.*[^a-z0-9]))$/
+
+const validateName = ({ name, type }: { name: string; type: 'namespace' | 'table' }) => {
+ const validRe = getValidRegex(type)
+ if (validRe.test(name)) return undefined
+
+ const errorRe = getErrorRegex(type)
+ const match = name.match(errorRe)?.groups || {}
+ if (match.starts_with_reserved) return "Namespace must not start with 'aws'"
+ if (match.invalid_start) return 'Name must begin with a lowercase letter or number'
+ if (match.invalid_end) return 'Name must end with a lowercase letter or number'
+ if (match.invalid_char) return 'Name may only contain lowercase letters, numbers, and underscores'
+
+ return 'Invalid name'
+}
+
+export const createFormSchema = () =>
+ z
+ .object({
+ namespace: z.string().min(1, 'Please select a namespace'),
+ newNamespace: z.string().max(255, 'Name must be within 255 characters').optional(),
+ name: z
+ .string()
+ .min(1, 'Provide a name for your table')
+ .max(255, 'Name must be within 255 characters'),
+ columns: z
+ .object({
+ name: z.string().min(1, 'Provide a name for your column'),
+ type: z.string().min(1, 'Select a type for your column'),
+ // For decimal type
+ precision: z.number().optional(),
+ scale: z.number().int().optional(),
+ // For fixed type
+ length: z.number().int().optional(),
+ })
+ .array()
+ .default([]),
+ })
+ .superRefine((data, ctx) => {
+ if (data.namespace === NEW_NAMESPACE_MARKER) {
+ if (data.newNamespace) {
+ const newNamespaceError = validateName({
+ name: data.newNamespace,
+ type: 'namespace',
+ })
+ if (newNamespaceError) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: newNamespaceError,
+ path: ['newNamespace'],
+ })
+ }
+ } else {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'Provide a name for your new namespace',
+ path: ['newNamespace'],
+ })
+ }
+ }
+
+ if (data.name) {
+ const newTableError = validateName({ name: data.name, type: 'table' })
+ if (newTableError) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: newTableError,
+ path: ['name'],
+ })
+ }
+ }
+ })
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.tsx
new file mode 100644
index 00000000000..fea88e742e1
--- /dev/null
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.tsx
@@ -0,0 +1,372 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import { Plus, X } from 'lucide-react'
+import { Fragment, useState } from 'react'
+import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import { z } from 'zod'
+
+import { useParams } from 'common'
+import { useIcebergNamespaceCreateMutation } from 'data/storage/iceberg-namespace-create-mutation'
+import {
+ NamespaceTableFields,
+ useIcebergNamespaceTableCreateMutation,
+} from 'data/storage/iceberg-namespace-table-create-mutation'
+import { useIcebergNamespaceTablesQuery } from 'data/storage/iceberg-namespace-tables-query'
+import { useIcebergNamespacesQuery } from 'data/storage/iceberg-namespaces-query'
+import {
+ Button,
+ DialogSectionSeparator,
+ Form_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Input_Shadcn_,
+ Select_Shadcn_,
+ SelectContent_Shadcn_,
+ SelectItem_Shadcn_,
+ SelectSeparator_Shadcn_,
+ SelectTrigger_Shadcn_,
+ SelectValue_Shadcn_,
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetSection,
+ SheetTitle,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import { COLUMN_TYPE_FIELDS, COLUMN_TYPES } from './CreateTableSheet.constants'
+import { createFormSchema } from './CreateTableSheet.schema'
+
+const formId = 'create-namespace-table'
+const NEW_NAMESPACE_MARKER = 'new-namespace'
+
+interface CreateTableSheetProps {
+ open: boolean
+ onOpenChange: (value: boolean) => void
+}
+
+export const CreateTableSheet = ({ open, onOpenChange }: CreateTableSheetProps) => {
+ const { ref: projectRef, bucketId } = useParams()
+ const [isCreating, setIsCreating] = useState(false)
+
+ const FormSchema = createFormSchema()
+ const defaultValues = {
+ namespace: '',
+ newNamespace: undefined,
+ name: '',
+ columns: [{ name: '', type: 'string' as any }],
+ }
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues,
+ mode: 'onChange',
+ })
+ const { namespace } = form.watch()
+ const {
+ fields: columns,
+ append: appendColumn,
+ remove: removeColumn,
+ } = useFieldArray({ control: form.control, name: 'columns' })
+
+ const { data: namespaces = [] } = useIcebergNamespacesQuery({ projectRef, warehouse: bucketId })
+ const { data: tables = [] } = useIcebergNamespaceTablesQuery(
+ {
+ projectRef,
+ warehouse: bucketId,
+ namespace,
+ },
+ { enabled: namespace !== NEW_NAMESPACE_MARKER }
+ )
+
+ const { mutateAsync: createNamespace } = useIcebergNamespaceCreateMutation()
+ const { mutateAsync: createTable } = useIcebergNamespaceTableCreateMutation()
+
+ const onSubmit: SubmitHandler> = async (values) => {
+ if (!bucketId) return console.error('Bucket ID is missing')
+ if (namespaces.includes(values.newNamespace ?? '')) {
+ return form.setError('newNamespace', { message: 'Namespace name already exists' })
+ }
+ if (tables.includes(values.name ?? '')) {
+ return form.setError('name', { message: 'Table name already exists' })
+ }
+
+ const isCreatingNewNamespace =
+ values.namespace === NEW_NAMESPACE_MARKER && !!values.newNamespace
+
+ try {
+ setIsCreating(true)
+ if (isCreatingNewNamespace) {
+ await createNamespace({
+ projectRef,
+ warehouse: bucketId,
+ namespace: values.newNamespace as string,
+ })
+ }
+
+ const fields = values.columns.map((column, idx) => {
+ return {
+ id: idx + 1,
+ name: column.name,
+ type:
+ column.type === 'decimal'
+ ? `decimal(${column.precision}, ${column.scale})`
+ : column.type === 'fixed'
+ ? `fixed[${column.length}]`
+ : column.type,
+ required: false,
+ }
+ }) as NamespaceTableFields
+
+ await createTable({
+ projectRef,
+ warehouse: bucketId,
+ namespace: isCreatingNewNamespace ? (values.newNamespace as string) : values.namespace,
+ name: values.name,
+ fields,
+ })
+
+ toast.success(`Successfully created table in ${values.newNamespace ?? values.namespace}!`)
+ onOpenChange(false)
+ form.reset(defaultValues)
+ } catch (error) {
+ } finally {
+ setIsCreating(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+ Create a new table
+
+
+
+
+
(
+
+
+ {
+ field.onChange(value)
+ form.resetField('newNamespace')
+ }}
+ >
+
+
+
+
+ {namespaces.map((x) => (
+
+ {x}
+
+ ))}
+ {namespaces.length > 0 && }
+
+
+
+
Create a new namespace
+
+
+
+
+
+
+ )}
+ />
+ {namespace === NEW_NAMESPACE_MARKER && (
+ (
+
+
+
+
+
+ )}
+ />
+ )}
+
+
+
+
+ {!!namespace && (
+
+
(
+
+
+
+
+
+ )}
+ />
+
+
+
+
Columns
+
}
+ onClick={() => appendColumn({ name: '', type: 'string' })}
+ >
+ Add column
+
+
+ {columns.length === 0 ? (
+
+ Add a column to your table
+
+ ) : (
+ <>
+
+ {columns.map((_, idx) => {
+ const columnType = form.watch(`columns.${idx}.type`)
+ const additionalFields =
+ COLUMN_TYPE_FIELDS[columnType as keyof typeof COLUMN_TYPE_FIELDS] ?? []
+
+ return (
+
+
+
(
+
+
+
+ )}
+ />
+ (
+
+
+
+
+
+
+ {COLUMN_TYPES.map((x) => (
+
+ {x}
+
+ ))}
+
+
+
+ )}
+ />
+
+ }
+ className="w-6 h-6"
+ onClick={() => removeColumn(idx)}
+ />
+
+
+ {additionalFields.length > 0 && (
+
+
+ {additionalFields.map((x, index) => (
+
+
+ {x.name}
+
+
(
+
+
+
+ )}
+ />
+
+ ))}
+
+
+
+ )}
+
+
+ )
+ })}
+ >
+ )}
+
+
+ )}
+
+
+
+ {
+ onOpenChange(false)
+ form.reset(defaultValues)
+ }}
+ >
+ Cancel
+
+
+ Create table
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/CreateTableInstructionsDialog.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/CreateTableInstructionsDialog.tsx
deleted file mode 100644
index 19c08f08015..00000000000
--- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTableInstructions/CreateTableInstructionsDialog.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Plus } from 'lucide-react'
-import {
- Button,
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from 'ui'
-import { CreateTableInstructions } from '.'
-
-export const CreateTableInstructionsDialog = () => {
- return (
-
-
- }>
- Create table
-
-
-
-
- Adding tables to your Analytics Bucket
-
- Tables can be created or added to your bucket via Pyiceberg
-
-
-
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/InsertDataDialog.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/InsertDataDialog.tsx
new file mode 100644
index 00000000000..451d30b25d9
--- /dev/null
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/InsertDataDialog.tsx
@@ -0,0 +1,87 @@
+import { useParams } from 'common'
+import { DocsButton } from 'components/ui/DocsButton'
+import { FDWTable } from 'data/fdw/fdws-query'
+import { SqlEditor } from 'icons'
+import { DOCS_URL } from 'lib/constants'
+import Link from 'next/link'
+import {
+ Button,
+ cn,
+ CodeBlock,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogSection,
+ DialogSectionSeparator,
+ DialogTitle,
+ DialogTrigger,
+} from 'ui'
+
+interface InsertDataDialogProps {
+ table: string
+ fdwTable: FDWTable
+}
+
+export const InsertDataDialog = ({ table, fdwTable }: InsertDataDialogProps) => {
+ const { ref } = useParams()
+
+ const sql = /* SQL */ `
+insert into ${fdwTable.schema}.${fdwTable.name} (
+ -- specify columns
+)
+values (
+ -- specify values for each column
+);
+`.trim()
+
+ return (
+
+
+ Insert data
+
+
+
+
+ Insert data into {table}
+
+
+
+
+
+
+
+ The Iceberg Foreign Data Wrapper (FDW) supports inserting data into Iceberg tables using
+ standard SQL INSERT statements.
+
+
+ Use the following SQL snippet to insert data into your iceberg table:
+
+
+
+
+
+
+ pre]:rounded-none [&>pre]:border-0')}
+ className="[&_code]:text-foreground"
+ language="sql"
+ value={sql}
+ />
+
+
+
+
+ }>
+
+ Open in SQL Editor
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx
index cb66723ac10..0bfc7b9fc9d 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx
@@ -12,12 +12,12 @@ import {
import { getDecryptedParameters } from 'components/interfaces/Storage/ImportForeignSchemaDialog.utils'
import { DotPing } from 'components/ui/DotPing'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
+import { useFDWDropForeignTableMutation } from 'data/fdw/fdw-drop-foreign-table-mutation'
+import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation'
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
import { useUpdatePublicationMutation } from 'data/replication/publication-update-mutation'
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
import { useReplicationTablesQuery } from 'data/replication/tables-query'
-import { useFDWDropForeignTableMutation } from 'data/fdw/fdw-drop-foreign-table-mutation'
-import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation'
import { useIcebergNamespaceTableDeleteMutation } from 'data/storage/iceberg-namespace-table-delete-mutation'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { SqlEditor, TableEditor } from 'icons'
@@ -43,6 +43,7 @@ import {
} from '../AnalyticsBucketDetails.utils'
import { useAnalyticsBucketAssociatedEntities } from '../useAnalyticsBucketAssociatedEntities'
import { useAnalyticsBucketWrapperInstance } from '../useAnalyticsBucketWrapperInstance'
+import { InsertDataDialog } from './InsertDataDialog'
import { inferPostgresTableFromNamespaceTable } from './NamespaceWithTables.utils'
interface TableRowComponentProps {
@@ -78,7 +79,7 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen
const { mutateAsync: updateFDW } = useFDWUpdateMutation()
const { mutateAsync: dropForeignTable } = useFDWDropForeignTableMutation()
- const { mutateAsync: deleteNamespaceTable, isLoading: isDeletingNamespaceTable } =
+ const { mutateAsync: deleteNamespaceTable, isPending: isDeletingNamespaceTable } =
useIcebergNamespaceTableDeleteMutation({ onError: () => {} })
const { mutateAsync: updatePublication } = useUpdatePublicationMutation()
const { mutateAsync: startPipeline } = useStartPipelineMutation()
@@ -227,7 +228,6 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen
const wrapperValues = convertKVStringArrayToJson(wrapperInstance?.server_options ?? [])
await deleteNamespaceTable({
projectRef,
- catalogUri: wrapperValues.catalog_uri,
warehouse: wrapperValues.warehouse,
namespace: namespace,
table: table.name,
@@ -257,7 +257,6 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen
const wrapperValues = convertKVStringArrayToJson(wrapperInstance?.server_options ?? [])
await deleteNamespaceTable({
projectRef,
- catalogUri: wrapperValues.catalog_uri,
warehouse: wrapperValues.warehouse,
namespace: namespace,
table: table.name,
@@ -419,7 +418,7 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen
-
+
-
+
+
} />
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/index.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/index.tsx
index 12c9c4ee83d..2294c4102fe 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/index.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/index.tsx
@@ -5,7 +5,6 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import { FormattedWrapperTable } from 'components/interfaces/Integrations/Wrappers/Wrappers.utils'
import { ImportForeignSchemaDialog } from 'components/interfaces/Storage/ImportForeignSchemaDialog'
-import { getCatalogURI } from 'components/interfaces/Storage/StorageSettings/StorageSettings.utils'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useFDWDropForeignTableMutation } from 'data/fdw/fdw-drop-foreign-table-mutation'
import { useFDWImportForeignSchemaMutation } from 'data/fdw/fdw-import-foreign-schema-mutation'
@@ -79,7 +78,6 @@ export const NamespaceWithTables = ({
isSuccess: isSuccessNamespaceTables,
} = useIcebergNamespaceTablesQuery(
{
- catalogUri: wrapperValues.catalog_uri,
warehouse: wrapperValues.warehouse,
namespace: namespace,
projectRef,
@@ -179,11 +177,6 @@ export const NamespaceWithTables = ({
const onConfirmDeleteNamespace = async () => {
if (!bucketId) return console.error('Bucket ID is required')
- // Construct catalog URI for namespace creation
- const protocol = projectSettings?.app_config?.protocol ?? 'https'
- const endpoint =
- projectSettings?.app_config?.storage_endpoint || projectSettings?.app_config?.endpoint
- const catalogUri = getCatalogURI(project?.ref ?? '', protocol, endpoint)
try {
setIsDeletingNamespace(true)
@@ -193,7 +186,6 @@ export const NamespaceWithTables = ({
allTables.map((table) =>
deleteNamespaceTable({
projectRef,
- catalogUri,
warehouse: bucketId,
namespace,
table: table.name,
@@ -213,7 +205,7 @@ export const NamespaceWithTables = ({
)
)
- await deleteNamespace({ projectRef, catalogUri, warehouse: bucketId, namespace })
+ await deleteNamespace({ projectRef, warehouse: bucketId, namespace })
toast.success(`Successfully deleted namespace "${namespace}"`)
setShowConfirmDeleteNamespace(false)
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx
index ae2196de2bc..0923b10b8f5 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx
@@ -38,7 +38,7 @@ import { useSelectedAnalyticsBucket } from '../useSelectedAnalyticsBucket'
import { HIDE_REPLICATION_USER_FLOW } from './AnalyticsBucketDetails.constants'
import { BucketHeader } from './BucketHeader'
import { ConnectTablesDialog } from './ConnectTablesDialog'
-import { CreateTableInstructions } from './CreateTableInstructions'
+import { CreateTableInstructions } from './CreateTable/CreateTableInstructions'
import { NamespaceWithTables } from './NamespaceWithTables'
import { SimpleConfigurationDetails } from './SimpleConfigurationDetails'
import { useAnalyticsBucketAssociatedEntities } from './useAnalyticsBucketAssociatedEntities'
@@ -120,7 +120,6 @@ export const AnalyticBucketDetails = () => {
} = useIcebergNamespacesQuery(
{
projectRef,
- catalogUri: wrapperValues.catalog_uri,
warehouse: wrapperValues.warehouse,
},
{
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.tsx
index 3d7e2772b76..6ca9035d2d9 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketModal.tsx
@@ -1,28 +1,21 @@
import { zodResolver } from '@hookform/resolvers/zod'
-import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { Plus } from 'lucide-react'
import { useRouter } from 'next/router'
-import { parseAsBoolean, useQueryState } from 'nuqs'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import z from 'zod'
import { useParams } from 'common'
-import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { InlineLink } from 'components/ui/InlineLink'
-import { useIsAnalyticsBucketsEnabled } from 'data/config/project-storage-config-query'
import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation'
import { useAnalyticsBucketCreateMutation } from 'data/storage/analytics-bucket-create-mutation'
import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query'
import { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-create-mutation'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
-import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import {
Button,
- cn,
Dialog,
DialogContent,
DialogFooter,
@@ -30,7 +23,6 @@ import {
DialogSection,
DialogSectionSeparator,
DialogTitle,
- DialogTrigger,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
@@ -122,40 +114,22 @@ const formId = 'create-analytics-storage-bucket-form'
export type CreateAnalyticsBucketForm = z.infer
interface CreateAnalyticsBucketModalProps {
- buttonSize?: 'tiny' | 'small'
- buttonType?: 'default' | 'primary'
- buttonClassName?: string
- disabled?: boolean
- tooltip?: {
- content: {
- side?: 'top' | 'bottom' | 'left' | 'right'
- text?: string
- }
- }
+ open: boolean
+ onOpenChange: (value: boolean) => void
}
export const CreateAnalyticsBucketModal = ({
- buttonSize = 'tiny',
- buttonType = 'default',
- buttonClassName,
- disabled = false,
- tooltip,
+ open,
+ onOpenChange,
}: CreateAnalyticsBucketModalProps) => {
const router = useRouter()
const { ref } = useParams()
const { data: org } = useSelectedOrganizationQuery()
const { data: project } = useSelectedProjectQuery()
- const { can: canCreateBuckets } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
const { extension: wrappersExtension, state: wrappersExtensionState } =
useIcebergWrapperExtension()
- const [visible, setVisible] = useQueryState(
- 'new',
- parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
- )
-
- const { data: buckets = [], isLoading } = useAnalyticsBucketsQuery({ projectRef: ref })
- const icebergCatalogEnabled = useIsAnalyticsBucketsEnabled({ projectRef: ref })
+ const { data: buckets = [] } = useAnalyticsBucketsQuery({ projectRef: ref })
const wrappersExtenstionNeedsUpgrading = wrappersExtensionState === 'needs-upgrade'
const { mutate: sendEvent } = useSendEventMutation()
@@ -174,8 +148,6 @@ export const CreateAnalyticsBucketModal = ({
const config = BUCKET_TYPES['analytics']
const isCreating = isEnablingExtension || isCreatingIcebergWrapper || isCreatingAnalyticsBucket
- const isDisabled =
- !canCreateBuckets || !icebergCatalogEnabled || isLoading || buckets.length >= 2 || disabled
const form = useForm({
resolver: zodResolver(FormSchema),
@@ -217,8 +189,7 @@ export const CreateAnalyticsBucketModal = ({
form.reset()
toast.success(`Created bucket “${values.name}”`)
- setVisible(false)
- router.push(`/project/${ref}/storage/analytics/buckets/${values.name}`)
+ onOpenChange(false)
} catch (error: any) {
toast.error(`Failed to create bucket: ${error.message}`)
}
@@ -226,44 +197,16 @@ export const CreateAnalyticsBucketModal = ({
const handleClose = () => {
form.reset()
- setVisible(false)
+ onOpenChange(false)
}
return (
{
if (!open) handleClose()
}}
>
-
- }
- disabled={isDisabled}
- style={{ justifyContent: 'start' }}
- onClick={() => setVisible(true)}
- tooltip={{
- content: {
- side: tooltip?.content?.side || 'bottom',
- className: cn(!icebergCatalogEnabled ? 'w-72 text-center' : ''),
- text: !icebergCatalogEnabled
- ? 'Analytics buckets are not enabled for your project. Please contact support to enable it.'
- : !canCreateBuckets
- ? 'You need additional permissions to create buckets'
- : buckets.length >= 2
- ? 'Bucket limit reached'
- : tooltip?.content?.text,
- },
- }}
- >
- New bucket
-
-
-
Create {config.singularName} bucket
@@ -336,7 +279,7 @@ export const CreateAnalyticsBucketModal = ({
- setVisible(false)}>
+ onOpenChange(false)}>
Cancel
{
@@ -34,6 +36,11 @@ export const AnalyticsBuckets = () => {
const [filterString, setFilterString] = useState('')
+ const [visible, setVisible] = useQueryState(
+ 'new',
+ parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
+ )
+
const {
data: buckets = [],
error: bucketsError,
@@ -64,133 +71,142 @@ export const AnalyticsBuckets = () => {
}
return (
-
-
-
-
+ <>
+
+
+
+
- {isLoadingBuckets && }
+ {isLoadingBuckets && }
- {isErrorBuckets && (
-
- )}
+ {isErrorBuckets && (
+
+ )}
- {isSuccessBuckets && (
- <>
- {hasNoBuckets ? (
-
- ) : (
-
-
-
Buckets
- {analyticsBuckets.length > 0 && (
-
-
-
- {analyticsBuckets.length}
- /2
-
-
-
- Each project can only have up to 2 buckets while analytics buckets are in
- alpha{' '}
-
-
+ {isSuccessBuckets && (
+ <>
+ {hasNoBuckets ? (
+
setVisible(true)}
+ />
+ ) : (
+
+
+
Buckets
+ {analyticsBuckets.length > 0 && (
+
+
+
+ {analyticsBuckets.length}
+ /2
+
+
+
+ Each project can only have up to 2 buckets while analytics buckets are
+ in alpha{' '}
+
+
+ )}
+
+
+ setFilterString(e.target.value)}
+ icon={ }
+ />
+ setVisible(true)} />
+
+
+ {isLoadingBuckets ? (
+
+ ) : (
+
+
+
+
+ {analyticsBuckets.length > 0 && (
+
+ Icon
+
+ )}
+ Name
+ Created at
+
+ Actions
+
+
+
+
+ {analyticsBuckets.length === 0 && filterString.length > 0 && (
+
+
+ No results found
+
+ Your search for "{filterString}" did not return any results
+
+
+
+ )}
+ {analyticsBuckets.map((bucket) => (
+ handleBucketNavigation(bucket.name, event)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ handleBucketNavigation(bucket.name, event)
+ }
+ }}
+ tabIndex={0}
+ >
+
+
+
+
+
+ {bucket.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
)}
-
- setFilterString(e.target.value)}
- icon={ }
- />
-
-
-
- {isLoadingBuckets ? (
-
- ) : (
-
-
-
-
- {analyticsBuckets.length > 0 && (
-
- Icon
-
- )}
- Name
- Created at
-
- Actions
-
-
-
-
- {analyticsBuckets.length === 0 && filterString.length > 0 && (
-
-
- No results found
-
- Your search for "{filterString}" did not return any results
-
-
-
- )}
- {analyticsBuckets.map((bucket) => (
- handleBucketNavigation(bucket.name, event)}
- onKeyDown={(event) => {
- if (event.key === 'Enter' || event.key === ' ') {
- event.preventDefault()
- handleBucketNavigation(bucket.name, event)
- }
- }}
- tabIndex={0}
- >
-
-
-
-
-
- {bucket.name}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
-
- )}
-
- )}
- >
- )}
-
-
-
+ )}
+ >
+ )}
+
+
+
+
+ >
)
}
diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx
index 0650510f289..aea27e3fc7a 100644
--- a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx
+++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx
@@ -1,20 +1,15 @@
import { zodResolver } from '@hookform/resolvers/zod'
-import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { Plus } from 'lucide-react'
import { useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import z from 'zod'
-import { parseAsBoolean, useQueryState } from 'nuqs'
import { useParams } from 'common'
import { StorageSizeUnits } from 'components/interfaces/Storage/StorageSettings/StorageSettings.constants'
-import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { InlineLink } from 'components/ui/InlineLink'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import { useBucketCreateMutation } from 'data/storage/bucket-create-mutation'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
-import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import {
@@ -26,7 +21,6 @@ import {
DialogSection,
DialogSectionSeparator,
DialogTitle,
- DialogTrigger,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
@@ -85,30 +79,17 @@ const formId = 'create-storage-bucket-form'
export type CreateBucketForm = z.infer
interface CreateBucketModalProps {
- buttonSize?: 'tiny' | 'small'
- buttonType?: 'default' | 'primary'
- buttonClassName?: string
- label?: string
+ open: boolean
+ onOpenChange: (value: boolean) => void
}
-export const CreateBucketModal = ({
- buttonSize = 'tiny',
- buttonType = 'default',
- buttonClassName,
- label = 'New bucket',
-}: CreateBucketModalProps) => {
+export const CreateBucketModal = ({ open, onOpenChange }: CreateBucketModalProps) => {
const { ref } = useParams()
const { data: org } = useSelectedOrganizationQuery()
- const [visible, setVisible] = useQueryState(
- 'new',
- parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
- )
const [selectedUnit, setSelectedUnit] = useState(StorageSizeUnits.MB)
const [hasAllowedMimeTypes, setHasAllowedMimeTypes] = useState(false)
- const { can: canCreateBuckets } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
-
const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0)
const formattedGlobalUploadLimit = `${value} ${unit}`
@@ -172,7 +153,7 @@ export const CreateBucketModal = ({
toast.success(`Successfully created bucket ${values.name}`)
form.reset()
setSelectedUnit(StorageSizeUnits.MB)
- setVisible(false)
+ onOpenChange(false)
} catch (error: any) {
// Handle specific error cases for inline display
const errorMessage = error.message?.toLowerCase() || ''
@@ -196,42 +177,18 @@ export const CreateBucketModal = ({
const handleClose = () => {
form.reset()
setSelectedUnit(StorageSizeUnits.MB)
- setVisible(false)
+ onOpenChange(false)
}
return (
{
if (!open) {
handleClose()
}
}}
>
-
- }
- disabled={!canCreateBuckets}
- style={{ justifyContent: 'start' }}
- onClick={() => setVisible(true)}
- tabIndex={!canCreateBuckets ? -1 : 0}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canCreateBuckets
- ? 'You need additional permissions to create buckets'
- : undefined,
- },
- }}
- >
- {label}
-
-
-
Create file bucket
@@ -378,7 +335,7 @@ export const CreateBucketModal = ({
setVisible(false)}
+ onClick={() => onOpenChange(false)}
>
Storage Settings
{' '}
@@ -392,7 +349,7 @@ export const CreateBucketModal = ({
setVisible(false)}
+ onClick={() => onOpenChange(false)}
>
global file size limit
{' '}
@@ -449,7 +406,7 @@ export const CreateBucketModal = ({
- setVisible(false)}>
+ onOpenChange(false)}>
Cancel
- {bucketType === 'files' && (
-
- )}
- {bucketType === 'analytics' && (
-
- )}
- {bucketType === 'vectors' && }
+
)
}
diff --git a/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx b/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx
index faa4c0c4ef3..e6b5ed6b8ff 100644
--- a/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx
+++ b/apps/studio/components/interfaces/Storage/FilesBuckets/index.tsx
@@ -9,6 +9,7 @@ import { useProjectStorageConfigQuery } from 'data/config/project-storage-config
import { useBucketsQuery } from 'data/storage/buckets-query'
import { IS_PLATFORM } from 'lib/constants'
import { formatBytes } from 'lib/helpers'
+import { parseAsBoolean, useQueryState } from 'nuqs'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
import {
Button,
@@ -25,6 +26,7 @@ import { PageContainer } from 'ui-patterns/PageContainer'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import { CreateBucketModal } from '../CreateBucketModal'
import { EmptyBucketState } from '../EmptyBucketState'
+import { CreateBucketButton } from '../NewBucketButton'
import { STORAGE_BUCKET_SORT } from '../Storage.constants'
import { BucketsTable } from './BucketsTable'
@@ -33,6 +35,11 @@ export const FilesBuckets = () => {
const snap = useStorageExplorerStateSnapshot()
const [filterString, setFilterString] = useState('')
+ const [visible, setVisible] = useQueryState(
+ 'new',
+ parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
+ )
+
const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
const {
data: buckets = [],
@@ -67,81 +74,84 @@ export const FilesBuckets = () => {
)
return (
-
-
-
- {isLoadingBuckets && }
- {isErrorBuckets && (
- <>
- {hasNoApiKeys ? (
-
-
- The Dashboard relies on having active API keys on the project to function. If
- you'd like to use Storage through the Dashboard, create a set of API keys{' '}
- here .
-
-
- ) : (
-
- )}
- >
- )}
- {isSuccessBuckets && (
- <>
- {hasNoBuckets ? (
-
- ) : (
- <>
-
-
-
setFilterString(e.target.value)}
- icon={
}
- />
-
-
- }>
- Sorted by {snap.sortBucket === 'alphabetical' ? 'name' : 'created at'}
-
-
-
-
- snap.setSortBucket(value as STORAGE_BUCKET_SORT)
- }
- >
-
- Sort by name
-
-
- Sort by created at
-
-
-
-
+ <>
+
+
+
+ {isLoadingBuckets && }
+ {isErrorBuckets && (
+ <>
+ {hasNoApiKeys ? (
+
+
+ The Dashboard relies on having active API keys on the project to function. If
+ you'd like to use Storage through the Dashboard, create a set of API keys{' '}
+ here .
+
+
+ ) : (
+
+ )}
+ >
+ )}
+ {isSuccessBuckets && (
+ <>
+ {hasNoBuckets ? (
+ setVisible(true)} />
+ ) : (
+ <>
+
+
+ setFilterString(e.target.value)}
+ icon={ }
+ />
+
+
+ }>
+ Sorted by {snap.sortBucket === 'alphabetical' ? 'name' : 'created at'}
+
+
+
+
+ snap.setSortBucket(value as STORAGE_BUCKET_SORT)
+ }
+ >
+
+ Sort by name
+
+
+ Sort by created at
+
+
+
+
+
+
setVisible(true)} />
-
-
-
-
-
- >
- )}
- >
- )}
-
-
-
+
+
+
+ >
+ )}
+ >
+ )}
+
+
+
+
+ >
)
}
diff --git a/apps/studio/components/interfaces/Storage/NewBucketButton.tsx b/apps/studio/components/interfaces/Storage/NewBucketButton.tsx
new file mode 100644
index 00000000000..f356f155c7b
--- /dev/null
+++ b/apps/studio/components/interfaces/Storage/NewBucketButton.tsx
@@ -0,0 +1,34 @@
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { Plus } from 'lucide-react'
+import { MouseEventHandler } from 'react'
+
+import { ButtonTooltip } from 'components/ui/ButtonTooltip'
+import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
+
+export const CreateBucketButton = ({
+ onClick,
+}: {
+ onClick?: MouseEventHandler
+}) => {
+ const { can: canCreateBuckets } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
+
+ return (
+ }
+ disabled={!canCreateBuckets}
+ onClick={onClick}
+ tooltip={{
+ content: {
+ side: 'bottom',
+ text: !canCreateBuckets ? 'You need additional permissions to create buckets' : undefined,
+ },
+ }}
+ >
+ New bucket
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx
index 4b013668ce2..c3f2492d349 100644
--- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx
+++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditPolicyModal.tsx
@@ -102,9 +102,25 @@ const StoragePoliciesEditPolicyModal = ({
allowedOperations: [operation],
})
}
- const updatedAllowedOperations = policyFormFields.allowedOperations.includes(operation)
- ? pull(policyFormFields.allowedOperations.slice(), operation)
- : policyFormFields.allowedOperations.concat([operation])
+
+ const currentOps = policyFormFields.allowedOperations
+ const isRemoving = currentOps.includes(operation)
+ let updatedAllowedOperations = isRemoving
+ ? pull(currentOps.slice(), operation)
+ : currentOps.concat([operation])
+
+ if (!isRemoving && (operation === 'UPDATE' || operation === 'DELETE')) {
+ if (!updatedAllowedOperations.includes('SELECT')) {
+ updatedAllowedOperations = updatedAllowedOperations.concat(['SELECT'])
+ }
+ }
+
+ if (isRemoving && operation === 'SELECT') {
+ updatedAllowedOperations = updatedAllowedOperations.filter(
+ (op: string) => op !== 'UPDATE' && op !== 'DELETE'
+ )
+ }
+
return setPolicyFormFields({
...policyFormFields,
allowedOperations: updatedAllowedOperations,
diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx
index ce58d3b5f36..b9e3a4b623e 100644
--- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx
+++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx
@@ -28,6 +28,9 @@ const PolicyDefinition = ({ definition = '', onUpdatePolicyDefinition = () => {}
const PolicyAllowedOperations = ({ allowedOperations = [], onToggleOperation = () => {} }: any) => {
const allowedClientLibraryMethods = deriveAllowedClientLibraryMethods(allowedOperations)
+ const hasUpdateOrDelete =
+ allowedOperations.includes('UPDATE') || allowedOperations.includes('DELETE')
+
return (
@@ -70,6 +73,12 @@ const PolicyAllowedOperations = ({ allowedOperations = [], onToggleOperation = (
checked={allowedOperations.includes('DELETE')}
/>
+ {hasUpdateOrDelete && (
+
+ SELECT has been auto selected as UPDATE and{' '}
+ DELETE require it
+
+ )}
{Object.keys(STORAGE_CLIENT_LIBRARY_MAPPINGS).map((method) => (
diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx
index 7582e3a531b..c7852cfce25 100644
--- a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx
@@ -89,7 +89,9 @@ export const S3Connection = () => {
const { mutate: updateStorageConfig, isPending: isUpdating } =
useProjectStorageConfigUpdateUpdateMutation({
onSuccess: (_, vars) => {
- if (vars.features) form.reset({ s3ConnectionEnabled: vars.features.s3Protocol.enabled })
+ if (vars.features?.s3Protocol) {
+ form.reset({ s3ConnectionEnabled: vars.features.s3Protocol.enabled })
+ }
toast.success('Successfully updated storage settings')
},
})
diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.constants.ts b/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.constants.ts
index 2ccc016e6f9..411a4fb9507 100644
--- a/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.constants.ts
+++ b/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.constants.ts
@@ -1,7 +1,7 @@
export const STORAGE_FILE_SIZE_LIMIT_MAX_BYTES_FREE_PLAN = 50 * 1024 * 1024 // 50 MB
/** Max bytes for capped storage (i.e. Pro with spend cap) */
export const STORAGE_FILE_SIZE_LIMIT_MAX_BYTES_CAPPED = 50 * 1024 * 1024 * 1024 // 50 GB
-/** Max bytes for uncapped storage (i.e. Pro without spend cap, Team, Enterprise, Platform) */
+/** Max bytes for uncapped storage (i.e. Pro without spend cap, Team, Enterprise) */
export const STORAGE_FILE_SIZE_LIMIT_MAX_BYTES_UNCAPPED = 500 * 1024 * 1024 * 1024 // 500 GB
export enum StorageSizeUnits {
diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx
index 2ab963bcf53..b5d9bf56a09 100644
--- a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx
+++ b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx
@@ -1,13 +1,10 @@
import { zodResolver } from '@hookform/resolvers/zod'
-import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { Plus } from 'lucide-react'
-import { MouseEventHandler, useEffect, useState } from 'react'
+import { useEffect, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import z from 'zod'
import { useParams } from 'common'
-import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { InlineLink } from 'components/ui/InlineLink'
import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation'
import { useSchemaCreateMutation } from 'data/database/schema-create-mutation'
@@ -15,7 +12,6 @@ import { useS3VectorsWrapperCreateMutation } from 'data/storage/s3-vectors-wrapp
import { useVectorBucketCreateMutation } from 'data/storage/vector-bucket-create-mutation'
import { useVectorBucketsQuery } from 'data/storage/vector-buckets-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
-import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
@@ -87,34 +83,6 @@ const formId = 'create-storage-bucket-form'
export type CreateBucketForm = z.infer
-export const CreateVectorBucketButton = ({
- onClick,
-}: {
- onClick?: MouseEventHandler
-}) => {
- const { can: canCreateBuckets } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
-
- return (
- }
- disabled={!canCreateBuckets}
- onClick={onClick}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canCreateBuckets ? 'You need additional permissions to create buckets' : undefined,
- },
- }}
- >
- New bucket
-
- )
-}
-
export const CreateVectorBucketDialog = ({
visible,
setVisible,
diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx
index eeba3b2f199..a1a5e83a3c9 100644
--- a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx
+++ b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx
@@ -327,7 +327,9 @@ export const CreateVectorTableSheet = ({ bucketName }: CreateVectorTableSheetPro
Metadata keys
-
+
{fields.map((field, index) => (
diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx
index 14e1fee38eb..b8585792eff 100644
--- a/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx
+++ b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx
@@ -16,7 +16,8 @@ import { PageContainer } from 'ui-patterns/PageContainer'
import { PageSection, PageSectionContent, PageSectionTitle } from 'ui-patterns/PageSection'
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
import { EmptyBucketState } from '../EmptyBucketState'
-import { CreateVectorBucketButton, CreateVectorBucketDialog } from './CreateVectorBucketDialog'
+import { CreateBucketButton } from '../NewBucketButton'
+import { CreateVectorBucketDialog } from './CreateVectorBucketDialog'
/**
* [Joshen] Low-priority refactor: We should use a virtualized table here as per how we do it
@@ -59,124 +60,131 @@ export const VectorsBuckets = () => {
}
return (
-
-
-
-
+ <>
+
+
+
+
- {isLoadingBuckets && }
+ {isLoadingBuckets && }
- {isErrorBuckets && (
-
- )}
+ {isErrorBuckets && (
+
+ )}
- {isSuccessBuckets && (
- <>
- {bucketsList.length === 0 ? (
- setVisible(true)} />
- ) : (
-
-
-
-
setFilterString(e.target.value)}
- icon={
}
- />
+ {isSuccessBuckets && (
+ <>
+ {bucketsList.length === 0 ? (
+
setVisible(true)} />
+ ) : (
+
+
+
+ setFilterString(e.target.value)}
+ icon={ }
+ />
- setVisible(true)} />
-
+
setVisible(true)} />
+
- {isLoadingBuckets ? (
-
- ) : (
-
-
-
-
- {filteredBuckets.length > 0 && (
-
- Icon
+ {isLoadingBuckets ? (
+
+ ) : (
+
+
+
+
+ {filteredBuckets.length > 0 && (
+
+ Icon
+
+ )}
+ Name
+ Created at
+
+ Actions
- )}
- Name
- Created at
-
- Actions
-
-
-
-
- {filteredBuckets.length === 0 && filterString.length > 0 && (
-
-
- No results found
-
- Your search for "{filterString}" did not return any results
-
-
- )}
- {filteredBuckets.map((bucket, idx: number) => {
- const id = `bucket-${idx}`
- const name = bucket.vectorBucketName
- // the creation time is in seconds, convert it to milliseconds
- const created = +bucket.creationTime * 1000
-
- return (
- handleBucketNavigation(name, event)}
- onKeyDown={(event) => {
- if (event.key === 'Enter' || event.key === ' ') {
- event.preventDefault()
- handleBucketNavigation(name, event)
- }
- }}
- tabIndex={0}
- >
-
-
-
-
- {name}
-
-
-
-
+
+
+ {filteredBuckets.length === 0 && filterString.length > 0 && (
+
+
+ No results found
+
+ Your search for "{filterString}" did not return any results
-
-
-
-
-
- )
- })}
-
-
-
- )}
-
- )}
- >
- )}
-
-
-
-
+ )}
+ {filteredBuckets.map((bucket, idx: number) => {
+ const id = `bucket-${idx}`
+ const name = bucket.vectorBucketName
+ // the creation time is in seconds, convert it to milliseconds
+ const created = +bucket.creationTime * 1000
+
+ return (
+ handleBucketNavigation(name, event)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ handleBucketNavigation(name, event)
+ }
+ }}
+ tabIndex={0}
+ >
+
+
+
+
+
+ {name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ })}
+
+
+
+ )}
+
+ )}
+ >
+ )}
+
+
+
+
+ >
)
}
diff --git a/apps/studio/components/interfaces/Storage/__tests__/CreateBucketModal.test.tsx b/apps/studio/components/interfaces/Storage/__tests__/CreateBucketModal.test.tsx
index 1527dac75c2..3f1ca183d7f 100644
--- a/apps/studio/components/interfaces/Storage/__tests__/CreateBucketModal.test.tsx
+++ b/apps/studio/components/interfaces/Storage/__tests__/CreateBucketModal.test.tsx
@@ -3,8 +3,8 @@ import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ProjectContextProvider } from 'components/layouts/ProjectLayout/ProjectContext'
-import { addAPIMock } from 'tests/lib/msw'
import { customRender } from 'tests/lib/custom-render'
+import { addAPIMock } from 'tests/lib/msw'
import { routerMock } from 'tests/lib/route-mock'
import { CreateBucketModal } from '../CreateBucketModal'
@@ -42,7 +42,7 @@ describe(`CreateBucketModal`, () => {
it(`renders a dialog with a form`, async () => {
customRender(
-
+ {}} />
,
{
nuqs: {
diff --git a/apps/studio/components/interfaces/Support/ProjectAndPlanInfo.tsx b/apps/studio/components/interfaces/Support/ProjectAndPlanInfo.tsx
index bd4065c6546..843595a5305 100644
--- a/apps/studio/components/interfaces/Support/ProjectAndPlanInfo.tsx
+++ b/apps/studio/components/interfaces/Support/ProjectAndPlanInfo.tsx
@@ -62,7 +62,6 @@ export function ProjectAndPlanInfo({
{showPlanExpectationInfo &&
orgSlug &&
subscriptionPlanId !== 'enterprise' &&
- subscriptionPlanId !== 'platform' &&
category !== 'Login_issues' && (
)}
diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx
index 824965c256c..ccf1e1a59ba 100644
--- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx
@@ -1,13 +1,10 @@
-import { Lightbulb, TriangleAlert } from 'lucide-react'
import { useState } from 'react'
-import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_ } from 'ui'
import { FeedbackWidget } from './FeedbackWidget'
export const FeedbackDropdown = ({ className }: { className?: string }) => {
const [isOpen, setIsOpen] = useState(false)
- const [stage, setStage] = useState<'select' | 'widget'>('select')
return (
{
open={isOpen}
onOpenChange={(e) => {
setIsOpen(e)
- if (!e) setStage('select')
}}
>
@@ -23,7 +19,6 @@ export const FeedbackDropdown = ({ className }: { className?: string }) => {
asChild
onClick={() => {
setIsOpen((isOpen) => !isOpen)
- setStage('select')
}}
type="text"
className="rounded-full h-[32px] text-foreground-light hover:text-foreground"
@@ -34,36 +29,11 @@ export const FeedbackDropdown = ({ className }: { className?: string }) => {
- {stage === 'select' && (
-
-
What would you like to share?
-
- setIsOpen(false)} asChild>
-
-
-
- Issue
- with my project
-
-
-
- setStage('widget')}>
-
-
- Idea
- to improve Supabase
-
-
-
-
- )}
- {stage === 'widget' && setIsOpen(false)} />}
+ setIsOpen(false)} />
)
}
-
-export default FeedbackDropdown
diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.utils.ts b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.utils.ts
index 497c8cca550..3430de35660 100644
--- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.utils.ts
+++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackDropdown.utils.ts
@@ -57,3 +57,80 @@ export const uploadAttachment = async ({ image, userId }: UploadAttachmentArgs)
})
return signedUrls[0]
}
+
+/**
+ * Client-side heuristic to quickly detect obvious support requests
+ * This provides immediate feedback before the AI classification completes
+ */
+export function isLikelySupportRequest(text: string): boolean {
+ if (!text || text.trim().length === 0) return false
+
+ const lowerText = text.toLowerCase()
+
+ // Common support request patterns
+ const supportPatterns = [
+ // Help requests
+ /need help/i,
+ /having trouble/i,
+ /having issues/i,
+ /having problems/i,
+ /can you help/i,
+ /could you help/i,
+ /please help/i,
+ /need help/i,
+ /how do/i,
+ /how can/i,
+ /how to/i,
+ /why isn't/i,
+ /why doesn't/i,
+ /why won't/i,
+ /why can't/i,
+
+ // Problem indicators
+ /it's not working/i,
+ /it doesn't work/i,
+ /it won't work/i,
+ /it can't work/i,
+ /isn't working/i,
+ /doesn't work/i,
+ /won't work/i,
+ /can't work/i,
+ /not work/i,
+ /i can't/i,
+ /i cannot/i,
+ /unable to/i,
+ /wrong/i,
+ /bad/i,
+
+ // Bug/error indicators
+ /\bbug\b/i,
+ /\bbroken\b/i,
+ /\berror\b/i,
+ /\berrors\b/i,
+ /\bfailed\b/i,
+ /\bfailure\b/i,
+ /\bfailing\b/i,
+ /\bcrash\b/i,
+ /\bcrashed\b/i,
+ /\bcrashing\b/i,
+
+ // Issue/problem keywords
+ /\bissue\b/i,
+ /\bissues\b/i,
+ /\bproblem\b/i,
+ /\bproblems\b/i,
+ /\btrouble\b/i,
+ /\bdifficulty\b/i,
+ /\bdifficulties\b/i,
+
+ // Support-specific phrases
+ /support ticket/i,
+ /contact support/i,
+ /get help/i,
+ /need assistance/i,
+ /require assistance/i,
+ /technical support/i,
+ ]
+
+ return supportPatterns.some((pattern) => pattern.test(lowerText))
+}
diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx
index 69552a572c5..64cc91db9ba 100644
--- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx
@@ -1,7 +1,7 @@
import { useDebounce } from '@uidotdev/usehooks'
import { AnimatePresence, motion } from 'framer-motion'
import { toPng } from 'html-to-image'
-import { Camera, CircleCheck, Image as ImageIcon, Upload, X } from 'lucide-react'
+import { Camera, CircleCheck, HelpCircle, Image as ImageIcon, Upload, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { ChangeEvent, useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
@@ -14,7 +14,6 @@ import { useSendFeedbackMutation } from 'data/feedback/feedback-send'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
-import { DOCS_URL } from 'lib/constants'
import { timeout } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import {
@@ -29,7 +28,11 @@ import {
TextArea_Shadcn_,
} from 'ui'
import { Admonition } from 'ui-patterns'
-import { convertB64toBlob, uploadAttachment } from './FeedbackDropdown.utils'
+import {
+ convertB64toBlob,
+ isLikelySupportRequest,
+ uploadAttachment,
+} from './FeedbackDropdown.utils'
interface FeedbackWidgetProps {
onClose: () => void
@@ -60,6 +63,10 @@ export const FeedbackWidget = ({ onClose }: FeedbackWidgetProps) => {
const { data: category } = useFeedbackCategoryQuery({ prompt: debouncedFeedback })
+ // Use client-side heuristic for immediate feedback, AI result takes precedence when available
+ const isLikelySupport = isLikelySupportRequest(feedback)
+ const effectiveCategory = category ?? (isLikelySupport ? 'support' : null)
+
const { mutate: sendEvent } = useSendEventMutation()
const { mutate: submitFeedback } = useSendFeedbackMutation({
onSuccess: () => {
@@ -158,72 +165,49 @@ export const FeedbackWidget = ({ onClose }: FeedbackWidgetProps) => {
) : (
<>
-
-
- setFeedback(e.target.value)}
- onPaste={handlePasteEvent}
- className="text-sm mt-4 mb-1"
- />
-
-
-
- {category === 'support' && (
-
-
-
- Please{' '}
-
- open a support ticket
- {' '}
- to get help with this issue, as we do not reply to all product feedback.
-
-
-
- )}
-
+
+ setFeedback(e.target.value)}
+ onPaste={handlePasteEvent}
+ className="text-sm mb-1 resize-none"
+ />
+
+ {effectiveCategory === 'support' && (
+
+
+
+ Please{' '}
+
+ open a support ticket
+ {' '}
+ to get help with this issue, as we do not reply to all product feedback.
+
+
+
+ )}
+
+
-
-
-
Have a technical issue?
-
- Contact{' '}
-
-
- support
-
- {' '}
- or{' '}
-
-
- see docs
-
-
- .
-
-
+
{!!screenshot ? (
void }) => {
return (
-
+
-
Your feedback has been sent. Thanks!
-
- We do not always respond to feedback. If you require assistance, please contact support
- instead.
-
+
+
Your feedback has been sent. Thanks!
+
+ We don’t always respond to feedback. If you require assistance, please contact support
+ via the menu
+ instead.
+
+
-
-
-
-
- Create a Support Ticket
-
-
-
-
+
Close
diff --git a/apps/studio/components/ui/FileExplorerAndEditor/index.tsx b/apps/studio/components/ui/FileExplorerAndEditor/index.tsx
index 7a1d3e62d66..51628b36adb 100644
--- a/apps/studio/components/ui/FileExplorerAndEditor/index.tsx
+++ b/apps/studio/components/ui/FileExplorerAndEditor/index.tsx
@@ -3,6 +3,7 @@ import { Edit, File, Plus, Trash } from 'lucide-react'
import { useEffect, useState } from 'react'
import AIEditor from 'components/ui/AIEditor'
+import { toast } from 'sonner'
import {
Button,
ContextMenu_Shadcn_,
@@ -113,8 +114,52 @@ export const FileExplorerAndEditor = ({
}
}
+ const handleStartRename = (id: number) => {
+ // Force re-render of the TreeView with the updated metadata
+ setTreeData({
+ name: '',
+ children: files.map((file) => ({
+ id: file.id.toString(),
+ name: file.name,
+ metadata: {
+ isEditing: file.id === id,
+ originalId: file.id,
+ },
+ })),
+ })
+ }
+
+ const exitEditMode = () => {
+ // Force re-render of the TreeView with the updated metadata
+ setTreeData({
+ name: '',
+ children: files.map((file) => ({
+ id: file.id.toString(),
+ name: file.name,
+ metadata: {
+ isEditing: false,
+ originalId: file.id,
+ },
+ })),
+ })
+ }
+
const handleFileNameChange = (id: number, newName: string) => {
- if (!newName.trim()) return // Don't allow empty names
+ // Don't allow empty names
+ if (!newName.trim()) {
+ toast.error('File name cannot be empty')
+ return exitEditMode()
+ }
+
+ // Check if the new name already exists in other files
+ const isDuplicate = files.some((file) => file.id !== id && file.name === newName)
+ if (isDuplicate) {
+ toast.error(
+ `The name ${newName} already exists in the current directory. Please use a different name.`
+ )
+ return exitEditMode()
+ }
+
const updatedFiles = files.map((file) =>
file.id === id
? {
@@ -157,23 +202,6 @@ export const FileExplorerAndEditor = ({
onFilesChange(updatedFiles)
}
- const handleStartRename = (id: number) => {
- const updatedTreeData = {
- name: '',
- children: files.map((file) => ({
- id: file.id.toString(),
- name: file.name,
- metadata: {
- isEditing: file.id === id,
- originalId: file.id,
- },
- })),
- }
-
- // Force re-render of the TreeView with the updated metadata
- setTreeData(updatedTreeData)
- }
-
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(true)
@@ -271,7 +299,9 @@ export const FileExplorerAndEditor = ({
}
}}
onClick={() => {
- if (originalId !== null) handleFileSelect(originalId)
+ if (originalId !== null && !element.metadata?.isEditing) {
+ handleFileSelect(originalId)
+ }
}}
onDoubleClick={() => {
if (originalId !== null) {
diff --git a/apps/studio/components/ui/InfoPill.tsx b/apps/studio/components/ui/InfoPill.tsx
deleted file mode 100644
index d1ff71ae968..00000000000
--- a/apps/studio/components/ui/InfoPill.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { ExternalLink } from 'lucide-react'
-import React, { ReactNode } from 'react'
-
-import { Badge } from 'ui/src/components/shadcn/ui/badge'
-import {
- HoverCard,
- HoverCardContent,
- HoverCardTrigger,
-} from 'ui/src/components/shadcn/ui/hover-card'
-
-interface Link {
- url: string
- label: string
-}
-
-interface InfoPillProps {
- label: string | ReactNode
- icon: ReactNode
- title: string
- description: string | ReactNode
- links?: Link[]
-}
-
-export const InfoPill = ({ label, icon, title, description, links }: InfoPillProps) => {
- return (
-
-
-
-
- {icon} {label}
-
-
-
-
-
- {React.cloneElement(icon as React.ReactElement, {
- className: 'w-5 h-5',
- strokeWidth: 1.5,
- })}
-
-
-
{title}
-
{description}
- {links && links.length > 0 && (
-
- )}
-
-
-
- )
-}
diff --git a/apps/studio/data/analytics/org-daily-stats-query.test.ts b/apps/studio/data/analytics/org-daily-stats-query.test.ts
new file mode 100644
index 00000000000..a3636d90758
--- /dev/null
+++ b/apps/studio/data/analytics/org-daily-stats-query.test.ts
@@ -0,0 +1,125 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { getOrgDailyStats } from './org-daily-stats-query'
+
+vi.mock('data/fetchers', () => ({
+ get: vi.fn(),
+ handleError: vi.fn((error) => {
+ throw error
+ }),
+}))
+
+describe('org-daily-stats-query', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe('getOrgDailyStats', () => {
+ it('throws error when orgSlug is not provided', async () => {
+ await expect(
+ getOrgDailyStats({
+ orgSlug: undefined,
+ startDate: '2025-01-01',
+ endDate: '2025-01-31',
+ })
+ ).rejects.toThrow('Org slug is required')
+ })
+
+ it('throws error when startDate is not provided', async () => {
+ await expect(
+ getOrgDailyStats({
+ orgSlug: 'test-org',
+ startDate: undefined,
+ endDate: '2025-01-31',
+ })
+ ).rejects.toThrow('Start date is required')
+ })
+
+ it('throws error when endDate is not provided', async () => {
+ await expect(
+ getOrgDailyStats({
+ orgSlug: 'test-org',
+ startDate: '2025-01-01',
+ endDate: undefined,
+ })
+ ).rejects.toThrow('Start date is required')
+ })
+
+ it('calls API with correct parameters including project_ref', async () => {
+ const { get } = await import('data/fetchers')
+ const mockGet = get as unknown as ReturnType
+
+ const mockResponse = { usages: [] }
+ mockGet.mockResolvedValueOnce({ data: mockResponse, error: null })
+
+ const result = await getOrgDailyStats({
+ orgSlug: 'test-org',
+ startDate: '2025-01-01',
+ endDate: '2025-01-31',
+ projectRef: 'test-project-ref',
+ })
+
+ expect(mockGet).toHaveBeenCalledWith(
+ '/platform/organizations/{slug}/usage/daily',
+ expect.objectContaining({
+ params: {
+ path: { slug: 'test-org' },
+ query: {
+ start: '2025-01-01',
+ end: '2025-01-31',
+ project_ref: 'test-project-ref',
+ },
+ },
+ })
+ )
+ expect(result).toEqual(mockResponse)
+ })
+
+ it('calls API without project_ref when not provided', async () => {
+ const { get } = await import('data/fetchers')
+ const mockGet = get as unknown as ReturnType
+
+ const mockResponse = { usages: [] }
+ mockGet.mockResolvedValueOnce({ data: mockResponse, error: null })
+
+ await getOrgDailyStats({
+ orgSlug: 'test-org',
+ startDate: '2025-01-01',
+ endDate: '2025-01-31',
+ })
+
+ expect(mockGet).toHaveBeenCalledWith(
+ '/platform/organizations/{slug}/usage/daily',
+ expect.objectContaining({
+ params: {
+ path: { slug: 'test-org' },
+ query: {
+ start: '2025-01-01',
+ end: '2025-01-31',
+ project_ref: undefined,
+ },
+ },
+ })
+ )
+ })
+
+ it('handles API errors correctly', async () => {
+ const { get, handleError } = await import('data/fetchers')
+ const mockGet = get as unknown as ReturnType
+ const mockHandleError = handleError as unknown as ReturnType
+
+ const mockError = { message: 'API Error' }
+ mockGet.mockResolvedValueOnce({ data: null, error: mockError })
+ mockHandleError.mockImplementation((error) => {
+ throw new Error(error.message)
+ })
+
+ await expect(
+ getOrgDailyStats({
+ orgSlug: 'test-org',
+ startDate: '2025-01-01',
+ endDate: '2025-01-31',
+ })
+ ).rejects.toThrow('API Error')
+ })
+ })
+})
diff --git a/apps/studio/data/analytics/org-daily-stats-query.ts b/apps/studio/data/analytics/org-daily-stats-query.ts
index 1a3c05ac6e2..3229ffdf0bf 100644
--- a/apps/studio/data/analytics/org-daily-stats-query.ts
+++ b/apps/studio/data/analytics/org-daily-stats-query.ts
@@ -42,10 +42,6 @@ export enum PricingMetric {
LOG_DRAIN_EVENTS = 'LOG_DRAIN_EVENTS',
AUTH_MFA_PHONE = 'AUTH_MFA_PHONE',
AUTH_MFA_WEB_AUTHN = 'AUTH_MFA_WEB_AUTHN',
- ACTIVE_COMPUTE_HOURS = 'ACTIVE_COMPUTE_HOURS',
- LOG_INGESTION = 'LOG_INGESTION',
- LOG_QUERYING = 'LOG_QUERYING',
- LOG_STORAGE = 'LOG_STORAGE',
}
export enum ComputeUsageMetric {
@@ -137,7 +133,7 @@ export async function getOrgDailyStats(
query: {
start: startDate,
end: endDate,
- projectRef,
+ project_ref: projectRef,
},
},
signal,
diff --git a/apps/studio/data/config/project-postgrest-config-query.ts b/apps/studio/data/config/project-postgrest-config-query.ts
index 178defcfb8c..a50c479e685 100644
--- a/apps/studio/data/config/project-postgrest-config-query.ts
+++ b/apps/studio/data/config/project-postgrest-config-query.ts
@@ -25,7 +25,7 @@ export async function getProjectPostgrestConfig(
})
if (error) handleError(error)
// [Joshen] Not sure why but db_pool isn't part of the API typing
- // https://github.com/supabase/infrastructure/blob/develop/api/src/routes/platform/projects/ref/config/postgrest.dto.ts#L6
+ // https://github.com/supabase/platform/blob/develop/api/src/routes/platform/projects/ref/config/postgrest.dto.ts#L6
return data as unknown as PostgrestConfigResponse
}
diff --git a/apps/studio/data/config/project-settings-v2-query.ts b/apps/studio/data/config/project-settings-v2-query.ts
index c86950615f7..a7684df716b 100644
--- a/apps/studio/data/config/project-settings-v2-query.ts
+++ b/apps/studio/data/config/project-settings-v2-query.ts
@@ -43,7 +43,7 @@ export const useProjectSettingsV2Query = (
}: UseCustomQueryOptions = {}
) => {
// [Joshen] Sync with API perms checking here - shouldReturnApiKeys
- // https://github.com/supabase/infrastructure/blob/develop/api/src/routes/platform/projects/ref/settings.controller.ts#L92
+ // https://github.com/supabase/platform/blob/develop/api/src/routes/platform/projects/ref/settings.controller.ts#L92
const { can: canReadAPIKeys } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'*'
diff --git a/apps/studio/data/integrations/vercel-integration-create-mutation.ts b/apps/studio/data/integrations/vercel-integration-create-mutation.ts
index 996dd0507f6..4eb1e164389 100644
--- a/apps/studio/data/integrations/vercel-integration-create-mutation.ts
+++ b/apps/studio/data/integrations/vercel-integration-create-mutation.ts
@@ -37,7 +37,7 @@ export async function createVercelIntegration({
if (error) handleError(error)
// [Joshen] API isn't typed on this endpoint
- // https://github.com/supabase/infrastructure/blob/develop/api/src/routes/platform/integrations/vercel/vercel-integration.controller.ts#L50
+ // https://github.com/supabase/platform/blob/develop/api/src/routes/platform/integrations/vercel/vercel-integration.controller.ts#L50
return data as { id: string }
}
diff --git a/apps/studio/data/organizations/organization-audit-logs-query.ts b/apps/studio/data/organizations/organization-audit-logs-query.ts
index 1701003f185..23d6368a052 100644
--- a/apps/studio/data/organizations/organization-audit-logs-query.ts
+++ b/apps/studio/data/organizations/organization-audit-logs-query.ts
@@ -8,6 +8,7 @@ export type AuditLog = {
action: {
metadata: {
method?: string
+ route?: string
status?: number
}[]
name: string
@@ -17,13 +18,17 @@ export type AuditLog = {
type: 'user' | string
metadata: {
email?: string
+ ip?: string
+ tokenType?: string
}[]
}
target: {
description: string
metadata: {
org_slug?: string
- project_ref?: string
+ project_ref?: string | null
+ ref?: string | null
+ slug?: string | null
}
}
occurred_at: string
diff --git a/apps/studio/data/organizations/organization-create-mutation.ts b/apps/studio/data/organizations/organization-create-mutation.ts
index acb1058db3c..f18a8bf2f80 100644
--- a/apps/studio/data/organizations/organization-create-mutation.ts
+++ b/apps/studio/data/organizations/organization-create-mutation.ts
@@ -12,7 +12,7 @@ export type OrganizationCreateVariables = {
name: string
kind?: string
size?: string
- tier: 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team' | 'tier_enterprise' | 'tier_platform'
+ tier: 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team' | 'tier_enterprise'
payment_method?: string
billing_name?: string | null
address?: CustomerAddress | null
diff --git a/apps/studio/data/read-replicas/replicas-status-query.ts b/apps/studio/data/read-replicas/replicas-status-query.ts
index 796d485c240..c815755af56 100644
--- a/apps/studio/data/read-replicas/replicas-status-query.ts
+++ b/apps/studio/data/read-replicas/replicas-status-query.ts
@@ -6,7 +6,7 @@ import type { ResponseError, UseCustomQueryOptions } from 'types'
import { replicaKeys } from './keys'
// [Joshen] Is it possible to import this from the code gen?
-// https://github.com/supabase/infrastructure/blob/develop/api/src/routes/platform/projects/ref/databases-statuses.dto.ts#L7
+// https://github.com/supabase/platform/blob/develop/api/src/routes/platform/projects/ref/databases-statuses.dto.ts#L7
export enum ReplicaInitializationStatus {
'InProgress' = 'in_progress',
'Completed' = 'completed',
diff --git a/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts
index 92de60e60e6..6937de4ed92 100644
--- a/apps/studio/data/reports/database-charts.ts
+++ b/apps/studio/data/reports/database-charts.ts
@@ -23,7 +23,7 @@ export const getReportAttributesV2: (
id: 'ram-usage',
label: 'Memory usage',
docsUrl: `${DOCS_URL}/guides/telemetry/reports#memory-usage`,
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
hide: false,
showTooltip: true,
showLegend: true,
@@ -68,7 +68,7 @@ export const getReportAttributesV2: (
syncId: 'database-reports',
format: '%',
valuePrecision: 2,
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
hide: false,
showTooltip: true,
showLegend: true,
@@ -135,7 +135,7 @@ export const getReportAttributesV2: (
label: 'Disk Input/Output operations per second (IOPS)',
docsUrl: `${DOCS_URL}/guides/telemetry/reports#disk-inputoutput-operations-per-second-iops`,
syncId: 'database-reports',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
hide: false,
showTooltip: true,
valuePrecision: 2,
@@ -179,7 +179,7 @@ export const getReportAttributesV2: (
label: 'Disk IO Usage',
docsUrl: `${DOCS_URL}/guides/telemetry/reports#disk-io-usage`,
syncId: 'database-reports',
- availableIn: ['team', 'enterprise', 'platform'],
+ availableIn: ['team', 'enterprise'],
hide: false,
showTooltip: true,
format: '%',
@@ -242,7 +242,7 @@ export const getReportAttributesV2: (
label: 'Database Connections',
syncId: 'database-reports',
valuePrecision: 0,
- availableIn: ['pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['pro', 'team', 'enterprise'],
hide: isFreePlan,
showTooltip: true,
showLegend: true,
@@ -306,7 +306,7 @@ export const getReportAttributesV2: (
label: 'Dedicated Pooler Client Connections',
syncId: 'database-reports',
valuePrecision: 0,
- availableIn: ['pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['pro', 'team', 'enterprise'],
hide: isFreePlan,
showTooltip: true,
showLegend: true,
@@ -338,7 +338,7 @@ export const getReportAttributesV2: (
label: 'Shared Pooler (Supavisor) client connections',
syncId: 'database-reports',
valuePrecision: 0,
- availableIn: ['pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['pro', 'team', 'enterprise'],
hide: isFreePlan,
showTooltip: false,
showLegend: false,
@@ -361,7 +361,7 @@ export const getReportAttributesV2: (
label: 'Disk Usage',
syncId: 'database-reports',
valuePrecision: 2,
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
hide: false,
showTooltip: true,
showLegend: true,
diff --git a/apps/studio/data/reports/v2/auth.config.ts b/apps/studio/data/reports/v2/auth.config.ts
index 43c75f1bc12..69e9ab74777 100644
--- a/apps/studio/data/reports/v2/auth.config.ts
+++ b/apps/studio/data/reports/v2/auth.config.ts
@@ -523,7 +523,7 @@ export const createUsageReportConfig = ({
defaultChartStyle: 'line',
titleTooltip:
"Users who generated any Auth event in this period. This metric tracks authentication activity, not total product usage. Some active users won't appear here if their session stayed valid.",
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{ attribute: 'ActiveUsers', provider: 'logs', label: 'Auth Activity', enabled: true },
@@ -553,7 +553,7 @@ export const createUsageReportConfig = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of sign in attempts by type.',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{
@@ -608,7 +608,7 @@ export const createUsageReportConfig = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of sign ups.',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{
@@ -641,7 +641,7 @@ export const createUsageReportConfig = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of password reset requests.',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{
@@ -690,7 +690,7 @@ export const createErrorsReportConfig = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of auth errors by status code from the API Gateway.',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const sql = AUTH_REPORT_SQL.ErrorsByStatus(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
@@ -716,7 +716,7 @@ export const createErrorsReportConfig = ({
defaultChartStyle: 'line',
titleTooltip:
'The total number of auth errors by Supabase Auth error code from the API Gateway.',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const sql = AUTH_REPORT_SQL.ErrorsByAuthCode(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
@@ -775,7 +775,7 @@ export const createLatencyReportConfig = ({
defaultChartStyle: 'line',
titleTooltip:
'Basic processing time metrics for sign in operations within the auth server (excludes network latency).',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{
@@ -816,7 +816,7 @@ export const createLatencyReportConfig = ({
defaultChartStyle: 'line',
titleTooltip:
'Percentile processing time metrics for sign in operations within the auth server (excludes network latency).',
- availableIn: ['pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{
@@ -857,7 +857,7 @@ export const createLatencyReportConfig = ({
defaultChartStyle: 'line',
titleTooltip:
'Basic processing time metrics for sign up operations within the auth server (excludes network latency).',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{
@@ -898,7 +898,7 @@ export const createLatencyReportConfig = ({
defaultChartStyle: 'line',
titleTooltip:
'Percentile processing time metrics for sign up operations within the auth server (excludes network latency).',
- availableIn: ['pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['pro', 'team', 'enterprise'],
dataProvider: async () => {
const attributes = [
{
diff --git a/apps/studio/data/reports/v2/edge-functions.config.ts b/apps/studio/data/reports/v2/edge-functions.config.ts
index 6c3ecc828e5..15fef6c6454 100644
--- a/apps/studio/data/reports/v2/edge-functions.config.ts
+++ b/apps/studio/data/reports/v2/edge-functions.config.ts
@@ -213,7 +213,7 @@ export const edgeFunctionReports = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of edge function invocations over time.',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const sql = METRIC_SQL.TotalInvocations(interval, filters)
const response = await fetchLogs(projectRef, sql, startDate, endDate)
@@ -245,7 +245,7 @@ export const edgeFunctionReports = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of edge function executions by status code.',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const sql = METRIC_SQL.ExecutionStatusCodes(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
@@ -277,7 +277,7 @@ export const edgeFunctionReports = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'Average execution time for edge functions.',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
YAxisProps: {
width: 50,
tickFormatter: (value: number) => `${value}ms`,
@@ -339,7 +339,7 @@ export const edgeFunctionReports = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'The total number of edge function invocations by region.',
- availableIn: ['pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['pro', 'team', 'enterprise'],
dataProvider: async () => {
const sql = METRIC_SQL.InvocationsByRegion(interval, filters)
const rawData = await fetchLogs(projectRef, sql, startDate, endDate)
diff --git a/apps/studio/data/reports/v2/realtime.config.ts b/apps/studio/data/reports/v2/realtime.config.ts
index ad25d621e67..bd4b5fe5a25 100644
--- a/apps/studio/data/reports/v2/realtime.config.ts
+++ b/apps/studio/data/reports/v2/realtime.config.ts
@@ -48,7 +48,7 @@ export const realtimeReports = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'Total number of connected realtime clients.',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const data = await runInfraMonitoringQuery(
projectRef,
@@ -81,7 +81,7 @@ export const realtimeReports = ({
hideChartType: false,
defaultChartStyle: 'bar',
titleTooltip: '',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const { data } = await runInfraMonitoringQuery(
projectRef,
@@ -120,7 +120,7 @@ export const realtimeReports = ({
hideChartType: false,
defaultChartStyle: 'bar',
titleTooltip: '',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const { data } = await runInfraMonitoringQuery(
projectRef,
@@ -159,7 +159,7 @@ export const realtimeReports = ({
hideChartType: false,
defaultChartStyle: 'bar',
titleTooltip: '',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const { data } = await runInfraMonitoringQuery(
projectRef,
@@ -197,7 +197,7 @@ export const realtimeReports = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: '',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
dataProvider: async () => {
const data = await runInfraMonitoringQuery(
projectRef,
@@ -231,7 +231,7 @@ export const realtimeReports = ({
hideChartType: false,
defaultChartStyle: 'line',
titleTooltip: 'Median size of message payloads sent',
- availableIn: ['free', 'pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
YAxisProps: {
width: 50,
tickFormatter: (value: number) => `${value}B`,
@@ -271,7 +271,7 @@ export const realtimeReports = ({
defaultChartStyle: 'line',
titleTooltip:
'Median time between database commit and broadcast when using broadcast from database.',
- availableIn: ['pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['pro', 'team', 'enterprise'],
YAxisProps: {
width: 50,
tickFormatter: (value: number) => `${value}ms`,
@@ -311,7 +311,7 @@ export const realtimeReports = ({
defaultChartStyle: 'line',
titleTooltip:
'Execution median time of RLS (Row Level Security) to subscribe to a private channel',
- availableIn: ['pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['pro', 'team', 'enterprise'],
YAxisProps: {
width: 50,
tickFormatter: (value: number) => `${value}ms`,
@@ -351,7 +351,7 @@ export const realtimeReports = ({
defaultChartStyle: 'line',
titleTooltip:
'Execution median time of RLS (Row Level Security) to publish to a private channel',
- availableIn: ['pro', 'team', 'enterprise', 'platform'],
+ availableIn: ['pro', 'team', 'enterprise'],
YAxisProps: {
width: 50,
tickFormatter: (value: number) => `${value}ms`,
diff --git a/apps/studio/data/sso/sso-config-query.ts b/apps/studio/data/sso/sso-config-query.ts
index 1892cd8a472..e411d0b2fd4 100644
--- a/apps/studio/data/sso/sso-config-query.ts
+++ b/apps/studio/data/sso/sso-config-query.ts
@@ -44,7 +44,7 @@ export const useOrgSSOConfigQuery = (
) => {
const { data: organization } = useSelectedOrganizationQuery()
const plan = organization?.plan.id
- const canSetupSSOConfig = ['team', 'enterprise', 'platform'].includes(plan ?? '')
+ const canSetupSSOConfig = ['team', 'enterprise'].includes(plan ?? '')
return useQuery({
queryKey: orgSSOKeys.orgSSOConfig(orgSlug),
diff --git a/apps/studio/data/storage/iceberg-namespace-create-mutation.ts b/apps/studio/data/storage/iceberg-namespace-create-mutation.ts
index fc638a387ff..86a1bd89784 100644
--- a/apps/studio/data/storage/iceberg-namespace-create-mutation.ts
+++ b/apps/studio/data/storage/iceberg-namespace-create-mutation.ts
@@ -1,56 +1,30 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
-import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils'
-import { constructHeaders, fetchHandler, handleError } from 'data/fetchers'
+import { handleError, post } from 'data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { storageKeys } from './keys'
type CreateIcebergNamespaceVariables = {
projectRef?: string
- catalogUri: string
warehouse: string
namespace: string
}
-const errorPrefix = 'Failed to create Iceberg namespace'
-
async function createIcebergNamespace({
projectRef,
- catalogUri,
warehouse,
namespace,
}: CreateIcebergNamespaceVariables) {
- try {
- if (!projectRef) throw new Error(`${errorPrefix}: projectRef is required`)
+ if (!projectRef) throw new Error('projectRef is required')
- const tempApiKeyObj = await getOrRefreshTemporaryApiKey(projectRef)
- const tempApiKey = tempApiKeyObj.apiKey
+ const { error } = await post('/platform/storage/{ref}/analytics-buckets/{id}/namespaces', {
+ params: { path: { ref: projectRef, id: warehouse } },
+ body: { namespace },
+ })
- let headers = new Headers()
- headers = await constructHeaders({
- 'Content-Type': 'application/json',
- apikey: tempApiKey,
- })
- headers.delete('Authorization')
-
- const url = `${catalogUri}/v1/${warehouse}/namespaces`.replaceAll(/(?>
@@ -72,18 +46,9 @@ export const useIcebergNamespaceCreateMutation = ({
return useMutation({
mutationFn: (vars) => createIcebergNamespace({ ...vars }),
async onSuccess(data, variables, context) {
- await queryClient.invalidateQueries({
- queryKey: storageKeys.icebergNamespace({
- projectRef: variables.projectRef,
- catalog: variables.catalogUri,
- warehouse: variables.warehouse,
- namespace: variables.namespace,
- }),
- })
await queryClient.invalidateQueries({
queryKey: storageKeys.icebergNamespaces({
projectRef: variables.projectRef,
- catalog: variables.catalogUri,
warehouse: variables.warehouse,
}),
})
diff --git a/apps/studio/data/storage/iceberg-namespace-delete-mutation.ts b/apps/studio/data/storage/iceberg-namespace-delete-mutation.ts
index 86bbc58faf2..43d3e55b4ff 100644
--- a/apps/studio/data/storage/iceberg-namespace-delete-mutation.ts
+++ b/apps/studio/data/storage/iceberg-namespace-delete-mutation.ts
@@ -1,52 +1,32 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
-import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils'
-import { constructHeaders, fetchHandler, handleError } from 'data/fetchers'
+import { del, handleError } from 'data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { storageKeys } from './keys'
type DeleteIcebergNamespaceVariables = {
projectRef?: string
- catalogUri: string
warehouse: string
namespace: string
}
-const errorPrefix = 'Failed to delete Iceberg namespace'
-
async function deleteIcebergNamespace({
projectRef,
- catalogUri,
warehouse,
namespace,
}: DeleteIcebergNamespaceVariables) {
- try {
- if (!projectRef) throw new Error(`${errorPrefix}: projectRef is required`)
+ if (!projectRef) throw new Error('projectRef is required')
- const tempApiKeyObj = await getOrRefreshTemporaryApiKey(projectRef)
- const tempApiKey = tempApiKeyObj.apiKey
+ const { error } = await del(
+ '/platform/storage/{ref}/analytics-buckets/{id}/namespaces/{namespace}',
+ {
+ params: { path: { ref: projectRef, id: warehouse, namespace } },
+ }
+ )
- let headers = new Headers()
- headers = await constructHeaders({
- 'Content-Type': 'application/json',
- apikey: tempApiKey,
- })
- headers.delete('Authorization')
-
- const url = `${catalogUri}/v1/${warehouse}/namespaces/${namespace}`.replaceAll(
- /(?>
@@ -71,7 +51,6 @@ export const useIcebergNamespaceDeleteMutation = ({
await queryClient.invalidateQueries({
queryKey: storageKeys.icebergNamespaces({
projectRef: variables.projectRef,
- catalog: variables.catalogUri,
warehouse: variables.warehouse,
}),
})
diff --git a/apps/studio/data/storage/iceberg-namespace-table-create-mutation.ts b/apps/studio/data/storage/iceberg-namespace-table-create-mutation.ts
new file mode 100644
index 00000000000..4b5ae9df424
--- /dev/null
+++ b/apps/studio/data/storage/iceberg-namespace-table-create-mutation.ts
@@ -0,0 +1,81 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+
+import { components } from 'api-types'
+import { handleError, post } from 'data/fetchers'
+import type { ResponseError, UseCustomMutationOptions } from 'types'
+import { storageKeys } from './keys'
+
+export type NamespaceTableFields = components['schemas']['CreateNamespaceTableBody']['fields']
+
+type CreateIcebergNamespaceTableVariables = {
+ projectRef?: string
+ warehouse: string
+ namespace: string
+ name: string
+ fields: NamespaceTableFields
+}
+
+async function createIcebergNamespaceTable({
+ projectRef,
+ warehouse,
+ namespace,
+ name,
+ fields,
+}: CreateIcebergNamespaceTableVariables) {
+ if (!projectRef) throw new Error('projectRef is required')
+
+ const { data, error } = await post(
+ '/platform/storage/{ref}/analytics-buckets/{id}/namespaces/{namespace}/tables',
+ {
+ params: { path: { ref: projectRef, id: warehouse, namespace } },
+ body: { name, fields },
+ }
+ )
+
+ if (error) handleError(error)
+ return data
+}
+
+type IcebergNamespaceTableCreateData = Awaited>
+
+export const useIcebergNamespaceTableCreateMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseCustomMutationOptions<
+ IcebergNamespaceTableCreateData,
+ ResponseError,
+ CreateIcebergNamespaceTableVariables
+ >,
+ 'mutationFn'
+> = {}) => {
+ const queryClient = useQueryClient()
+
+ return useMutation<
+ IcebergNamespaceTableCreateData,
+ ResponseError,
+ CreateIcebergNamespaceTableVariables
+ >({
+ mutationFn: (vars) => createIcebergNamespaceTable({ ...vars }),
+ async onSuccess(data, variables, context) {
+ await queryClient.invalidateQueries({
+ queryKey: storageKeys.icebergNamespace({
+ projectRef: variables.projectRef,
+ warehouse: variables.warehouse,
+ namespace: variables.namespace,
+ }),
+ })
+ await onSuccess?.(data, variables, context)
+ },
+ async onError(data, variables, context) {
+ if (onError === undefined) {
+ toast.error(`Failed to create Iceberg namespace table: ${data.message}`)
+ } else {
+ onError(data, variables, context)
+ }
+ },
+ ...options,
+ })
+}
diff --git a/apps/studio/data/storage/iceberg-namespace-table-delete-mutation.ts b/apps/studio/data/storage/iceberg-namespace-table-delete-mutation.ts
index 23ebca37862..cd16fb305d5 100644
--- a/apps/studio/data/storage/iceberg-namespace-table-delete-mutation.ts
+++ b/apps/studio/data/storage/iceberg-namespace-table-delete-mutation.ts
@@ -1,52 +1,37 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
-import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils'
-import { constructHeaders, fetchHandler, handleError } from 'data/fetchers'
+import { del, handleError } from 'data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { storageKeys } from './keys'
type DeleteIcebergNamespaceTableVariables = {
- catalogUri: string
warehouse: string
namespace: string
table: string
projectRef?: string
}
-const errorPrefix = 'Failed to delete Iceberg namespace table'
-
async function deleteIcebergNamespaceTable({
projectRef,
- catalogUri,
warehouse,
namespace,
table,
}: DeleteIcebergNamespaceTableVariables) {
- try {
- if (!projectRef) throw new Error(`${errorPrefix}: projectRef is required`)
+ if (!projectRef) throw new Error('projectRef is required')
- const tempApiKeyObj = await getOrRefreshTemporaryApiKey(projectRef)
- const tempApiKey = tempApiKeyObj.apiKey
+ const { error } = await del(
+ '/platform/storage/{ref}/analytics-buckets/{id}/namespaces/{namespace}/tables/{table}',
+ {
+ params: {
+ path: { ref: projectRef, id: warehouse, namespace, table },
+ query: { purge: true },
+ },
+ }
+ )
- let headers = new Headers()
- headers = await constructHeaders({
- 'Content-Type': 'application/json',
- apikey: tempApiKey,
- })
- headers.delete('Authorization')
-
- const url =
- `${catalogUri}/v1/${warehouse}/namespaces/${namespace}/tables/${table}?purgeRequested=true`.replaceAll(
- /(?>
@@ -75,7 +60,6 @@ export const useIcebergNamespaceTableDeleteMutation = ({
await queryClient.invalidateQueries({
queryKey: storageKeys.icebergNamespace({
projectRef: variables.projectRef,
- catalog: variables.catalogUri,
warehouse: variables.warehouse,
namespace: variables.namespace,
}),
diff --git a/apps/studio/data/storage/iceberg-namespace-tables-query.ts b/apps/studio/data/storage/iceberg-namespace-tables-query.ts
index e5edcf0bdca..fd699bb6674 100644
--- a/apps/studio/data/storage/iceberg-namespace-tables-query.ts
+++ b/apps/studio/data/storage/iceberg-namespace-tables-query.ts
@@ -1,55 +1,33 @@
import { useQuery } from '@tanstack/react-query'
-import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils'
-import { constructHeaders, fetchHandler, handleError } from 'data/fetchers'
+import { get, handleError } from 'data/fetchers'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { storageKeys } from './keys'
type GetNamespaceTablesVariables = {
- catalogUri: string
- warehouse: string
- namespace: string
+ warehouse?: string
+ namespace?: string
projectRef?: string
}
-const errorPrefix = 'Failed to retrieve Iceberg namespace tables'
+async function getNamespaceTables(
+ { projectRef, warehouse, namespace }: GetNamespaceTablesVariables,
+ signal?: AbortSignal
+) {
+ if (!projectRef) throw new Error('projectRef is required')
+ if (!namespace) throw new Error('namespace is required')
+ if (!warehouse) throw new Error('warehouse is required')
-async function getNamespaceTables({
- projectRef,
- catalogUri,
- warehouse,
- namespace,
-}: GetNamespaceTablesVariables) {
- try {
- if (!projectRef) throw new Error(`${errorPrefix}: projectRef is required`)
-
- const tempApiKeyObj = await getOrRefreshTemporaryApiKey(projectRef)
- const tempApiKey = tempApiKeyObj.apiKey
-
- let headers = new Headers()
- headers = await constructHeaders({
- 'Content-Type': 'application/json',
- apikey: tempApiKey,
- })
- headers.delete('Authorization')
-
- const url = `${catalogUri}/v1/${warehouse}/namespaces/${namespace}/tables`.replaceAll(
- /(? i.name)
- } catch (error) {
- handleError(error)
- }
+ if (error) handleError(error)
+ return data.data.map((x) => x.name)
}
type IcebergNamespaceTablesData = Awaited>
@@ -63,22 +41,20 @@ export const useIcebergNamespaceTablesQuery = = {}
) => {
- const { projectRef, catalogUri, warehouse, namespace } = params
+ const { projectRef, warehouse, namespace } = params
return useQuery({
queryKey: storageKeys.icebergNamespaceTables({
projectRef,
warehouse,
namespace,
- catalog: catalogUri,
}),
queryFn: () => getNamespaceTables({ ...params }),
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof warehouse !== 'undefined' &&
- typeof namespace !== 'undefined' &&
- typeof catalogUri !== 'undefined',
+ typeof namespace !== 'undefined',
...options,
})
}
diff --git a/apps/studio/data/storage/iceberg-namespaces-query.ts b/apps/studio/data/storage/iceberg-namespaces-query.ts
index be5e4057388..651efe03054 100644
--- a/apps/studio/data/storage/iceberg-namespaces-query.ts
+++ b/apps/studio/data/storage/iceberg-namespaces-query.ts
@@ -1,46 +1,28 @@
import { useQuery } from '@tanstack/react-query'
-import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils'
-import { constructHeaders, fetchHandler, handleError } from 'data/fetchers'
+import { get, handleError } from 'data/fetchers'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { storageKeys } from './keys'
type GetNamespacesVariables = {
- catalogUri: string
- warehouse: string
+ warehouse?: string
projectRef?: string
}
-const errorPrefix = 'Failed to retrieve Iceberg namespaces'
+async function getNamespaces(
+ { projectRef, warehouse }: GetNamespacesVariables,
+ signal?: AbortSignal
+) {
+ if (!projectRef) throw new Error('projectRef is required')
+ if (!warehouse) throw new Error('warehouse is required')
-async function getNamespaces({ projectRef, catalogUri, warehouse }: GetNamespacesVariables) {
- try {
- if (!projectRef) throw new Error(`${errorPrefix}: projectRef is required`)
+ const { data, error } = await get('/platform/storage/{ref}/analytics-buckets/{id}/namespaces', {
+ params: { path: { ref: projectRef, id: warehouse } },
+ signal,
+ })
- const tempApiKeyObj = await getOrRefreshTemporaryApiKey(projectRef)
- const tempApiKey = tempApiKeyObj.apiKey
-
- let headers = new Headers()
- headers = await constructHeaders({
- 'Content-Type': 'application/json',
- apikey: tempApiKey,
- })
- headers.delete('Authorization')
-
- const url = `${catalogUri}/v1/${warehouse}/namespaces`.replaceAll(/(? x.namespace).flat()
}
type IcebergNamespacesData = Awaited>
@@ -54,20 +36,17 @@ export const useIcebergNamespacesQuery = (
...options
}: UseCustomQueryOptions = {}
) => {
- const { projectRef, catalogUri, warehouse } = params
+ const { projectRef, warehouse } = params
return useQuery({
queryKey: storageKeys.icebergNamespaces({
projectRef,
warehouse,
- catalog: catalogUri,
}),
- queryFn: () => getNamespaces({ ...params }),
+ queryFn: ({ signal }) => getNamespaces({ projectRef, warehouse }, signal),
enabled:
options &&
typeof projectRef !== 'undefined' &&
- typeof catalogUri !== 'undefined' &&
- catalogUri.length > 0 &&
typeof warehouse !== 'undefined' &&
warehouse.length > 0,
...options,
diff --git a/apps/studio/data/storage/keys.ts b/apps/studio/data/storage/keys.ts
index 952d0a59059..fb9bf4a9315 100644
--- a/apps/studio/data/storage/keys.ts
+++ b/apps/studio/data/storage/keys.ts
@@ -9,45 +9,24 @@ export const storageKeys = {
vectorBucketsIndexes: (projectRef: string | undefined, vectorBucketName: string | undefined) =>
['projects', projectRef, 'vector-buckets', vectorBucketName, 'indexes'] as const,
archive: (projectRef: string | undefined) => ['projects', projectRef, 'archive'] as const,
- icebergNamespaces: ({
- projectRef,
- catalog,
- warehouse,
- }: {
- projectRef?: string
- catalog: string
- warehouse: string
- }) => [projectRef, 'catalog', catalog, 'warehouse', warehouse, 'namespaces'] as const,
+ icebergNamespaces: ({ projectRef, warehouse }: { projectRef?: string; warehouse?: string }) =>
+ [projectRef, 'warehouse', warehouse, 'namespaces'] as const,
icebergNamespace: ({
projectRef,
- catalog,
warehouse,
namespace,
}: {
projectRef?: string
- catalog: string
warehouse: string
namespace: string
- }) => [projectRef, 'catalog', catalog, 'warehouse', warehouse, 'namespaces', namespace] as const,
+ }) => [projectRef, 'warehouse', warehouse, 'namespaces', namespace] as const,
icebergNamespaceTables: ({
projectRef,
- catalog,
warehouse,
namespace,
}: {
projectRef?: string
- catalog: string
- warehouse: string
- namespace: string
- }) =>
- [
- projectRef,
- 'catalog',
- catalog,
- 'warehouse',
- warehouse,
- 'namespaces',
- namespace,
- 'tables',
- ] as const,
+ warehouse?: string
+ namespace?: string
+ }) => [projectRef, 'warehouse', warehouse, 'namespaces', namespace, 'tables'] as const,
}
diff --git a/apps/studio/data/subscriptions/org-subscription-query.ts b/apps/studio/data/subscriptions/org-subscription-query.ts
index dc78d08ce69..8b3fc04da3e 100644
--- a/apps/studio/data/subscriptions/org-subscription-query.ts
+++ b/apps/studio/data/subscriptions/org-subscription-query.ts
@@ -53,9 +53,5 @@ export const useOrgSubscriptionQuery = (
export const useHasAccessToProjectLevelPermissions = (slug: string) => {
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: slug })
- return (
- subscription?.plan.id === 'enterprise' ||
- subscription?.plan.id === 'team' ||
- subscription?.plan.id === 'platform'
- )
+ return subscription?.plan.id === 'enterprise' || subscription?.plan.id === 'team'
}
diff --git a/apps/studio/data/subscriptions/types.ts b/apps/studio/data/subscriptions/types.ts
index 3a0d3ab9517..4591d175e36 100644
--- a/apps/studio/data/subscriptions/types.ts
+++ b/apps/studio/data/subscriptions/types.ts
@@ -6,7 +6,6 @@ export type SubscriptionTier =
| 'tier_payg'
| 'tier_team'
| 'tier_enterprise'
- | 'tier_platform'
export type AddonVariantId = components['schemas']['UpdateAddonBody']['addon_variant']
diff --git a/apps/studio/data/usage/org-usage-query.test.ts b/apps/studio/data/usage/org-usage-query.test.ts
new file mode 100644
index 00000000000..e694790dffc
--- /dev/null
+++ b/apps/studio/data/usage/org-usage-query.test.ts
@@ -0,0 +1,125 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { getOrgUsage } from './org-usage-query'
+
+vi.mock('data/fetchers', () => ({
+ get: vi.fn(),
+ handleError: vi.fn((error) => {
+ throw error
+ }),
+}))
+
+describe('org-usage-query', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe('getOrgUsage', () => {
+ it('throws error when orgSlug is not provided', async () => {
+ await expect(getOrgUsage({ orgSlug: undefined })).rejects.toThrow('orgSlug is required')
+ })
+
+ it('calls API with correct parameters including project_ref', async () => {
+ const { get } = await import('data/fetchers')
+ const mockGet = get as unknown as ReturnType
+
+ const mockResponse = { usages: [] }
+ mockGet.mockResolvedValueOnce({ data: mockResponse, error: null })
+
+ const startDate = new Date('2025-01-01T00:00:00.000Z')
+ const endDate = new Date('2025-01-31T23:59:59.000Z')
+
+ const result = await getOrgUsage({
+ orgSlug: 'test-org',
+ projectRef: 'test-project-ref',
+ start: startDate,
+ end: endDate,
+ })
+
+ expect(mockGet).toHaveBeenCalledWith(
+ '/platform/organizations/{slug}/usage',
+ expect.objectContaining({
+ params: {
+ path: { slug: 'test-org' },
+ query: {
+ project_ref: 'test-project-ref',
+ start: '2025-01-01T00:00:00.000Z',
+ end: '2025-01-31T23:59:59.000Z',
+ },
+ },
+ })
+ )
+ expect(result).toEqual(mockResponse)
+ })
+
+ it('calls API without project_ref when projectRef is null', async () => {
+ const { get } = await import('data/fetchers')
+ const mockGet = get as unknown as ReturnType
+
+ const mockResponse = { usages: [] }
+ mockGet.mockResolvedValueOnce({ data: mockResponse, error: null })
+
+ await getOrgUsage({
+ orgSlug: 'test-org',
+ projectRef: null,
+ })
+
+ expect(mockGet).toHaveBeenCalledWith(
+ '/platform/organizations/{slug}/usage',
+ expect.objectContaining({
+ params: {
+ path: { slug: 'test-org' },
+ query: {
+ project_ref: undefined,
+ start: undefined,
+ end: undefined,
+ },
+ },
+ })
+ )
+ })
+
+ it('calls API without date params when start and end are undefined', async () => {
+ const { get } = await import('data/fetchers')
+ const mockGet = get as unknown as ReturnType
+
+ const mockResponse = { usages: [] }
+ mockGet.mockResolvedValueOnce({ data: mockResponse, error: null })
+
+ await getOrgUsage({
+ orgSlug: 'test-org',
+ })
+
+ expect(mockGet).toHaveBeenCalledWith(
+ '/platform/organizations/{slug}/usage',
+ expect.objectContaining({
+ params: {
+ path: { slug: 'test-org' },
+ query: {
+ project_ref: undefined,
+ start: undefined,
+ end: undefined,
+ },
+ },
+ })
+ )
+ })
+
+ it('handles API errors correctly', async () => {
+ const { get, handleError } = await import('data/fetchers')
+ const mockGet = get as unknown as ReturnType
+ const mockHandleError = handleError as unknown as ReturnType
+
+ const mockError = { message: 'API Error' }
+ mockGet.mockResolvedValueOnce({ data: null, error: mockError })
+ mockHandleError.mockImplementation((error) => {
+ throw new Error(error.message)
+ })
+
+ await expect(
+ getOrgUsage({
+ orgSlug: 'test-org',
+ })
+ ).rejects.toThrow('API Error')
+ })
+ })
+})
diff --git a/apps/studio/hooks/misc/useLegacyInlineEditorHotkeyMigration.ts b/apps/studio/hooks/misc/useLegacyInlineEditorHotkeyMigration.ts
deleted file mode 100644
index 01f359647eb..00000000000
--- a/apps/studio/hooks/misc/useLegacyInlineEditorHotkeyMigration.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { useEffect } from 'react'
-
-import { LOCAL_STORAGE_KEYS } from 'common'
-import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
-import { useLocalStorageQuery } from './useLocalStorage'
-
-const LEGACY_INLINE_EDITOR_HOTKEY_KEY = 'supabase-dashboard-hotkey-inline-editor'
-
-/**
- * Migrates the inline editor hotkey preference to the new sidebar editor panel key.
- * Runs when idle (or within 5 seconds) to avoid blocking render.
- */
-const useLegacyInlineEditorHotkeyMigration = () => {
- const [_inlineEditorEnabled, setInlineEditorEnabled] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL),
- true
- )
-
- useEffect(() => {
- if (typeof window === 'undefined') return
-
- const migrate = () => {
- try {
- const legacyValue = window.localStorage.getItem(LEGACY_INLINE_EDITOR_HOTKEY_KEY)
-
- if (legacyValue !== null) {
- setInlineEditorEnabled(legacyValue === 'true')
- window.localStorage.removeItem(LEGACY_INLINE_EDITOR_HOTKEY_KEY)
- }
- } catch (error) {
- console.warn('Failed to migrate inline editor hotkey preference', error)
- }
- }
-
- if (typeof window.requestIdleCallback === 'function') {
- const idleCallbackId = window.requestIdleCallback(migrate, { timeout: 5000 })
- return () => window.cancelIdleCallback?.(idleCallbackId)
- }
-
- const timeoutId = window.setTimeout(migrate, 5000)
- return () => window.clearTimeout(timeoutId)
- }, [setInlineEditorEnabled])
-}
-
-export const LegacyInlineEditorHotkeyMigration = () => {
- useLegacyInlineEditorHotkeyMigration()
- return null
-}
diff --git a/apps/studio/hooks/misc/useReportDateRange.ts b/apps/studio/hooks/misc/useReportDateRange.ts
index c993d6f6677..af3039f782d 100644
--- a/apps/studio/hooks/misc/useReportDateRange.ts
+++ b/apps/studio/hooks/misc/useReportDateRange.ts
@@ -16,7 +16,6 @@ export const DATERANGE_LIMITS: { [key: string]: number } = {
pro: 7,
team: 28,
enterprise: 90,
- platform: 1,
}
export interface ReportDateRange {
diff --git a/apps/studio/package.json b/apps/studio/package.json
index 649d785f98e..70a96386ac1 100644
--- a/apps/studio/package.json
+++ b/apps/studio/package.json
@@ -8,7 +8,7 @@
"build": "next build && if [ \"$SKIP_ASSET_UPLOAD\" != \"1\" ]; then ./../../scripts/upload-static-assets.sh; fi",
"start": "next start",
"lint": "eslint .",
- "lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rule react-hooks/exhaustive-deps --rule import/no-anonymous-default-export --rule @tanstack/query/exhaustive-deps --rule @tanstack/query/no-deprecated-options",
+ "lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rule react-hooks/exhaustive-deps --rule import/no-anonymous-default-export --rule @tanstack/query/exhaustive-deps --rule @tanstack/query/no-deprecated-options --rule @typescript-eslint/no-explicit-any",
"clean": "rimraf node_modules tsconfig.tsbuildinfo .next .turbo",
"test": "vitest --run --coverage",
"test:watch": "vitest watch",
diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx
index 4e1430b0d99..124028c19fd 100644
--- a/apps/studio/pages/_app.tsx
+++ b/apps/studio/pages/_app.tsx
@@ -52,7 +52,6 @@ import { GlobalErrorBoundaryState } from 'components/ui/ErrorBoundary/GlobalErro
import { useRootQueryClient } from 'data/query-client'
import { customFont, sourceCodePro } from 'fonts'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
-import { LegacyInlineEditorHotkeyMigration } from 'hooks/misc/useLegacyInlineEditorHotkeyMigration'
import { AuthProvider } from 'lib/auth'
import { API_URL, BASE_PATH, IS_PLATFORM, useDefaultProvider } from 'lib/constants'
import { ProfileProvider } from 'lib/profile'
@@ -164,8 +163,6 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
- {/* Temporary migration, to be removed by 2025-11-28 */}
-
{!isTestEnv && (
diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx
index 899461efa3e..cffab63b560 100644
--- a/apps/studio/pages/new/[slug].tsx
+++ b/apps/studio/pages/new/[slug].tsx
@@ -91,7 +91,7 @@ const Wizard: NextPageWithLayout = () => {
const isHomeNew = useFlag('homeNew')
const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod'
- const isNotOnHigherPlan = !['team', 'enterprise', 'platform'].includes(currentOrg?.plan.id ?? '')
+ const isNotOnTeamOrEnterprisePlan = !['team', 'enterprise'].includes(currentOrg?.plan.id ?? '')
// This is to make the database.new redirect work correctly. The database.new redirect should be set to supabase.com/dashboard/new/last-visited-org
if (slug === 'last-visited-org') {
@@ -162,10 +162,10 @@ const Wizard: NextPageWithLayout = () => {
const hasOAuthApps = approvedOAuthApps.length > 0
const { data: allOverdueInvoices = [] } = useOverdueInvoicesQuery({
- enabled: isNotOnHigherPlan,
+ enabled: isNotOnTeamOrEnterprisePlan,
})
const overdueInvoices = allOverdueInvoices.filter((x) => x.organization_id === currentOrg?.id)
- const hasOutstandingInvoices = isNotOnHigherPlan && overdueInvoices.length > 0
+ const hasOutstandingInvoices = isNotOnTeamOrEnterprisePlan && overdueInvoices.length > 0
const { data: orgProjectsFromApi } = useOrgProjectsInfiniteQuery({ slug: currentOrg?.slug })
const allOrgProjects = useMemo(
diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx
index dea6dd62212..9d287ed00d1 100644
--- a/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx
+++ b/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx
@@ -68,8 +68,8 @@ export default function SignUp() {
- You've successfully signed up. Please check your email to confirm your account
- before signing in.
+ You've successfully signed up. Please check your email to confirm your
+ account before signing in.
diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx
index 056ada53d79..9b5d77b1187 100644
--- a/apps/ui-library/registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx
+++ b/apps/ui-library/registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx
@@ -55,8 +55,8 @@ export function SignUpForm({ className, ...props }: React.ComponentPropsWithoutR
- You've successfully signed up. Please check your email to confirm your account before
- signing in.
+ You've successfully signed up. Please check your email to confirm your account
+ before signing in.
diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx
index 5a59b834113..8765ff067b9 100644
--- a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx
+++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx
@@ -23,7 +23,7 @@ function SignUpSuccess() {
- You've successfully signed up. Please check your email to confirm your account
+ You've successfully signed up. Please check your email to confirm your account
before signing in.
diff --git a/apps/ui-library/registry/default/platform/platform-kit-nextjs/app/api/supabase-proxy/[...path]/route.ts b/apps/ui-library/registry/default/platform/platform-kit-nextjs/app/api/supabase-proxy/[...path]/route.ts
index 9c8de65bc50..4727e7231e9 100644
--- a/apps/ui-library/registry/default/platform/platform-kit-nextjs/app/api/supabase-proxy/[...path]/route.ts
+++ b/apps/ui-library/registry/default/platform/platform-kit-nextjs/app/api/supabase-proxy/[...path]/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server'
async function forwardToSupabaseAPI(request: Request, method: string, params: { path: string[] }) {
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
if (!process.env.SUPABASE_MANAGEMENT_API_TOKEN) {
console.error('Supabase Management API token is not configured.')
return NextResponse.json({ message: 'Server configuration error.' }, { status: 500 })
@@ -30,6 +31,7 @@ async function forwardToSupabaseAPI(request: Request, method: string, params: {
try {
const forwardHeaders: HeadersInit = {
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
Authorization: `Bearer ${process.env.SUPABASE_MANAGEMENT_API_TOKEN}`,
}
diff --git a/apps/ui-library/registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions.tsx b/apps/ui-library/registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions.tsx
index 070c6069804..a6b5135fbea 100644
--- a/apps/ui-library/registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions.tsx
+++ b/apps/ui-library/registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions.tsx
@@ -37,7 +37,7 @@ export function SuggestionsManager({ projectRef }: { projectRef: string }) {
Suggestions
- Improve your project's security and performance.
+ Improve your project's security and performance.
{isLoading && (
diff --git a/apps/www/_blog/2025-12-01-vector-buckets.mdx b/apps/www/_blog/2025-12-01-vector-buckets.mdx
index fee362e2b8b..8140126b186 100644
--- a/apps/www/_blog/2025-12-01-vector-buckets.mdx
+++ b/apps/www/_blog/2025-12-01-vector-buckets.mdx
@@ -6,6 +6,7 @@ image: 2025-12-01-vector-buckets/og.png?v=3
thumb: 2025-12-01-vector-buckets/thumb.png?v=3
categories:
- product
+ - launch-week
date: '2025-12-01'
toc_depth: 2
---
diff --git a/apps/www/_blog/2025-12-02-introducing-analytics-buckets.mdx b/apps/www/_blog/2025-12-02-introducing-analytics-buckets.mdx
new file mode 100644
index 00000000000..04eef79ac36
--- /dev/null
+++ b/apps/www/_blog/2025-12-02-introducing-analytics-buckets.mdx
@@ -0,0 +1,210 @@
+---
+title: 'Introducing Analytics Buckets'
+description: 'Use Analytics Buckets to store huge datasets in Supabase Storage with Apache Iceberg and columnar Parquet format, optimized for analytical workloads.'
+author: fabrizio
+image: 2025-12-02-introducing-analytics-buckets/og.png
+thumb: 2025-12-02-introducing-analytics-buckets/thumb.png
+categories:
+ - product
+ - launch-week
+date: '2025-12-02:10:00'
+toc_depth: 2
+---
+
+Today Supabase is introducing Analytics Buckets, which you can use to store huge sets of data in Supabase Storage. Postgres is great for your app. But Postgres isn't designed for analytical workloads.
+
+Analytics Buckets are a specialized storage type in Supabase designed for analytical workloads and built on [Apache Iceberg](https://iceberg.apache.org) and Amazon S3. They store data in columnar Parquet format, which is optimized for scans, aggregations, and time-series queries.
+
+Think of them as cold storage for your data, with a query engine attached.
+
+Your hot transactional data stays in Postgres. Your historical data and analytical workloads live in Analytics Buckets. You query both using familiar tools.
+
+## What do they do?
+
+Analytics Buckets give you:
+
+- **Cost-effective storage.** S3 pricing instead of database storage. Documented savings of 30-90% on storage costs for large datasets.
+
+- **Open table format.** Apache Iceberg means no vendor lock-in. Query your data from any compatible tool.
+
+- **Schema evolution.** Change your table schema without rewriting data.
+
+- **Time travel.** Query historical snapshots of your data. See what a table looked like at any point in time.
+
+- **Full audit history.** Every change is preserved. Track what changed, when, and how.
+
+## When to use Analytics Buckets vs Postgres
+
+Analytics Buckets and Postgres are complementary. They serve different workloads.
+
+### Keep data in Postgres when:
+
+- You need low-latency reads for your application
+
+- Data changes frequently and consistency matters
+
+- Your dataset is small to medium size
+
+- You need real-time access from your app
+
+### Use Analytics Buckets when:
+
+- You are storing millions or billions of rows
+
+- You run heavy analytical queries that scan large tables
+
+- You need long-term retention at low cost
+
+- You want to query data from multiple tools
+
+- You need complete audit history and time travel
+
+Many teams use both. Keep the last 90 days in Postgres. Archive everything to Analytics Buckets. Query historical data when needed.
+
+## How they work
+
+Analytics Buckets use [Apache Iceberg](https://iceberg.apache.org), an open table format created for large analytical datasets. Here is what happens under the hood:
+
+1. Data is stored in Parquet files on S3
+
+2. Iceberg manages metadata including schema, partitions, and snapshots
+
+3. An Iceberg REST Catalog provides the interface for querying
+
+4. You connect using any Iceberg-compatible tool
+
+The separation of compute and storage means you can scale each independently. Store petabytes of data and query only what you need.
+
+## Creating an Analytics Bucket
+
+You can create an Analytics Bucket from the Dashboard or using the SDK.
+
+### Using the Dashboard
+
+1. Navigate to Storage in your Supabase Dashboard
+
+2. Click Create Bucket
+
+3. Enter a name for your bucket
+
+4. Select Analytics Bucket as the bucket type
+
+5. Click Create
+
+You can use the Supabase Dashboard to define columns and set data types, including complex types like decimal with precision and scale. The foreign data wrapper schema will automatically be configured for you. Once your table is created, you can manage Analytics Buckets in the same way that you manage your Postgres tables.
+
+### Using the SDK
+
+```jsx
+import { createClient } from '@supabase/supabase-js'
+
+const supabase = createClient('https://your-project.supabase.co', 'your-service-key')
+
+await supabase.storage.createBucket('my-analytics-bucket', {
+ type: 'ANALYTICS',
+})
+```
+
+## Connecting to Analytics Buckets
+
+Analytics Buckets require authentication with two services:
+
+**Iceberg REST Catalog** manages metadata for your tables. It handles schema, partitions, and snapshots.
+
+**S3-Compatible Storage** stores the actual data in Parquet format.
+
+You authenticate using your Supabase service key for the catalog and S3 credentials for storage.
+
+## Streaming data with Supabase ETL
+
+Analytics Buckets work hand in hand with [Supabase ETL](/blog/introducing-supabase-etl). ETL captures changes from your Postgres database and streams them to Analytics Buckets in near real time.
+
+This gives you:
+
+- Automatic replication of your Postgres tables
+
+- Near real-time data in your analytics bucket
+
+- Complete changelog with every insert, update, and delete
+
+- No manual data movement or scheduled jobs
+
+To set up replication, create a Postgres publication for the tables you want to replicate, then add an Analytics Buckets destination in the Replication section of the Dashboard.
+
+## Querying from Postgres
+
+You can query Analytics Buckets directly from Postgres using Foreign Data Wrappers. This lets you join hot data in Postgres with historical data in Analytics Buckets.
+
+```sql
+-- Create a foreign server for your Iceberg data
+create server iceberg_server
+foreign data wrapper iceberg_wrapper
+options (
+ aws_access_key_id 'your-access-key',
+ aws_secret_access_key 'your-secret-key',
+ region_name 'us-east-1'
+);
+
+-- Import tables from your analytics bucket
+import foreign schema "analytics"
+from server iceberg_server
+into iceberg;
+
+-- Query historical data
+select * from iceberg.events
+where event_timestamp > '2024-01-01';
+```
+
+## Data tiering pattern
+
+A common architecture is data tiering: keeping recent data in Postgres and archiving history to Analytics Buckets.
+
+1. Partition tables by time in Postgres, keeping a rolling window like the last 90 days
+
+2. Stream all data to Analytics Buckets using Supabase ETL
+
+3. Drop old partitions from Postgres
+
+4. Query recent data from Postgres, historical data from Analytics Buckets
+
+This keeps your Postgres database small and fast. Storage costs drop. Analytics queries run on data optimized for scans.
+
+## Compatible tools
+
+Analytics Buckets work with any tool that supports the Iceberg REST Catalog API:
+
+- PyIceberg
+
+- Apache Spark
+
+- DuckDB
+
+- Amazon Athena
+
+- Trino
+
+- Apache Flink
+
+- Snowflake (via external tables)
+
+- BigQuery (via BigLake)
+
+## Pricing and availability
+
+Analytics Buckets are free during the Private Alpha. Standard egress charges apply when you move data out of the region.
+
+To request access, fill out the form at [forms.supabase.com/analytics-buckets](http://forms.supabase.com/analytics-buckets).
+
+## Get started
+
+1. Request access to the Private Alpha
+
+2. Create an Analytics Bucket in the Dashboard
+
+3. Connect using PyIceberg, Spark, or your tool of choice
+
+4. Set up ETL to stream data from Postgres automatically
+
+Separate your transactional and analytical workloads. Keep Postgres fast. Store history at S3 prices. Query from any tool.
+
+We are excited to see what you build.
diff --git a/apps/www/_blog/2025-12-02-introducing-supabase-etl.mdx b/apps/www/_blog/2025-12-02-introducing-supabase-etl.mdx
new file mode 100644
index 00000000000..dec76d78f64
--- /dev/null
+++ b/apps/www/_blog/2025-12-02-introducing-supabase-etl.mdx
@@ -0,0 +1,165 @@
+---
+title: 'Introducing Supabase ETL'
+description: 'A change-data-capture pipeline that replicates your Postgres tables to analytical destinations like Analytics Buckets and BigQuery in near real time.'
+author: riccardo_busetti
+image: 2025-12-02-introducing-supabase-etl/og.png
+thumb: 2025-12-02-introducing-supabase-etl/thumb.png
+categories:
+ - product
+ - launch-week
+date: '2025-12-02:11:00'
+toc_depth: 2
+---
+
+Today we're introducing **Supabase ETL**: a change-data-capture pipeline that replicates your Postgres tables to analytical destinations in near real time.
+
+Supabase ETL reads changes from your Postgres database and writes them to external destinations. It uses logical replication to capture inserts, updates, deletes and truncates as they happen. **Setup takes minutes in the Supabase Dashboard.**
+
+The first supported destinations are [Analytics Buckets](/blog/introducing-analytics-buckets) (powered by Iceberg) and BigQuery.
+
+Supabase ETL is open source. You can find the code on GitHub at [github.com/supabase/etl](http://github.com/supabase/etl).
+
+## Why separate OLTP and OLAP?
+
+Postgres is excellent for transactional workloads like reading a single user record or inserting an order. But when you need to scan millions of rows for analytics, Postgres slows down.
+
+Column-oriented systems like BigQuery, or those built on open formats like Apache Iceberg, are designed for this. They can aggregate massive datasets **orders of magnitude faster**, compress data more efficiently, and handle complex analytical queries that would choke a transactional database.
+
+**Supabase ETL gives you the best of both worlds**: keep your app fast on Postgres while unlocking powerful analytics on purpose-built systems.
+
+## How it works
+
+Supabase ETL captures every change in your Postgres database and delivers it to your analytics destination in near real time.
+
+Here's how:
+
+1. You create a Postgres publication that defines which tables to replicate
+
+2. You configure ETL to connect a publication to a destination
+
+3. ETL reads changes from the publication through a logical replication slot
+
+4. Changes are batched and written to your destination
+
+5. Your data is available for querying in the destination
+
+The pipeline starts with an initial copy of your selected tables, then switches to streaming mode. **Your analytics stay fresh with latency measured in milliseconds to seconds.**
+
+## Setting up ETL
+
+You configure ETL entirely through the Supabase Dashboard. **No code required.**
+
+### Step 1: Create a publication
+
+A publication defines which tables to replicate. You create it with SQL or via the UI:
+
+```sql
+-- Replicate specific tables
+create publication analytics_pub
+for table events, orders, users;
+
+-- Or replicate all tables in a schema
+create publication analytics_pub
+for tables in schema public;
+```
+
+### Step 2: Enable replication
+
+Navigate to `Database` in your Supabase Dashboard. Select the `Replication` tab and click `Enable Replication`.
+
+### Step 3: Add a destination
+
+Click `Add Destination` and choose your destination type.
+
+**Note:** For Analytics Buckets, you will need to create an analytics bucket first in the Storage section.
+
+Configure the destination with your bucket credentials and select your publication. Click Create and `Start` to begin replication.
+
+### Step 4: Monitor your pipeline
+
+The Dashboard shows pipeline status and lag. You can start, stop, restart, or delete pipelines from the actions menu.
+
+## Available destinations
+
+Our goal with Supabase ETL is to let you connect your existing data systems to Supabase. We're actively expanding the list of supported destinations. Right now, the official destinations are Analytics Buckets and BigQuery.
+
+### Analytics Buckets
+
+[Analytics Buckets](https://supabase.com/docs/guides/storage/analytics/introduction) are specialized storage buckets built on Apache Iceberg, an open table format designed for large analytical datasets. Your data is stored in Parquet files on S3.
+
+When you replicate to Analytics Buckets, your tables are created with a changelog structure. Each row includes a `cdc_operation` column indicating whether the change was an `INSERT`, `UPDATE`, or `DELETE`. **This append-only format preserves the complete history of all changes.**
+
+You can query Analytics Buckets from PyIceberg, Apache Spark, DuckDB, Amazon Athena, or any tool that supports the Iceberg REST Catalog API.
+
+### BigQuery
+
+BigQuery is Google's serverless data warehouse, built for large-scale analytics. It handles petabytes of data and integrates well with existing BI tools and data pipelines.
+
+When you replicate to BigQuery, Supabase ETL creates a view for each table and uses an underlying versioned table to support all operations efficiently. You query the view, and ETL handles the rest.
+
+## Adding and removing tables
+
+You can modify which tables are replicated after your pipeline is running.
+
+To add a table:
+
+```sql
+alter publication analytics_pub add table products;
+```
+
+To remove a table:
+
+```sql
+alter publication analytics_pub drop table orders;
+```
+
+After changing your publication, restart the pipeline from the Dashboard actions menu for the changes to take effect.
+
+**Note:** ETL does not remove data from your destination when you remove a table from a publication. This is by design to prevent accidental data loss.
+
+## When to use ETL vs read replicas
+
+Read replicas and ETL solve different problems.
+
+Read replicas help when you need to scale concurrent queries, but they're still Postgres. They don't make analytics faster.
+
+ETL moves your data to systems built for analytics. You get **faster queries on large datasets**, **lower storage costs** through compression, and **complete separation** between your production workload and analytics.
+
+You can use both: read replicas for application read scaling, ETL for analytics.
+
+## Things to know
+
+Replication with Supabase ETL has a few constraints to be aware of:
+
+- Tables must have primary keys (this is a Postgres logical replication requirement)
+- Generated columns are not supported
+- Custom data types are replicated as strings
+- Schema changes are not automatically propagated to destinations
+- Data is replicated as-is, without transformation
+- During the initial copy phase, changes accumulate in the WAL and are replayed once streaming begins
+
+We're working on schema change support and additional destinations, and evaluating different streaming techniques to improve flexibility and performance.
+
+## Pricing
+
+Supabase ETL is usage-based:
+
+- **$25 per connector per month**
+- **$15 per GB** of change data processed after the initial sync
+- **Initial copy is free**
+
+## Get started
+
+**Supabase ETL is in private alpha.** To request access, contact your account manager or fill out the form in the Dashboard.
+
+If you want to dive into the code, **the ETL framework is open source** and written in Rust. Check out the repository at [github.com/supabase/etl](http://github.com/supabase/etl).
+
+
+
+
diff --git a/apps/www/components/Blog/BlogListItem.tsx b/apps/www/components/Blog/BlogListItem.tsx
index a852693b37c..165e9519bc2 100644
--- a/apps/www/components/Blog/BlogListItem.tsx
+++ b/apps/www/components/Blog/BlogListItem.tsx
@@ -26,15 +26,16 @@ const getAuthors = (post: PostTypes | CMSPostTypes) => {
return authors
}
- const authorArray = post.author?.split(',') || []
+ const authorArray = post.author?.split(',').map((a) => a.trim()) || []
const authors = []
for (let i = 0; i < authorArray.length; i++) {
- authors.push(
- blogAuthors.find((authors: any) => {
- return authors.author_id === authorArray[i]
- })
- )
+ const foundAuthor = blogAuthors.find((authors: any) => {
+ return authors.author_id === authorArray[i]
+ })
+ if (foundAuthor) {
+ authors.push(foundAuthor)
+ }
}
return authors
}
@@ -55,6 +56,7 @@ const BlogListItem = ({ post }: Props) => {
{authors.map((author: any, i: number) => {
+ if (!author) return null
return (
{author.author_image_url && (
@@ -74,7 +76,7 @@ const BlogListItem = ({ post }: Props) => {
{post.categories.map(
(category, i) =>
i === 0 && (
-
+
{sanitizeCategory(category)}
)
diff --git a/apps/www/components/Blog/FeaturedThumb.tsx b/apps/www/components/Blog/FeaturedThumb.tsx
index 641b33d3659..5172ddf3c86 100644
--- a/apps/www/components/Blog/FeaturedThumb.tsx
+++ b/apps/www/components/Blog/FeaturedThumb.tsx
@@ -35,15 +35,16 @@ function FeaturedThumb(blog: PostTypes | CMSPostTypes) {
}
// For static posts, look up author info from authors.json
- const authorArray = blog.author?.split(',') || []
+ const authorArray = blog.author?.split(',').map((a) => a.trim()) || []
const author = []
for (let i = 0; i < authorArray.length; i++) {
- author.push(
- authors.find((authors: any) => {
- return authors.author_id === authorArray[i]
- })
- )
+ const foundAuthor = authors.find((authors: any) => {
+ return authors.author_id === authorArray[i]
+ })
+ if (foundAuthor) {
+ author.push(foundAuthor)
+ }
}
return renderFeaturedThumb(blog, author)
diff --git a/apps/www/components/Events/EventListItem.tsx b/apps/www/components/Events/EventListItem.tsx
index 91863d6a176..84765051721 100644
--- a/apps/www/components/Events/EventListItem.tsx
+++ b/apps/www/components/Events/EventListItem.tsx
@@ -47,7 +47,7 @@ const EventListItem = ({ event }: Props) => {
{event.categories.map(
(category, i) =>
i === 0 && (
-
+
{category}
)
diff --git a/apps/www/lib/authors.json b/apps/www/lib/authors.json
index a45255368af..d8b417c1547 100644
--- a/apps/www/lib/authors.json
+++ b/apps/www/lib/authors.json
@@ -739,5 +739,13 @@
"company": "No Code MBA",
"author_url": "https://www.linkedin.com/in/seth-kramer-62806b63/",
"author_image_url": "/images/avatars/seth-kramer.jpeg"
+ },
+ {
+ "author_id": "riccardo_busetti",
+ "author": "Riccardo Busetti",
+ "username": "iambriccardo",
+ "position": "Engineering",
+ "author_url": "https://github.com/iambriccardo",
+ "author_image_url": "https://github.com/iambriccardo.png"
}
]
diff --git a/apps/www/public/images/blog/2025-12-02-introducing-analytics-buckets/og.png b/apps/www/public/images/blog/2025-12-02-introducing-analytics-buckets/og.png
new file mode 100644
index 00000000000..0d6c64131be
Binary files /dev/null and b/apps/www/public/images/blog/2025-12-02-introducing-analytics-buckets/og.png differ
diff --git a/apps/www/public/images/blog/2025-12-02-introducing-analytics-buckets/thumb.png b/apps/www/public/images/blog/2025-12-02-introducing-analytics-buckets/thumb.png
new file mode 100644
index 00000000000..19c7148f2c4
Binary files /dev/null and b/apps/www/public/images/blog/2025-12-02-introducing-analytics-buckets/thumb.png differ
diff --git a/apps/www/public/images/blog/2025-12-02-introducing-supabase-etl/og.png b/apps/www/public/images/blog/2025-12-02-introducing-supabase-etl/og.png
new file mode 100644
index 00000000000..6753f113f75
Binary files /dev/null and b/apps/www/public/images/blog/2025-12-02-introducing-supabase-etl/og.png differ
diff --git a/apps/www/public/images/blog/2025-12-02-introducing-supabase-etl/thumb.png b/apps/www/public/images/blog/2025-12-02-introducing-supabase-etl/thumb.png
new file mode 100644
index 00000000000..8a2b5966616
Binary files /dev/null and b/apps/www/public/images/blog/2025-12-02-introducing-supabase-etl/thumb.png differ
diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml
index 316b760b6f5..fe20b729a5b 100644
--- a/apps/www/public/rss.xml
+++ b/apps/www/public/rss.xml
@@ -4,9 +4,23 @@
https://supabase.com
Latest news from Supabase
en
- Mon, 01 Dec 2025 00:00:00 -0700
+ Tue, 02 Dec 2025 00:00:00 -0700
-
+
https://supabase.com/blog/introducing-analytics-buckets
+ Introducing Analytics Buckets
+ https://supabase.com/blog/introducing-analytics-buckets
+ Use Analytics Buckets to store huge datasets in Supabase Storage with Apache Iceberg and columnar Parquet format, optimized for analytical workloads.
+ Tue, 02 Dec 2025 00:00:00 -0700
+
+-
+
https://supabase.com/blog/introducing-supabase-etl
+ Introducing Supabase ETL
+ https://supabase.com/blog/introducing-supabase-etl
+ A change-data-capture pipeline that replicates your Postgres tables to analytical destinations like Analytics Buckets and BigQuery in near real time.
+ Tue, 02 Dec 2025 00:00:00 -0700
+
+-
https://supabase.com/blog/vector-buckets
Introducing Vector Buckets
https://supabase.com/blog/vector-buckets
diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts
index 538c1d80f78..70c3a5a3c00 100644
--- a/packages/api-types/types/api.d.ts
+++ b/packages/api-types/types/api.d.ts
@@ -3113,9 +3113,9 @@ export interface components {
name: string
}[]
/** @enum {string} */
- source_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise' | 'platform'
+ source_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise'
/** @enum {string|null} */
- target_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise' | 'platform' | null
+ target_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise' | null
valid: boolean
warnings: {
key: string
@@ -3518,7 +3518,7 @@ export interface components {
upstreamTarget: 'main' | 'canary'
}
features: {
- icebergCatalog?: {
+ icebergCatalog: {
enabled: boolean
maxCatalogs: number
maxNamespaces: number
@@ -3530,7 +3530,7 @@ export interface components {
s3Protocol: {
enabled: boolean
}
- vectorBuckets?: {
+ vectorBuckets: {
enabled: boolean
maxBuckets: number
maxIndexes: number
@@ -4044,10 +4044,10 @@ export interface components {
maxNamespaces: number
maxTables: number
}
- imageTransformation: {
+ imageTransformation?: {
enabled: boolean
}
- s3Protocol: {
+ s3Protocol?: {
enabled: boolean
}
vectorBuckets?: {
@@ -4352,7 +4352,7 @@ export interface components {
| 'AI_LOG_GENERATOR_OPT_IN'
)[]
/** @enum {string} */
- plan?: 'free' | 'pro' | 'team' | 'enterprise' | 'platform'
+ plan?: 'free' | 'pro' | 'team' | 'enterprise'
}
V1PatchMigrationBody: {
name?: string
diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts
index 9fae3cc87be..059a6f99aec 100644
--- a/packages/api-types/types/platform.d.ts
+++ b/packages/api-types/types/platform.d.ts
@@ -1171,6 +1171,23 @@ export interface paths {
patch?: never
trace?: never
}
+ '/platform/organizations/{slug}/billing/upgrade-request': {
+ parameters: {
+ query?: never
+ header?: never
+ path?: never
+ cookie?: never
+ }
+ get?: never
+ put?: never
+ /** Request organization upgrade - notifies billing owners */
+ post: operations['UpgradeRequestController_requestUpgrade']
+ delete?: never
+ options?: never
+ head?: never
+ patch?: never
+ trace?: never
+ }
'/platform/organizations/{slug}/cloud-marketplace/link': {
parameters: {
query?: never
@@ -4124,23 +4141,6 @@ export interface paths {
patch?: never
trace?: never
}
- '/platform/storage/{ref}/buckets/{id}/objects/download': {
- parameters: {
- query?: never
- header?: never
- path?: never
- cookie?: never
- }
- get?: never
- put?: never
- /** Downloads a file from a private bucket */
- post: operations['StorageObjectsController_download']
- delete?: never
- options?: never
- head?: never
- patch?: never
- trace?: never
- }
'/platform/storage/{ref}/buckets/{id}/objects/list': {
parameters: {
query?: never
@@ -5186,13 +5186,7 @@ export interface components {
value: string
}
/** @enum {string} */
- tier:
- | 'tier_free'
- | 'tier_pro'
- | 'tier_payg'
- | 'tier_team'
- | 'tier_enterprise'
- | 'tier_platform'
+ tier: 'tier_free' | 'tier_pro' | 'tier_payg' | 'tier_team' | 'tier_enterprise'
}
CreateOrganizationResponse:
| {
@@ -5210,7 +5204,7 @@ export interface components {
organization_requires_mfa: boolean
plan: {
/** @enum {string} */
- id: 'free' | 'pro' | 'team' | 'enterprise' | 'platform'
+ id: 'free' | 'pro' | 'team' | 'enterprise'
name: string
}
restriction_data: {
@@ -6059,22 +6053,6 @@ export interface components {
DownloadBackupResponse: {
fileUrl: string
}
- DownloadObjectBody: {
- options?: {
- download?: boolean
- downloadName?: string
- transform?: {
- /** @enum {string} */
- format?: 'origin'
- height?: number
- quality?: number
- /** @enum {string} */
- resize?: 'cover' | 'contain' | 'fill'
- width?: number
- }
- }
- path: string
- }
DynamicRegisterOAuthAppBody: {
client_name: string
/** Format: uri */
@@ -6532,7 +6510,7 @@ export interface components {
payment_method_type: string
plan: {
/** @enum {string} */
- id: 'free' | 'pro' | 'team' | 'enterprise' | 'platform'
+ id: 'free' | 'pro' | 'team' | 'enterprise'
name: string
}
project_addons: {
@@ -6593,7 +6571,7 @@ export interface components {
/** Format: date-time */
at: string
/** @enum {string} */
- target_plan: 'free' | 'pro' | 'team' | 'enterprise' | 'platform'
+ target_plan: 'free' | 'pro' | 'team' | 'enterprise'
usage_billing_enabled: boolean
} | null
usage_billing_enabled: boolean
@@ -7493,7 +7471,7 @@ export interface components {
organization_requires_mfa: boolean
plan: {
/** @enum {string} */
- id: 'free' | 'pro' | 'team' | 'enterprise' | 'platform'
+ id: 'free' | 'pro' | 'team' | 'enterprise'
name: string
}
restriction_data: {
@@ -7554,7 +7532,7 @@ export interface components {
opt_in_tags: string[]
plan: {
/** @enum {string} */
- id: 'free' | 'pro' | 'team' | 'enterprise' | 'platform'
+ id: 'free' | 'pro' | 'team' | 'enterprise'
name: string
}
projects: {
@@ -7663,10 +7641,6 @@ export interface components {
| 'PITR_28'
| 'IPV4'
| 'LOG_DRAIN'
- | 'LOG_INGESTION'
- | 'LOG_QUERYING'
- | 'LOG_STORAGE'
- | 'ACTIVE_COMPUTE_HOURS'
usage: number
usage_original: number
}[]
@@ -7727,10 +7701,6 @@ export interface components {
| 'PITR_28'
| 'IPV4'
| 'LOG_DRAIN'
- | 'LOG_INGESTION'
- | 'LOG_QUERYING'
- | 'LOG_STORAGE'
- | 'ACTIVE_COMPUTE_HOURS'
pricing_free_units?: number
pricing_package_price?: number
pricing_package_size?: number
@@ -7809,7 +7779,7 @@ export interface components {
/** @enum {string} */
effective_at?: 'now' | 'end_of_billing_period' | 'none'
/** @enum {string} */
- id: 'free' | 'pro' | 'team' | 'enterprise' | 'platform'
+ id: 'free' | 'pro' | 'team' | 'enterprise'
is_current: boolean
name: string
price: number
@@ -8075,9 +8045,9 @@ export interface components {
name: string
}[]
/** @enum {string} */
- source_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise' | 'platform'
+ source_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise'
/** @enum {string|null} */
- target_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise' | 'platform' | null
+ target_subscription_plan: 'free' | 'pro' | 'team' | 'enterprise' | null
valid: boolean
warnings: {
key: string
@@ -8999,6 +8969,14 @@ export interface components {
schema: string
}[]
}
+ RequestUpgradeBody: {
+ note?: string
+ /** @enum {string} */
+ requested_plan: 'Pro' | 'Team' | 'Enterprise'
+ }
+ RequestUpgradeResponse: {
+ message: string
+ }
ResetPasswordBody: {
/** Format: email */
email: string
@@ -9290,7 +9268,7 @@ export interface components {
upstreamTarget: 'main' | 'canary'
}
features: {
- icebergCatalog?: {
+ icebergCatalog: {
enabled: boolean
maxCatalogs: number
maxNamespaces: number
@@ -9302,7 +9280,7 @@ export interface components {
s3Protocol: {
enabled: boolean
}
- vectorBuckets?: {
+ vectorBuckets: {
enabled: boolean
maxBuckets: number
maxIndexes: number
@@ -9388,7 +9366,6 @@ export interface components {
vectorBucketName: string
}[]
}
- StreamableFile: Record
SupavisorConfigResponse: {
connection_string: string
/** @description Use connection_string instead */
@@ -9639,10 +9616,6 @@ export interface components {
| 'PITR_28'
| 'IPV4'
| 'LOG_DRAIN'
- | 'LOG_INGESTION'
- | 'LOG_QUERYING'
- | 'LOG_STORAGE'
- | 'ACTIVE_COMPUTE_HOURS'
usage_original?: number
}[]
subscription_id: string
@@ -10536,10 +10509,10 @@ export interface components {
maxNamespaces: number
maxTables: number
}
- imageTransformation: {
+ imageTransformation?: {
enabled: boolean
}
- s3Protocol: {
+ s3Protocol?: {
enabled: boolean
}
vectorBuckets?: {
@@ -10568,13 +10541,7 @@ export interface components {
value: string
}
/** @enum {string} */
- tier:
- | 'tier_free'
- | 'tier_pro'
- | 'tier_payg'
- | 'tier_team'
- | 'tier_enterprise'
- | 'tier_platform'
+ tier: 'tier_free' | 'tier_pro' | 'tier_payg' | 'tier_team' | 'tier_enterprise'
}
UpdateSubscriptionResponse: {
pending_payment_intent_secret: string | null
@@ -14094,6 +14061,61 @@ export interface operations {
}
}
}
+ UpgradeRequestController_requestUpgrade: {
+ parameters: {
+ query?: never
+ header?: never
+ path: {
+ /** @description Organization slug */
+ slug: string
+ }
+ cookie?: never
+ }
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['RequestUpgradeBody']
+ }
+ }
+ responses: {
+ /** @description Upgrade request has been sent to billing owners */
+ 201: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'application/json': components['schemas']['RequestUpgradeResponse']
+ }
+ }
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Failed to send upgrade request */
+ 500: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ }
+ }
ClazarController_link: {
parameters: {
query?: never
@@ -25492,62 +25514,6 @@ export interface operations {
}
}
}
- StorageObjectsController_download: {
- parameters: {
- query?: never
- header?: never
- path: {
- /** @description Storage bucket id */
- id: string
- /** @description Project ref */
- ref: string
- }
- cookie?: never
- }
- requestBody: {
- content: {
- 'application/json': components['schemas']['DownloadObjectBody']
- }
- }
- responses: {
- 201: {
- headers: {
- [name: string]: unknown
- }
- content: {
- 'application/json': components['schemas']['StreamableFile']
- }
- }
- /** @description Unauthorized */
- 401: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- /** @description Forbidden action */
- 403: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- /** @description Rate limit exceeded */
- 429: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- /** @description Failed to download the file */
- 500: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- }
- }
StorageObjectsController_getObjects: {
parameters: {
query?: never
diff --git a/packages/common/configcat.ts b/packages/common/configcat.ts
index c70d83a695c..0316badb37f 100644
--- a/packages/common/configcat.ts
+++ b/packages/common/configcat.ts
@@ -66,16 +66,20 @@ async function getClient() {
export async function getFlags(userEmail: string = '', customAttributes?: Record) {
const client = await getClient()
+ const _customAttributes = {
+ ...customAttributes,
+ is_staff: !!userEmail ? userEmail.includes('@supabase.').toString() : 'false',
+ }
if (!client) {
return []
} else if (userEmail) {
return client.getAllValuesAsync(
- new configcat.User(userEmail, undefined, undefined, customAttributes)
+ new configcat.User(userEmail, undefined, undefined, _customAttributes)
)
} else {
return client.getAllValuesAsync(
- new configcat.User('anonymous', undefined, undefined, customAttributes)
+ new configcat.User('anonymous', undefined, undefined, _customAttributes)
)
}
}
diff --git a/packages/eslint-config-supabase/next.js b/packages/eslint-config-supabase/next.js
index 667ffba631d..b59b28777b2 100644
--- a/packages/eslint-config-supabase/next.js
+++ b/packages/eslint-config-supabase/next.js
@@ -5,6 +5,8 @@ const prettierConfig = require('eslint-config-prettier/flat')
const { default: turboConfig } = require('eslint-config-turbo/flat')
const { fixupPluginRules } = require('@eslint/compat')
const tanstackQuery = require('@tanstack/eslint-plugin-query')
+const tseslint = require('@typescript-eslint/eslint-plugin')
+const tsparser = require('@typescript-eslint/parser')
const compat = new FlatCompat({
baseDirectory: __dirname,
@@ -25,12 +27,32 @@ const tanstackQueryConfig = {
},
}
+// TypeScript ESLint config for TypeScript files
+const typescriptConfig = {
+ name: 'typescript',
+ files: ['**/*.ts', '**/*.tsx'],
+ languageOptions: {
+ parser: tsparser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
+ },
+ plugins: {
+ '@typescript-eslint': tseslint,
+ },
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'warn',
+ },
+}
+
module.exports = defineConfig([
// Global ignore for the .next folder
{ ignores: ['.next', 'public'] },
turboConfig,
prettierConfig,
tanstackQueryConfig,
+ typescriptConfig,
{
extends: compat.extends('next/core-web-vitals'),
linterOptions: {
diff --git a/packages/ui/src/components/shadcn/ui/table.tsx b/packages/ui/src/components/shadcn/ui/table.tsx
index fefe8967216..d56edc29c5a 100644
--- a/packages/ui/src/components/shadcn/ui/table.tsx
+++ b/packages/ui/src/components/shadcn/ui/table.tsx
@@ -1,5 +1,6 @@
import type { ComponentProps } from 'react'
import * as React from 'react'
+import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react'
import { cn } from '../../../lib/utils/cn'
import { ShadowScrollArea } from '../../ShadowScrollArea'
@@ -12,7 +13,11 @@ const Table = React.forwardRef(
({ className, containerProps, ...props }, ref) => {
return (
-
+
)
}
@@ -76,6 +81,72 @@ const TableHead = React.forwardRef<
))
TableHead.displayName = 'TableHead'
+interface TableHeadSortProps {
+ column: TColumn
+ currentSort: string
+ onSortChange: (column: TColumn) => void
+ children: React.ReactNode
+ className?: string
+}
+
+function TableHeadSort({
+ column,
+ currentSort,
+ onSortChange,
+ children,
+ className,
+}: TableHeadSortProps) {
+ const [currentCol, currentOrder] = currentSort.split(':')
+ const isActive = currentCol === column
+ const isAsc = isActive && currentOrder === 'asc'
+ const isDesc = isActive && currentOrder === 'desc'
+
+ const getSortIcon = () => {
+ const baseIconClass = 'w-3 h-3 absolute inset-0'
+
+ return (
+ <>
+
+
+
+ >
+ )
+ }
+
+ return (
+ onSortChange(column)}
+ >
+ {children}
+ {getSortIcon()}
+
+ )
+}
+TableHeadSort.displayName = 'TableHeadSort'
+
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes
@@ -96,4 +167,14 @@ const TableCaption = React.forwardRef<
))
TableCaption.displayName = 'TableCaption'
-export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow }
+export {
+ Table,
+ TableBody,
+ TableCaption,
+ TableCell,
+ TableFooter,
+ TableHead,
+ TableHeader,
+ TableHeadSort,
+ TableRow,
+}
diff --git a/turbo.json b/turbo.json
index 1888dac5b6e..66455bb6507 100644
--- a/turbo.json
+++ b/turbo.json
@@ -197,7 +197,8 @@
"NOTION_SUPASQUAD_APPLICATIONS_DB_ID",
"CUSTOMERIO_SITE_ID",
"CUSTOMERIO_API_KEY",
- "CUSTOMERIO_APP_API_KEY"
+ "CUSTOMERIO_APP_API_KEY",
+ "NEXT_RUNTIME"
],
"outputs": [".next/**", "!.next/cache/**", ".contentlayer/**"]
},
@@ -220,7 +221,9 @@
"NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID",
"NEXT_PUBLIC_GOTRUE_URL",
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
- "NEXT_PUBLIC_USERCENTRICS_RULESET_ID"
+ "NEXT_PUBLIC_USERCENTRICS_RULESET_ID",
+ "SUPABASE_MANAGEMENT_API_TOKEN",
+ "OPENAI_API_KEY"
],
"outputs": [".next/**", "!.next/cache/**", ".contentlayer/**"]
},