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:

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() {

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() { 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 = () => {
- King's Treasury + King's Treasury - People's happiness + People's happiness
- King's Treasury + King's Treasury - People's happiness + People's happiness
- 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) => (
{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 ( + <> +
+ + {enableCreationOfTablesFromDashboard && ( + + +
+ + + + + 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 ( + + +
+ + + Create a new table + + + +
+ ( + + + { + field.onChange(value) + form.resetField('newNamespace') + }} + > + + + + + {namespaces.map((x) => ( + + {x} + + ))} + {namespaces.length > 0 && } + +
+ +

Create a new namespace

+
+
+
+
+
+
+ )} + /> + {namespace === NEW_NAMESPACE_MARKER && ( + ( + + + + + + )} + /> + )} +
+ + + + {!!namespace && ( +
+ ( + + + + + + )} + /> + +
+
+

Columns

+ +
+ {columns.length === 0 ? ( +
+ Add a column to your table +
+ ) : ( + <> +
+

Name

+

Type

+
+ {columns.map((_, idx) => { + const columnType = form.watch(`columns.${idx}.type`) + const additionalFields = + COLUMN_TYPE_FIELDS[columnType as keyof typeof COLUMN_TYPE_FIELDS] ?? [] + + return ( + +
+ ( + + + + )} + /> + ( + + + + + + + {COLUMN_TYPES.map((x) => ( + + {x} + + ))} + + + + )} + /> +
+
+ + {additionalFields.length > 0 && ( +
+
+ {additionalFields.map((x, index) => ( +
+
+ {x.name} +
+ ( + + + + )} + /> +
+ ))} +
+
+
+ )} +
+ + ) + })} + + )} +
+
+ )} + + + + + + + + + + + ) +} 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 ( - - - - - - - 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 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} + /> + + + + + + +
+
+ ) +} 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
-
+
- + +
- - - {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 = ({ - - - - - 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={} + /> + + + + + + + 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)} /> - ) : ( -
-
- Buckets -
-
- setFilterString(e.target.value)} - icon={} - /> + {isSuccessBuckets && ( + <> + {bucketsList.length === 0 ? ( + setVisible(true)} /> + ) : ( +
+
+ Buckets +
+
+ 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). + +
+