diff --git a/apps/design-system/content/docs/components/table.mdx b/apps/design-system/content/docs/components/table.mdx
index ab2cc6bf5dc55..6dcafe2b601f3 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/registry/default/example/dialog-demo.tsx b/apps/design-system/registry/default/example/dialog-demo.tsx
index 68e81be28d595..3316817aea995 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/page-layout-detail.tsx b/apps/design-system/registry/default/example/page-layout-detail.tsx
index 6553522b8ed09..a5036cca81c4b 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 2de1b7fdfebe8..86c2d7006643d 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 d7fe367d0cefe..d5f24f153b02f 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 15c95473fd210..4d0ae0e529e79 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 60ec59526bc6c..3ea54c3a470b8 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 0698c475194e7..7a91dfda84446 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/docs/instrumentation.ts b/apps/docs/instrumentation.ts
index 3063091e693ee..b1d3d20b2228b 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/studio/.github/eslint-rule-baselines.json b/apps/studio/.github/eslint-rule-baselines.json
index a2729485e4510..016422c7cf99d 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/components/interfaces/Account/AuditLogs.tsx b/apps/studio/components/interfaces/Account/AuditLogs.tsx
index 35a1ed4e3a6c4..9b77f7a6460bf 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/Auth/OAuthApps/OAuthAppsList.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx
index 8e77b34ab23d3..9db1788627d8f 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) => (
) => {
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 992311dd4ca05..99aaf046f8bf3 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.schema.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts
index 789a42f1dbc25..a93062840e895 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 4de34f0e8550c..49bc1820a7089 100644
--- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx
+++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx
@@ -256,6 +256,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 +275,7 @@ export function DiskManagementForm() {
}
if (
+ !isAwsK8s &&
!isAwsNimbus &&
(payload.growthPercent !== form.formState.defaultValues?.growthPercent ||
payload.minIncrementGb !== form.formState.defaultValues?.minIncrementGb ||
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx
index 5845431ffb27f..161057c79951e 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/Organization/AuditLogs/AuditLogs.tsx b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx
index 69b50dc194cb1..56d2ed914a013 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/Usage/Activity.tsx b/apps/studio/components/interfaces/Organization/Usage/Activity.tsx
index 394694da3a697..4ee8fc903666b 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 d9988db578c5d..8a20726ccc19a 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 ebaa53b183774..0887860f75a4c 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/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx
index 9e5bf136e9aab..ec443ebbdd7b5 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 {
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/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketHeader.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketHeader.tsx
index d2802ff321201..033fd0c409d1e 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 d09baff9804ba..67d0494b2ccc8 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 0000000000000..9f5d0d1b61f29
--- /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 0000000000000..63389dbc20d08
--- /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 0000000000000..d9aac58c84323
--- /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 0000000000000..fea88e742e15b
--- /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 (
+
+
+
+
+
+ )
+}
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 19c08f08015cd..0000000000000
--- 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 0000000000000..451d30b25d949
--- /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 cb66723ac1020..0bfc7b9fc9d6f 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 12c9c4ee83d96..2294c4102fec2 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 ae2196de2bca2..0923b10b8f578 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 3d7e2772b7674..6ca9035d2d9c4 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{' '}
-
-
- )}
-
-
- setFilterString(e.target.value)}
- icon={ }
- />
-
-
+ {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
+ {isLoadingBuckets ? (
+
+ ) : (
+
+
+
+
+ {analyticsBuckets.length > 0 && (
+
+ Icon
+
+ )}
+ Name
+ Created at
+
+ Actions
- )}
- 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}
-
-
-
-
-
-
+
+ {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 0650510f289cb..aea27e3fc7a65 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 faa4c0c4ef3ac..e6b5ed6b8ffcb 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 0000000000000..f356f155c7be3
--- /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/VectorBuckets/CreateVectorBucketDialog.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx
index 2ab963bcf53f4..b5d9bf56a0959 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/index.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx
index 14e1fee38eb4a..b8585792effd8 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 1527dac75c2f4..3f1ca183d7f02 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/data/analytics/org-daily-stats-query.test.ts b/apps/studio/data/analytics/org-daily-stats-query.test.ts
new file mode 100644
index 0000000000000..a3636d90758c9
--- /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 1a3c05ac6e2d1..d2742f603d681 100644
--- a/apps/studio/data/analytics/org-daily-stats-query.ts
+++ b/apps/studio/data/analytics/org-daily-stats-query.ts
@@ -137,7 +137,7 @@ export async function getOrgDailyStats(
query: {
start: startDate,
end: endDate,
- projectRef,
+ project_ref: projectRef,
},
},
signal,
diff --git a/apps/studio/data/organizations/organization-audit-logs-query.ts b/apps/studio/data/organizations/organization-audit-logs-query.ts
index 1701003f185fc..23d6368a052d1 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/storage/iceberg-namespace-create-mutation.ts b/apps/studio/data/storage/iceberg-namespace-create-mutation.ts
index fc638a387ff3c..86a1bd897841d 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`)
-
- 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')
+ if (!projectRef) throw new Error('projectRef is required')
- 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 86bbc58faf2ba..43d3e55b4ff5f 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`)
-
- 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')
+ if (!projectRef) throw new Error('projectRef is required')
- 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 0000000000000..4b5ae9df42481
--- /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 23ebca378629c..cd16fb305d590 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`)
-
- 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')
+ if (!projectRef) throw new Error('projectRef is required')
- 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 e5edcf0bdcaa9..fd699bb6674c1 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,
- 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 be5e4057388ac..651efe030547d 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 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 952d0a59059f3..fb9bf4a931533 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/usage/org-usage-query.test.ts b/apps/studio/data/usage/org-usage-query.test.ts
new file mode 100644
index 0000000000000..e694790dffc80
--- /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/package.json b/apps/studio/package.json
index 649d785f98eb1..70a96386ac168 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/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 dea6dd62212db..9d287ed00d138 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 056ada53d7960..9b5d77b118715 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 5a59b834113e0..8765ff067b9f4 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 9c8de65bc50d4..4727e7231e90e 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 070c606980451..a6b5135fbead5 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 fee362e2b8bd1..8140126b186cd 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 0000000000000..04eef79ac3609
--- /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 0000000000000..dec76d78f64e6
--- /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 a852693b37c36..ce04574eddb02 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 && (
diff --git a/apps/www/components/Blog/FeaturedThumb.tsx b/apps/www/components/Blog/FeaturedThumb.tsx
index 641b33d36592c..5172ddf3c86c2 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/lib/authors.json b/apps/www/lib/authors.json
index a45255368af82..d8b417c154773 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 0000000000000..0d6c64131be35
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 0000000000000..19c7148f2c4c8
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 0000000000000..6753f113f75b3
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 0000000000000..8a2b59666165b
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 316b760b6f5e7..fe20b729a5b56 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/common/configcat.ts b/packages/common/configcat.ts
index c70d83a695cc9..0316badb37f18 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 667ffba631de0..b59b28777b2af 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 fefe89672164d..d56edc29c5a73 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 1888dac5b6e9e..66455bb6507cd 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/**"]
},